PyTorch C++ 前端 CUDA 编程全指南:设备、Stream 与 Guards/工具 API 实战解析
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
本篇技术指南以 PyTorch 仓库的官方 C++ 文档《CUDA Support》系列(入口文档及其子篇 streams、guards、utilities)为主体,系统讲解在 LibTorch C++ 扩展中如何选择 GPU 设备、借助 CUDA Stream 实现异步执行、通过 RAII Guard 管理 device/stream 上下文,以及调用 cuBLAS/cuDNN 底层句柄与 descriptor。读完本文,你将掌握从单卡 Tensor 分配、多卡切换、流池取流到自定义 CUDA 算子中直接驱动 cuBLAS/cuDNN 的一整套可落地的工程写法,并理解其背后的源码实现原理。
何时需要使用 C++ CUDA API
PyTorch 在 C++ 侧(LibTorch /torch::前端)提供了完整的 CUDA 支持,覆盖张量运算、设备管理与异步内核调度。官方文档给出的典型使用场景如下:
- 需要显式控制使用哪一块 GPU 设备(例如多卡推理服务中把不同请求分派到不同卡);
- 正在实现自定义 CUDA kernel 或自定义算子(Custom Operator),需要把设备、流与底层库句柄对齐到正确上下文;
- 希望通过异步 Stream 执行重叠计算与数据传输、追求极致吞吐;
- 正在管理 multi-GPU 工作负载,例如数据并行训练时在每张卡上分别构造并执行图。
事实说明:这里的
torch::cuda::is_available()、torch::device(torch::kCUDA)等能力由 ATen / c10 运行时提供。本文所有 API 均以当前仓库main分支实际存在的头文件为准(详见各小节“源码位置”)。
快速上手的典型调用链
官方文档给出的一段"最小完整"示例涵盖了"可用性探测 → 建张量 → 切设备 → 取流 → 搬模型"五个步骤:
#include <torch/torch.h> #include <c10/cuda/CUDAGuard.h> // Check if CUDA is available if (torch::cuda::is_available()) { // Create tensor on GPU auto tensor = torch::randn({2, 3}, torch::device(torch::kCUDA)); // Switch to a specific GPU c10::cuda::CUDAGuard guard(0); // Use GPU 0 // Get the current CUDA stream auto stream = c10::cuda::getCurrentCUDAStream(); // Move model to GPU model->to(torch::kCUDA); }其中几个关键点值得展开:
c10::cuda::CUDAGuard guard(0)接受整数设备索引,把它解释为 CUDA 设备号。根据 CUDAGuard.h 源码注释,它是DeviceGuard的 CUDA 特化:相比通用的DeviceGuard编译后是直落的cudaSetDevice/cudaGetDevice调用,效率更高,但只允许在直接链接 CUDA 的代码里使用。torch::device(torch::kCUDA)只声明“用 CUDA”,实际落到哪张卡取决于当时的 current device,通常配合CUDAGuard或 Python 侧的cuda.set_device语义。model->to(torch::kCUDA)会把模型全部参数/缓冲区搬到当前设备——因此若存在并发/多卡场景,必须在合适的 device guard 作用域内调用,避免参数被搬到错误设备。
核心头文件地图
本文涉及的功能分布在四个头文件中,文档列出的划分如下(仓库内均可直接 include):
| 头文件 | 用途 | 仓库实际路径 |
|---|---|---|
c10/cuda/CUDAStream.h | CUDA 流管理与获取 | c10/cuda/CUDAStream.h |
c10/cuda/CUDAGuard.h | CUDA 设备/流 RAII Guard | c10/cuda/CUDAGuard.h |
ATen/cuda/CUDAContext.h | CUDA 上下文、设备属性、cuBLAS/cuSPARSE 等句柄 | aten/src/ATen/cuda/CUDAContext.h(include 根为aten/src) |
ATen/cudnn/Descriptors.h | cuDNN tensor/conv 等 descriptor RAII 封装 | aten/src/ATen/cudnn/Descriptors.h |
说明:仓库中 ATen 源码位于
aten/src/ATen/,所以文档里写的 include 名ATen/cuda/CUDAContext.h、ATen/cudnn/Descriptors.h对应文件在 aten/src/ATen/cudnn/Descriptors.h 等位置,编译时把aten/src加入 include path 即可。c10 系列头文件(CUDAStream/CUDAGuard/CUDAFunctions)与 include 名一致,直接在 c10/cuda 目录下。
CUDA Stream 详解:异步执行的核心
CUDA Stream 提供了 GPU 上异步执行的机制:投递到同一条 stream 上的操作按序执行;不同 stream 上的操作则可以并发执行。PyTorch C++ 侧用CUDAStream这个“值对象”抽象底层cudaStream_t。
CUDAStream 的三种获取途径
官方文档 streams 篇 将“获取流”归纳为三种方式:
1. 从流池获取(round-robin 分配)
// Normal priority stream at::cuda::CUDAStream stream = at::cuda::getStreamFromPool(); // High priority stream at::cuda::CUDAStream high_prio = at::cuda::getStreamFromPool(/*isHighPriority=*/true); // Stream for specific device at::cuda::CUDAStream dev1_stream = at::cuda::getStreamFromPool(false, /*device=*/1);getStreamFromPool的签名是getStreamFromPool(bool isHighPriority = false, DeviceIndex device = -1),device传-1表示当前设备。
2. 默认流(大部分计算所在的流)
at::cuda::CUDAStream defaultStream = at::cuda::getDefaultCUDAStream();3. 当前流(可能被 Guard 改写过)
at::cuda::CUDAStream currentStream = at::cuda::getCurrentCUDAStream();这三个函数(含getStreamFromExternal、setCurrentCUDAStream)在 CUDAStream.h 中声明,例如:
C10_CUDA_API CUDAStream getDefaultCUDAStream(DeviceIndex device_index = -1); C10_CUDA_API CUDAStream getCurrentCUDAStream(DeviceIndex device_index = -1);流池机制(源码级原理)
CUDAStream.h 顶部的 “Stream pool note” 详细解释了流池设计,这是理解 PyTorch CUDA 编程模型的关键:
- 每张设备有三个池,懒加载创建;
- 第一个池只含 default stream,请求 default stream 时直接返回;
- 第二个池是“低优先级(默认优先级)”流,每设备 32 条,轮询(round-robin)返回:第 1 次请求拿到 index 0,第 2 次拿 index 1 …… 到 31 后回到 0。因此若同时申请超过 32 条低优先级流,第 1 条与第 33 条实际是同一条底层流,其上排队的内核无法并发——这是设计上的显式取舍;
- 第三个池是高优先级流,机制与第二个池相同,只是创建时使用更高优先级;
- 池的成本启示:流的获取与释放成本近似为零,应优先使用大量短生命周期流;若在性能关键路径上需要长期占用多条流,则当前机制可能需要扩展出“预留子池”的能力;
- 流池是全局(跨线程共享)的,而“某设备当前流”是线程局部的——不同 OS 线程各有自己的 current stream;不过流本身是线程安全的,两个线程可以在同一条流上安全地排队内核。
设置当前流:两种写法
方式一:setCurrentCUDAStream(需手动还原)
torch::Tensor tensor0 = torch::ones({2, 2}, torch::device(torch::kCUDA)); // Get a new stream and set it as current at::cuda::CUDAStream myStream = at::cuda::getStreamFromPool(); at::cuda::setCurrentCUDAStream(myStream); // Operations now use myStream tensor0.sum(); // Restore default stream at::cuda::setCurrentCUDAStream(at::cuda::getDefaultCUDAStream());方式二:CUDAStreamGuard(推荐,RAII 自动还原)
torch::Tensor tensor0 = torch::ones({2, 2}, torch::device(torch::kCUDA)); at::cuda::CUDAStream myStream = at::cuda::getStreamFromPool(); { at::cuda::CUDAStreamGuard guard(myStream); // Operations use myStream within this scope tensor0.sum(); } // Stream automatically restored to default两种方式语义等价,但 Guard 写法把“还原”交给析构函数,避免异常路径上忘记还原导致后续代码跑错流。官方文档明确推荐 Guard 写法。
流的同步
auto stream = c10::cuda::getDefaultCUDAStream(); // ... 排队若干内核 ... stream.synchronize();CUDAStream::synchronize()会阻塞宿主机直到该流上所有已排队的操作完成;另有query()可非阻塞查询流是否空闲(两者均声明于 CUDAStream.h)。需要精确同步时,也可用cudaStreamSynchronize于底层cudaStream_t,因为CUDAStream提供到cudaStream_t的隐式转换。
包装外部创建的流
当需要把第三方库(如 NCCL、自建 CUDA 资源)创建的流接入 PyTorch 的 current stream 体系时,使用getStreamFromExternal:
cudaStream_t ext_stream; cudaStreamCreate(&ext_stream); auto wrapped = c10::cuda::getStreamFromExternal(ext_stream, /*device_index=*/0);CUDA Guards:RAII 管理 device / stream 上下文
PyTorch C++ 的 CUDA Guards 是 RAII 包装器:构造时把指定 CUDA 设备或流设为当前上下文,析构时自动恢复此前上下文,从而保证函数/作用域不“污染”调用方的设备与流状态。详见 guards 篇。
CUDAGuard:切换设备
#include <c10/cuda/CUDAGuard.h> { c10::cuda::CUDAGuard guard(1); // Switch to device 1 // All CUDA operations here run on device 1 auto tensor = torch::zeros({2, 2}, torch::device(torch::kCUDA)); } // Previous device is restored从 CUDAGuard.h 的实现看:
CUDAGuard可接收DeviceIndex(整数)或Device构造,接收非 CUDA 的Device会直接报错;- 拷贝/移动构造均被
delete(RAII 不可搬移),保证生命周期严格绑定作用域; - 内部持有
c10::impl::InlineDeviceGuard<impl::CUDAGuardImpl>,并提供set_device/reset_device/set_index在运行中改变目标,original_device()返回构造时的设备,current_device()返回最近一次set_device设置的设备(若无则返回构造参数)。
CUDAStreamGuard:同时切换 device + stream
#include <c10/cuda/CUDAGuard.h> auto stream = c10::cuda::getStreamFromPool(); { c10::cuda::CUDAStreamGuard guard(stream); // Operations here use the specified stream } // Previous stream is restored注意CUDAStreamGuard与CUDAGuard的语义差异(下节“三种模式”会再次强调):前者同时把 current device 切到该流所在设备并把 current stream 设为该流;后者只切设备不碰流。
OptionalCUDAGuard:按条件切换
c10::cuda::OptionalCUDAGuard guard; if (use_cuda) { guard.set_device(0); } // Guard only switches device if set_device was calledOptionalCUDAGuard支持默认构造(未初始化态),仅在显式调用set_device/reset_device后才真正切设备;对“可能使用 CUDA、也可能纯 CPU”的通用算子实现尤其有用。与之对应的还有OptionalCUDAStreamGuard(可选切换流的变体)。
CUDAMultiStreamGuard:一次性设置多设备流
at::cuda::CUDAStream stream0 = at::cuda::getStreamFromPool(false, 0); at::cuda::CUDAStream stream1 = at::cuda::getStreamFromPool(false, 1); { at::cuda::CUDAMultiStreamGuard multi_guard({stream0, stream1}); // stream0 is current on device 0, stream1 on device 1 } // Both streams restored官方文档对此给出了明确警告(attention块):
CUDAMultiStreamGuard不会改变 current device index。它只把传入的每条流设为其所在设备上的 current stream。
换言之它修改的是“各设备上的当前流”,而不是全局 current device;这一点在多卡代码里极易踩坑。
多设备 Stream 处理:三种典型模式
文档用一段“骨架代码”归纳了跨多卡管理流时最常用的三种模式(完整代码见 streams.md),核心流程与语义如下:
// Create stream vectors on device 0 std::vector<at::cuda::CUDAStream> streams0 = {at::cuda::getDefaultCUDAStream(), at::cuda::getStreamFromPool()}; at::cuda::setCurrentCUDAStream(streams0[0]); // Create stream vector on device 1 using CUDAGuard std::vector<at::cuda::CUDAStream> streams1; { at::cuda::CUDAGuard device_guard(1); streams1.push_back(at::cuda::getDefaultCUDAStream()); streams1.push_back(at::cuda::getStreamFromPool()); } at::cuda::setCurrentCUDAStream(streams1[0]);要点:为 device 1 获取流时必须先CUDAGuard切到设备 1(因为流是“属于某设备的”),这正好印证了“在设备上下文中创建该设备的流”的标准姿势。随后三种模式:
// Pattern 1: CUDAGuard changes current device only, not streams { at::cuda::CUDAGuard device_guard(1); // current device is 1, current stream on device 1 is still streams1[0] } // Pattern 2: CUDAStreamGuard changes both current device and current stream { at::cuda::CUDAStreamGuard stream_guard(streams1[1]); // current device is 1, current stream is streams1[1] } // restored to device 0, stream streams0[0] // Pattern 3: CUDAMultiStreamGuard sets streams on multiple devices at once { at::cuda::CUDAMultiStreamGuard multi_guard({streams0[1], streams1[1]}); // current device unchanged (still 0) // stream on device 0 is streams0[1], stream on device 1 is streams1[1] } // streams restored to streams0[0] and streams1[0]三种模式的记忆口诀:
- Pattern 1(CUDAGuard):只改 current device,不改任何流;
- Pattern 2(CUDAStreamGuard):改 current device并改 current stream,作用域退出后 device 与 stream 一并还原;
- Pattern 3(CUDAMultiStreamGuard):不改 current device,但把每张传入设备上的 current stream 一次性设好,作用域退出后全部还原。
多卡数据并行、pipeline 并行等场景通常组合使用这三者:外层用CUDAMultiStreamGuard或逐卡CUDAStreamGuard建立每张卡的执行上下文,内层计算期间再临时用CUDAGuard做设备切换。
CUDA 工具函数:设备查询与库句柄
utilities 篇 集中讲解查询/管理设备、属性与底层库句柄的工具函数,编写自定义 kernel 时几乎是必用。
设备管理
#include <c10/cuda/CUDAFunctions.h> // Check available devices int num_devices = c10::cuda::device_count(); // Get current device int current = c10::cuda::current_device();device_count()、current_device()等设备级函数在 c10/cuda/CUDAFunctions.h 中声明(另有device_count_ensure_non_zero变体,用于要求至少存在一张设备否则抛错的场景)。c10::cuda::device_count()与 Python 侧torch.cuda.device_count()语义对应。
设备属性
#include <ATen/cuda/CUDAContext.h> // Query properties of the current device cudaDeviceProp* props = at::cuda::getCurrentDeviceProperties(); std::cout << "Device: " << props->name << std::endl; std::cout << "Compute capability: " << props->major << "." << props->minor << std::endl; // Query a specific device cudaDeviceProp* dev1_props = at::cuda::getDeviceProperties(1); // Check peer access bool can_access = at::cuda::canDeviceAccessPeer(0, 1);cudaDeviceProp是 CUDA Runtime 的标准结构体,字段包括name、major/minor(计算能力)、显存大小、多处理器数量等。canDeviceAccessPeer(a, b)对应 P2P(Peer-to-Peer)访问能力探测,常用于决定是否启用跨卡直连。
数学库句柄(cuBLAS / cuSPARSE / cuSOLVER)
在自定义 kernel 中需要直接调用 cuBLAS、cuSPARSE、cuSOLVER时,必须使用与当前设备、当前流绑定的句柄,否则算子会落到错误的上下文。文档列出四个核心 getter:
at::cuda::getCurrentCUDABlasHandle()—— cuBLASat::cuda::getCurrentCUDABlasLtHandle()—— cuBLASLtat::cuda::getCurrentCUDASparseHandle()—— cuSPARSEat::cuda::getCurrentCUDASolverDnHandle()—— cuSOLVER
用法示例:
#include <ATen/cuda/CUDAContext.h> // Get cuBLAS handle for current device/stream cublasHandle_t handle = at::cuda::getCurrentCUDABlasHandle(); // Get cuSPARSE handle cusparseHandle_t sparse_handle = at::cuda::getCurrentCUDASparseHandle();这些句柄由 ATen 的 CUDA 上下文(aten/src/ATen/cuda/下实现)按“当前设备 × 当前流”缓存并复用,保证直接调用底层库时与 PyTorch 上层算子保持一致的上下文。
cuDNN Descriptor 的 RAII 封装
当自定义 kernel 直接调用 cuDNN(例如自研卷积/RNN 路径)时,需要构造各类cudnn*Descriptor_t并管理其生命周期。PyTorch 在 aten/src/ATen/cudnn/Descriptors.h 提供了一系列 RAII 包装类,统一命名为at::native::Descriptor的派生/变体:
| 包装类 | 封装的 cuDNN 类型 | 用途 |
|---|---|---|
Descriptor(基类) | 通用 | 默认构造为nullptr,首次使用经mut_desc()初始化,只读访问走desc() |
TensorDescriptor | cudnnTensorDescriptor_t | 张量布局;支持把低维张量补维以满足 cuDNN 广播要求(见pad参数) |
FilterDescriptor | cudnnFilterDescriptor_t | 卷积滤波器权重 |
ConvolutionDescriptor | cudnnConvolutionDescriptor_t | 配置 padding、stride、dilation、groups 及 math type(TF32、tensor ops) |
RNNDataDescriptor | cudnnRNNDataDescriptor_t | 变长序列数据 |
DropoutDescriptor | cudnnDropoutDescriptor_t | 管理 cuDNN dropout 的 RNG 状态 |
ActivationDescriptor | cudnnActivationDescriptor_t | 激活函数描述 |
SpatialTransformerDescriptor | — | 空间变换描述 |
CTCLossDescriptor | — | CTC Loss 描述 |
TensorDescriptor 的最小用法:
#include <ATen/cudnn/Descriptors.h> at::Tensor input = torch::randn({32, 3, 224, 224}, torch::kCUDA); at::native::TensorDescriptor desc(input); cudnnTensorDescriptor_t raw = desc.desc();把at::Tensor直接传入 descriptor 构造器即可同步其 shape/stride/dtype 到 cuDNN 描述,后续自定义 cuDNN 调用只需desc.desc()取原生句柄传入。这类封装让“用完释放”交给析构,规避手动cudnnDestroyXxxDescriptor的内存泄漏风险。
工程实践小结
综合官方文档与源码,编写 C++ CUDA 算子/扩展时的推荐纪律可归纳为:
- 统一入口探测:任何 CUDA 代码先
torch::cuda::is_available()与c10::cuda::device_count()判环境; - 设备切换一律 RAII:优先
c10::cuda::CUDAGuard,避免手写cudaSetDevice/手动还原;通用型算子考虑OptionalCUDAGuard兼容 CPU/CUDA 双路径; - 流优先取自流池:
getStreamFromPool成本近零,短生命周期流是最佳实践;注意同池仅 32 条低优先级流、轮询复用,长活性能关键流不要互相抢占; - 区分三种 Guard 语义:需要只切设备用
CUDAGuard,同时切设备+流用CUDAStreamGuard,跨多卡一次性设置各卡流用CUDAMultiStreamGuard(且它不改 current device); - 底层库调用前对齐上下文:直呼 cuBLAS/cuSPARSE/cuSOLVER 前用
getCurrentCUDA*Handle取“当前设备×当前流”句柄;直呼 cuDNN 前通过 Descriptors.h 的 RAII 类构造描述; - 验证路径:ATen 层 CUDA 算子实现与
c10/cuda下的 Guard/Stream 实现(CUDAGuard.h、CUDAStream.h、CUDAFunctions.h)是上述 API 的事实来源;如需更细行为(如流池轮询、guard 是否允许搬移),直接阅读这些头文件中的注释与定义即可。
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考