首页
/ PyTorch C++ CUDA 实用工具函数完全指南:设备查询、库句柄、cuDNN 描述符与流管理

PyTorch C++ CUDA 实用工具函数完全指南:设备查询、库句柄、cuDNN 描述符与流管理

2026-09-07 19:53:48作者:毕习沙Eudora

导读

PyTorch 的 C++ 前端(libtorch / ATen)在底层封装了大量 CUDA 运行时与 cuBLAS/cuDNN/cuSPARSE/cuSOLVER 等数学库的句柄,用于统一管理设备、流和描述符资源。本文以 docs/cpp/source/api/cuda/utilities.md 为核心骨架,结合仓库源码带你逐层剖析 torch::cuda / c10::cuda / at::cuda 的完整工具面:如何在 C++ 扩展中查询 CUDA 设备与属性、判断 P2P 访问能力、获取当前流的 cuBLAS/cuSPARSE/cuSOLVER 句柄,如何使用 RAII 封装类安全地操作 cuDNN 描述符,以及如何创建、切换、包装 CUDA 流。读完本文,你将具备在不依赖 Python 层的纯 C++ 场景下编写自定义 CUDA kernel 时所需的所有“环境接线”能力。

本文所引用的源码均位于本仓库的 c10/cudaaten/src/ATen/cudaaten/src/ATen/cudnn 目录下,可对照原文逐条核实。


一、Device Management:查询设备数量与当前设备

CUDA 设备管理的基础操作是“这台机器上有几块卡、我现在在操作哪块卡”。PyTorch 在 C++ 层将这些封装为无错误码、直接抛 C++ 异常的友好接口,其命名约定与 Python 端 torch.cuda 保持一致(c10/cuda/CUDAFunctions.h 的注释明确说明了这一点)。

1.1 核心函数与语义

函数 命名空间 返回值 语义
c10::cuda::device_count() c10::cuda DeviceIndex(即 int 返回可用的 CUDA 设备数量;驱动出问题时不会抛错,而是返回 0
c10::cuda::device_count_ensure_non_zero() c10::cuda DeviceIndex device_count() 相同,但检测不到设备时抛异常
c10::cuda::current_device() c10::cuda DeviceIndex 返回当前 CUDA 设备的索引
c10::cuda::set_device(device, force) c10::cuda void 切换当前设备,force=true 时即使该设备尚未初始化上下文也会强制设置

device_count() 的“失败返回 0”行为是有意设计而非疏漏。源码注释(c10/cuda/CUDAFunctions.h)记录了这一决策过程:历史上该函数在驱动异常时是否报错并不一致,而实践表明用户几乎从不希望它失败——驱动有问题时返回 0 即可。但它第一次被调用时仍可能打印一条警告日志。

在同一头文件中,c10::cuda 还保留了原始的 cudaError_t 返回风格 API(GetDeviceCountGetDeviceSetDeviceExchangeDevice 等)以及 device_synchronize()stream_synchronize()memcpy_and_sync() 等封装。其中 memcpy_and_syncstream_synchronize 之所以定义为 inline,是为了在同步发生时能先检查全局 SyncDebugModeL_DISABLED / L_WARN / L_ERROR),再决定是静默、警告还是报错,从而帮助定位隐式同步导致的性能问题。

兼容性提示:在 USE_ROCM 构建下,同一头文件会通过 namespace c10::hip { using c10::cuda::...; } 提供向后兼容别名,因此 CUDA 代码的符号在 HIP 环境下依然可解析(见 c10/cuda/CUDAFunctions.h)。

1.2 完整示例

#include <c10/cuda/CUDAFunctions.h>

// 检查可用设备(失败不会抛错,仅返回 0)
int num_devices = c10::cuda::device_count();

// 若要求必须有设备,可用会抛异常的版本
// int num_devices = c10::cuda::device_count_ensure_non_zero();

// 获取当前设备
int current = c10::cuda::current_device();

需要说明的是,原文示例中第一行调用的 c10::cuda::device_count() 在 C++ 标准中同样成立——PyTorch 在 c10/cuda/CUDAFunctions.h 中将其声明于 c10::cuda 命名空间。文档写作时将其归入 torch::cuda 仅为表述方便,编译时以 c10::cuda 为准。


二、Device Properties:查询设备属性与 P2P 访问能力

拿到设备索引后,下一步通常是读取该设备的硬件属性(如名称、计算能力、显存等)或判断两块卡之间能否直接对等访问(Peer-to-Peer)。

2.1 函数声明与实际位置

在源码中,这组函数声明于 aten/src/ATen/cuda/CUDAContextLight.h,其中 CUDAContextLight.h 是一个“少传递包含”的精简头文件,被 aten/src/ATen/cuda/CUDAContext.h 反向包含并对外兼容。因此,用户代码中 #include <ATen/cuda/CUDAContext.h> 即可使用全部声明:

函数 签名 说明
at::cuda::getCurrentDeviceProperties() 返回 cudaDeviceProp* 返回当前设备的只读属性结构体指针
at::cuda::getDeviceProperties(device) 返回 cudaDeviceProp*,参数 c10::DeviceIndex 返回指定设备的属性结构体指针
at::cuda::canDeviceAccessPeer(device, peer_device) 返回 bool 查询 device 能否访问 peer_device 的显存
at::cuda::warp_size() 返回 int 返回当前设备的 warp 大小(NVIDIA 上通常为 32)

属性查询的实际实现位于 aten/src/ATen/cuda/CUDAContext.cppcudaDeviceProp 由 CUDA 运行时填充,因此其全部字段(namemajorminortotalGlobalMemmultiProcessorCount 等)均可直接读取。ATen 内部(例如 kernel launch 网格计算、cub.cuh 等基础设施)就大量依赖 getCurrentDeviceProperties() 来读取 major/minormaxThreadsPerBlock

2.2 示例代码

#include <ATen/cuda/CUDAContext.h>
#include <iostream>

// 查询当前设备的属性
cudaDeviceProp* props = at::cuda::getCurrentDeviceProperties();
std::cout << "Device: " << props->name << std::endl;
std::cout << "Compute capability: " << props->major << "." << props->minor << std::endl;

// 查询指定设备(例如索引为 1 的卡)
cudaDeviceProp* dev1_props = at::cuda::getDeviceProperties(1);

// 判断设备 0 与设备 1 之间能否进行 P2P 访问
bool can_access = at::cuda::canDeviceAccessPeer(0, 1);

// 读取当前设备的 warp 大小
int ws = at::cuda::warp_size();

补充一个与设备属性相关的便捷接口:at::cuda::is_available() 等价于 c10::cuda::device_count() > 0(见 aten/src/ATen/cuda/CUDAContextLight.h)。若编译期就未启用 CUDA 或驱动不可用,它返回 false 而非抛错,适合做运行时能力探测。


三、Library Handles:获取 cuBLAS / cuBLASLt / cuSPARSE / cuSOLVER 句柄

PyTorch 内部所有 cuBLAS、cuSPARSE 运算都复用了按“(设备, 流)”维度缓存的句柄池。当你在自定义 CUDA kernel 中需要直接调用这些数学库时,不应自己 cublasCreate 一个新的句柄,而应复用 PyTorch 已经与当前设备/当前流绑定的句柄——否则 kernel 执行的流与库调用所在的流不一致,会产生隐式同步甚至错误。

3.1 句柄函数一览

这些函数同样声明于 aten/src/ATen/cuda/CUDAContextLight.h

函数 返回类型 底层库
at::cuda::getCurrentCUDABlasHandle(bool setup = true) cublasHandle_t cuBLAS
at::cuda::getCurrentCUDABlasLtHandle() cublasLtHandle_t cuBLASLt(用于轻量/定制 GEMM)
at::cuda::getCurrentCUDASparseHandle() cusparseHandle_t cuSPARSE
at::cuda::getCurrentCUDASolverDnHandle() cusolverDnHandle_t cuSOLVER(稠密线性求解)

句柄池的分配与按流复用逻辑分别实现在 aten/src/ATen/cuda/CublasHandlePool.cppaten/src/ATen/cuda/CuSparseHandlePool.cpp。正因为句柄与当前 CUDA 流强绑定,任何获取句柄前都要确保当前设备与期望流已就绪——这也是后文“Stream Management”一节的必要性所在。

CUDAContextLight.h 中还暴露了一组工作区(workspace)管理接口(clearCublasWorkspacesgetCUDABlasLtWorkspacesetCUDABlasLtWorkspaceSize 等),用于控制 cuBLASLt 在按流缓存的工作区内分配临时内存;常规用户一般不需要直接调用它们,但了解其存在有助于理解 PyTorch 为何能在同一设备上并行执行多个流上的 cuBLAS 调用。

3.2 示例代码

#include <ATen/cuda/CUDAContext.h>

// 获取与“当前设备 + 当前流”绑定的 cuBLAS 句柄
cublasHandle_t handle = at::cuda::getCurrentCUDABlasHandle();

// 获取 cuSPARSE 句柄
cusparseHandle_t sparse_handle = at::cuda::getCurrentCUDASparseHandle();

拿到句柄后即可调用 cublasXgemmcusparseXcsrmm 等原生 API,PyTorch 已把底层错误转换成 C++ 异常抛出(C10_CUDA_CHECK / CUDACHECK),无需手动检查返回码。


四、cuDNN Descriptors:用 RAII 安全管理描述符生命周期

cuDNN 的 API 风格要求先创建描述符对象(如 cudnnTensorDescriptor_t),用完后显式销毁。手写时极易遗忘释放或拷贝出错,PyTorch 因此在头文件 aten/src/ATen/cudnn/Descriptors.h 中提供了基于 RAII(资源获取即初始化)的封装族,默认构造为空指针、首次使用时通过 mut_desc() 惰性初始化、desc() 提供只读访问,析构时自动调用对应的 cudnnDestroyXxx

4.1 Descriptor 基类

at::native::Descriptor(声明于 aten/src/ATen/cudnn/Descriptors.h)是一个模板基类:类型参数决定了它包裹哪种 cuDNN 句柄以及使用哪对 create/destroy 函数。它内部通过 DescriptorDeleter(同文件 L78 附近)在析构时释放底层描述符。所有具体描述符类都继承它并指定各自的 cudnnCreate* / cudnnDestroy* 回调。要点:

  • 默认构造后内部句柄为 nullptr,不占用任何 cuDNN 资源;
  • 第一次调用 mut_desc() 时才真正创建底层描述符;
  • 平时只读场景统一走 desc(),返回原生 cudnnXxxDescriptor_t 供 cuDNN API 使用。

4.2 具体描述符类

下列类全部定义在 aten/src/ATen/cudnn/Descriptors.h

包裹的原生类型 用途与注意点
at::native::TensorDescriptor cudnnTensorDescriptor_t 描述 4D/5D 张量布局;构造时可用 pad 参数把低维张量“填充”到 cuDNN 广播所需维度(见 L137-L143 的 TensorDescriptor(const at::Tensor& t, size_t pad = 0)
at::native::FilterDescriptor cudnnFilterDescriptor_t 描述卷积滤波核(权重)的维度与数据类型(L178 附近)
at::native::ConvolutionDescriptor cudnnConvolutionDescriptor_t 配置卷积的 padding、stride、dilation、groups 与数学模式(TF32、tensor ops 等)
at::native::RNNDataDescriptor cudnnRNNDataDescriptor_t 描述变长序列数据(L125 起,set 方法接收 maxSeqLength/batchSize/vectorSize/seqLengthArray
at::native::RNNDescriptor cudnnRNNDescriptor_t 描述 RNN 结构,内部组合持有 DropoutDescriptor(L268 起)
at::native::DropoutDescriptor cudnnDropoutDescriptor_t 管理 cuDNN dropout 的 RNG 状态、dropout 概率与种子;仓库中对 set/restore/disable 三条路径分别封装(L230 起)
at::native::ActivationDescriptor cudnnActivationDescriptor_t 描述激活函数模式与参数
at::native::SpatialTransformerDescriptor cudnnSpatialTransformerDescriptor_t 用于空间变换网络(STN)的采样网格描述(L219)
at::native::CTCLossDescriptor cudnnCTCLossDescriptor_t 描述 CTC loss 的数据类型、归一化与梯度模式,且按 cuDNN 版本选择 set / v8 / v9 接口(L345-L374)

从实现细节可以看出这些封装并非简单套壳:例如 DropoutDescriptor::set 会申请并维护 dropout RNG state 张量,set_dropout 在 dropout 概率为 0 时走廉价路径(注释明确指出“empirically cudnnSetDropoutDescriptor is cheap when…”);CTCLossDescriptor 则按编译期 cuDNN 版本分支,兼容 v8 与 v9 两代 API。这说明在自行复刻此类封装时需留意 cuDNN 版本差异。

4.3 TensorDescriptor 使用示例

#include <ATen/cudnn/Descriptors.h>
#include <ATen/ATen.h>

// 构造一个 4D 输入张量(N=32, C=3, H=W=224),位于 CUDA 上
at::Tensor input = torch::randn({32, 3, 224, 224}, torch::kCUDA);

// 用 RAII 封装描述它
at::native::TensorDescriptor desc(input);

// 取出原生句柄交给 cuDNN API 使用
cudnnTensorDescriptor_t raw = desc.desc();

需要注意头文件路径:源码中位于 aten/src/ATen/cudnn/Descriptors.h,因而在构建时通常写作 #include <ATen/cudnn/Descriptors.h>aten/src/ATen 为 include 根)。如需打印描述符内容用于调试,TensorDescriptorFilterDescriptor 还重载了 operator<<


五、Stream Management:创建、切换与包装 CUDA 流

PyTorch 的流管理全部集中在 c10::cuda 命名空间的 c10/cuda/CUDAStream.h 中。所有句柄类函数(见上文第三节)都以“当前流”为基准工作,因此理解并正确设置流,是保证自写 kernel 与 PyTorch 内部计算正确串并行关系的前提。

5.1 核心函数语义

函数 说明
c10::cuda::getDefaultCUDAStream(device_index = -1) 获取指定设备的默认流(device_index=-1 表示当前设备)
c10::cuda::getCurrentCUDAStream(device_index = -1) 获取当前线程在指定设备上正在使用的流;若有人调用过 setCurrentCUDAStream 或使用过 StreamGuard,它会与默认流不同
c10::cuda::setCurrentCUDAStream(stream) 把给定流设为当前线程的当前流,后续 CUDA 操作默认进入该流
c10::cuda::getStreamFromPool(isHighPriority = false, device = -1) 从 PyTorch 内部的流池中取出一条(低优先级/高优先级)流
c10::cuda::getStreamFromPool(priority, device = -1) 重载:直接以数值优先级从流池取流
c10::cuda::getStreamFromExternal(ext_stream, device_index) 把外部(第三方库或裸 CUDA 创建的)cudaStream_t 包装成 CUDAStream 对象,纳入 PyTorch 统一管理

对应声明见 c10/cuda/CUDAStream.h。此外该头文件还提供了 CUDAStreamGuardOptionalCUDAStreamGuard 等 RAII 守卫类,用于在作用域内临时切换当前流并在退出时自动恢复。

5.2 完整示例

#include <c10/cuda/CUDAStream.h>

// 1. 从流池创建一条自定义流(低优先级)
auto stream = c10::cuda::getStreamFromPool();
// 高优先级版本:
// auto high_prio = c10::cuda::getStreamFromPool(true);

// 2. 把它设为当前线程的当前流
c10::cuda::setCurrentCUDAStream(stream);

// 3. 再取回默认流(用于回到主路径)
auto default_stream = c10::cuda::getDefaultCUDAStream();

// 4. 包装一条外部创建的裸 CUDA 流,使其可被 PyTorch 管理
cudaStream_t ext_stream;
cudaStreamCreate(&ext_stream);
auto wrapped = c10::cuda::getStreamFromExternal(ext_stream, /*device_index=*/0);

// 显式同步 / 等待(需要时)
// c10::cuda::device_synchronize();

注意 getStreamFromExternal 不会替你释放外部流的资源——它只是建立一层可被 at::cuda 句柄池识别的包装;裸流的生命周期仍需由创建方管理。这是与 getStreamFromPool(池内流由 PyTorch 统一回收)最大的差别。

5.3 与上文功能的联动

  • 自定义 kernel 调 cuBLAS:先在目标流上 setCurrentCUDAStream,再 getCurrentCUDABlasHandle(),即可保证 kernel 与库调用落在同一条流上。
  • 自写 kernel 使用外部推理引擎的流:用 getStreamFromExternal 包装后,直接调用 at::cuda 各函数而无需关心流的 CUDA 类型差异。
  • ROCm 环境c10/cuda/CUDAStream.h 同样在文件尾部提供 c10::hip 别名(如 getDefaultHIPStream 指向同一实现),确保跨厂商一致性。

六、相关头文件与进一步阅读指引

关注点 头文件 / 源码 说明
设备管理 c10/cuda/CUDAFunctions.h device_countcurrent_deviceset_device、同步与同步调试模式
设备属性与句柄声明 aten/src/ATen/cuda/CUDAContextLight.h 属性查询、P2P 判断、warp_size、四个数学库句柄及工作区管理
对外兼容入口 aten/src/ATen/cuda/CUDAContext.h #include <ATen/cuda/CUDAContext.h> 即可获得上述全部声明
属性查询实现 aten/src/ATen/cuda/CUDAContext.cpp getCurrentDeviceProperties 等的具体实现
句柄池实现 aten/src/ATen/cuda/CublasHandlePool.cppaten/src/ATen/cuda/CuSparseHandlePool.cpp cuBLAS / cuSPARSE 句柄按流缓存与复用
流管理 c10/cuda/CUDAStream.h 默认流、当前流、流池、外部流包装与守卫类
cuDNN 描述符 aten/src/ATen/cudnn/Descriptors.h Descriptor 基类与全部具体描述符的 RAII 实现

若要进一步阅读 PyTorch C++ API 的其他 CUDA 相关主题,可参考本仓库 docs/cpp/source 下的文档体系,以及 C++ 扩展的单元测试目录 test/cpp_extensions(其中包含大量调用这些底层接口的示例代码)。


结语

PyTorch 的 C++ CUDA 工具层为开发者屏蔽了三类最易出错的底层工作:设备的枚举与属性探测、数学库句柄的按流复用、以及 cuDNN 描述符的 RAII 生命周期管理。理解 c10::cuda(设备与流)与 at::cuda / at::native(属性、句柄、描述符)两层命名空间的边界,是在 libtorch 生态中编写高性能且线程/流安全的自定义 CUDA 算子的关键。以官方文档 docs/cpp/source/api/cuda/utilities.md 为索引、以上文给出的源码位置为地图,你便可以快速把每一个工具函数落实到自己的 kernel 实现中。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395