CUDA学习之路[6]:PyTorch CUDA 扩展完全指南
2058 字
10 分钟
CUDA学习之路[6]:PyTorch CUDA 扩展完全指南
更新中…
前提摘要
引言
在上一篇 CUDA 学习之路中,我们学会了第一个Kernel算子的写法。 但很多同学会问:“我写的 kernel 如何优雅地嵌入到 Python 流程中?”
PyTorch 提供了 torch.utils.cpp_extension 工具箱,让我们能以极小的代价将 C++/CUDA 代码编译为 Python 模块。本文将梳理三种主流方式,并深入解释为什么需要自定义算子、如何避免环境坑,以及如何让自定义 kernel 无缝支持autograd。
本篇学习目标
- 掌握 PyTorch 调用 CUDA 的三种方式及其适用场景
- 理解
BuildExtension、CUDAExtension、load的内部机制 - 学会为自定义算子编写
torch.autograd.Function反向传播 - 能够将一个独立 CUDA kernel 转化为可直接
pip install的 Python 包
核心概念速查
在深入代码之前,先梳理几个易混淆的关键对象:
| 概念 | 作用 | 典型位置 |
|---|---|---|
torch.utils.cpp_extension.load_inline | 直接编译字符串中的 C++/CUDA 代码 | Jupyter / 快速原型 |
torch.utils.cpp_extension.load | 编译指定 .cpp/.cu 文件 | 脚本中临时编译 |
CUDAExtension / CppExtension | 在 setup.py 中声明扩展模块 | 正式打包分发 |
BuildExtension | 替换 setuptools 默认构建命令,注入 PyTorch 编译参数 | setup.py 的 cmdclass |
PYBIND11_MODULE | 将 C++ 函数暴露给 Python 的宏 | .cpp 文件末尾 |
torch.autograd.Function | 自定义算子前向/反向传播的包装类 | 让算子支持自动微分 |
本质关系
load 和 load_inline 是 JIT 便捷工具,背后调用相同的编译器逻辑;setup.py + CUDAExtension 则是预编译方案,更适合生产环境。
方式一:load_inline 即时编译
适用场景:在 Jupyter Notebook 中快速验证一个小 kernel,或写一次性实验脚本。
C++的编译都需要ninja,使用uv pip install ninja提前进行安装。
import torchfrom torch.utils.cpp_extension import load_inline
# C++ 头文件声明cpp_src = """torch::Tensor add_cuda(torch::Tensor a, torch::Tensor b);"""
# CUDA 实现(注意:不包含 PYBIND11_MODULE 宏)cuda_src = """#include <torch/extension.h>#include <cuda_runtime.h>
__global__ void add_kernel(const float* a, const float* b, float* c, int N) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < N) c[i] = a[i] + b[i];}
torch::Tensor add_cuda(torch::Tensor a, torch::Tensor b) { auto c = torch::empty_like(a); int N = a.numel(); const int threads = 256; const int blocks = (N + threads - 1) / threads; add_kernel<<<blocks, threads>>>(a.data_ptr<float>(), b.data_ptr<float>(), c.data_ptr<float>(), N); return c;}"""
# 自动编译并加载,add_cuda 将绑定为模块的 add 方法module = load_inline( name="inline_cuda_add", cpp_sources=cpp_src, cuda_sources=cuda_src, functions=["add_cuda"], # 指定要暴露的函数名 verbose=True)
# 测试a = torch.randn(1000, device='cuda')b = torch.randn(1000, device='cuda')c = module.add_cuda(a, b) # 调用暴露的 C++ 函数print(torch.allclose(c, a + b)) # True优缺点
- 优点:无需任何文件操作,代码与结果同屏,调试直观。
- 缺点:每次运行都重新编译且错误定位较难。
方式二:load 与预编译扩展
load 是 load_inline 的文件版,适合脚本中编译已有的 .cu / .cpp 文件。
from torch.utils.cpp_extension import load
vector_add = load( name="vector_add_ext", sources=["add_kernel.cu"], verbose=True, extra_cuda_cflags=["-O3"])
x = torch.ones(10, device='cuda')y = torch.ones(10, device='cuda')print(vector_add.add(x, y))对应的 add_kernel.cu 内容与后文 vector_add.cu 类似。load 会在 ~/.cache/torch_extensions/ 下生成编译产物,存储空间不足时可以直接删除对应空间中的内容,第二次运行将跳过编译直接加载。
常见坑
- CUDA 版本不匹配:
load会检测系统nvcc版本,若与torch.version.cuda不一致则报错。解决:安装与 PyTorch CUDA 版本一致的工具包。 - 重复编译耗时:代码量较大时建议改用
setup.py预编译。
1. load 和 load_inline 参数解释
load 和 load_inline 都是 PyTorch 提供的 JIT 编译工具,它们的核心参数基本一致。
verbose(bool)控制是否打印详细的编译日志(False为静默编译,True输出完整的编译指令)name(str)用于生成对应的Python模块名,也是编译中间文件的目录名,例如name="ops",最终可通过import ops调用。sources需要指定编译的源文件functions(仅load_inline)指定需要暴露给Python的C++函数名列表。 绑定的Python方法名与C++函数名相同。extra_cuda_cflags/extra_cxxflags向nvcc或者C++传递额外的编译选项。
| 参数 | 适用函数 | 说明 |
|---|---|---|
sources | load | 传入 .cpp 或 .cu 文件列表,自动识别类型 |
cpp_sources | load_inline | C++ 代码字符串(通常只放函数声明) |
cuda_sources | load_inline | CUDA 代码字符串(包含 kernel 实现) |
extra_cuda_cflags=["-O3", "-arch=sm_80"] # 针对 .cu 文件extra_cxxflags=["-O3"] # 针对 .cpp 文件- 常用选项:
-O3(最高优化)、-g(调试符号)、-arch=sm_xx(指定 GPU 计算能力)。
方式三:setup.py 与 CUDAExtension
这是正式项目的首选。只需写好 setup.py,运行 pip install . 即可将扩展永久安装到当前 Python 环境。
1. 项目结构
custom_ops/├── setup.py├── cuda_add/│ └── vector_add.cu2. 算子编写注意事项
#include <torch/extension.h>#include <cuda_runtime.h>
// 高性能向量加法 kernel (float4 向量化)__global__ void add_kernel_vec4(const float* __restrict__ a, const float* __restrict__ b, float* __restrict__ c, int N) { const float4* a4 = reinterpret_cast<const float4*>(a); const float4* b4 = reinterpret_cast<const float4*>(b); float4* c4 = reinterpret_cast<float4*>(c);
int idx = blockIdx.x * blockDim.x + threadIdx.x; int stride = blockDim.x * gridDim.x; int N4 = N / 4;
for (int i = idx; i < N4; i += stride) { float4 av = a4[i]; float4 bv = b4[i]; float4 cv; cv.x = av.x + bv.x; cv.y = av.y + bv.y; cv.z = av.z + bv.z; cv.w = av.w + bv.w; c4[i] = cv; }
int remainder_start = N4 * 4; for (int i = remainder_start + idx; i < N; i += stride) { c[i] = a[i] + b[i]; }}
torch::Tensor add_forward(torch::Tensor a, torch::Tensor b) { TORCH_CHECK(a.device().is_cuda(), "a must be CUDA tensor"); TORCH_CHECK(b.device().is_cuda(), "b must be CUDA tensor"); auto c = torch::empty_like(a); int N = a.numel(); const int threads = 256; int blocks = ((N / 4) + threads - 1) / threads; if (blocks == 0) blocks = 1; add_kernel_vec4<<<blocks, threads>>>( a.data_ptr<float>(), b.data_ptr<float>(), c.data_ptr<float>(), N ); return c;}
// 自动微分包装class AddFunction : public torch::autograd::Function<AddFunction> {public: static torch::Tensor forward( torch::autograd::AutogradContext* ctx, torch::Tensor a, torch::Tensor b) { ctx->save_for_backward({a, b}); return add_forward(a, b); }
static torch::autograd::variable_list backward( torch::autograd::AutogradContext* ctx, torch::autograd::variable_list grad_output) { auto grad = grad_output[0]; return {grad, grad}; }};
torch::Tensor add_autograd(torch::Tensor a, torch::Tensor b) { return AddFunction::apply(a, b);}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("add", &add_autograd, "Vector addition with autograd"); m.def("add_forward", &add_forward, "Raw forward (no autograd)");}3. 编写setup.py
from setuptools import setupfrom torch.utils.cpp_extension import CUDAExtension, BuildExtension
setup( name="cuda_add", version="0.1.0", ext_modules=[ CUDAExtension( name="cuda_add", sources=["vector_add.cu"], extra_compile_args={"cxx": ["-O3"], "nvcc": ["-O3"]} ) ], cmdclass={"build_ext": BuildExtension}, install_requires=["torch"],)4. 安装与使用
python setup.py install # 或 pip install .import torchimport cuda_add
a = torch.randn(10_000_000, device='cuda')b = torch.randn(10_000_000, device='cuda')c = cuda_add.add(a, b) # 带 autogradc_no_grad = cuda_add.add_forward(a, b) # 纯前向,稍快为什么推荐预编译?
- 一次编译,随处 import:省去 JIT 等待时间。
- 依赖管理清晰:可指定 PyTorch 版本、打包上传 PyPI。
- 错误提示友好:编译失败会生成完整日志。
自动微分
且看mycuda的系列教程。
完整案例:RGB 转灰度与三维归一化
下面展示如何将两个经典 CUDA 任务封装成 PyTorch 扩展。
1. 算子定义
#include <torch/extension.h>#include <cuda_runtime.h>
// 定义 uchar3 结构体(与 OpenCV 对应)struct uchar3 { unsigned char x, y, z;};
__global__ void rgbToGrayKernel(const uchar3* img, unsigned char* gray, int width, int height) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; if (x < width && y < height) { int idx = y * width + x; uchar3 pixel = img[idx]; gray[idx] = (unsigned char)(0.299f * pixel.x + 0.587f * pixel.y + 0.114f * pixel.z); }}
torch::Tensor rgb_to_gray_cuda(torch::Tensor img) { // 假设输入是 uint8 [H, W, 3] 且已在 GPU 上 TORCH_CHECK(img.dim() == 3 && img.size(2) == 3, "Input must be HxWx3"); TORCH_CHECK(img.dtype() == torch::kUInt8, "Input must be uint8"); int height = img.size(0); int width = img.size(1); auto gray = torch::empty({height, width}, img.options().dtype(torch::kUInt8));
dim3 block(16, 16); dim3 grid((width + 15) / 16, (height + 15) / 16); rgbToGrayKernel<<<grid, block>>>( reinterpret_cast<const uchar3*>(img.data_ptr<unsigned char>()), gray.data_ptr<unsigned char>(), width, height ); return gray;}
__global__ void normalizeVolumeKernel(const unsigned short* in, float* out, int dimX, int dimY, int dimZ, float maxVal) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; int z = blockIdx.z * blockDim.z + threadIdx.z; if (x < dimX && y < dimY && z < dimZ) { int idx = z * dimY * dimX + y * dimX + x; out[idx] = (float)in[idx] / maxVal; }}
torch::Tensor normalize_volume_cuda(torch::Tensor volume, float maxVal) { TORCH_CHECK(volume.dim() == 3, "Volume must be 3D"); TORCH_CHECK(volume.dtype() == torch::kUInt16, "Input must be uint16"); int dimX = volume.size(2); // 注意 PyTorch 维度顺序 DxHxW,这里假设 Z,Y,X int dimY = volume.size(1); int dimZ = volume.size(0); auto out = torch::empty_like(volume, volume.options().dtype(torch::kFloat32));
dim3 block(8, 8, 4); dim3 grid((dimX + 7) / 8, (dimY + 7) / 8, (dimZ + 3) / 4); normalizeVolumeKernel<<<grid, block>>>( volume.data_ptr<unsigned short>(), out.data_ptr<float>(), dimX, dimY, dimZ, maxVal ); return out;}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("rgb_to_gray", &rgb_to_gray_cuda, "RGB to grayscale (CUDA)"); m.def("normalize_volume", &normalize_volume_cuda, "3D volume normalization (CUDA)");}2. Python 测试代码
import torchimport cv2import numpy as npimport custom_image_ops # 编译后的模块名
# RGB 转灰度测试img = cv2.imread("input.jpg")img_tensor = torch.from_numpy(img).cuda() # HxWx3gray = custom_image_ops.rgb_to_gray(img_tensor)cv2.imwrite("gray.png", gray.cpu().numpy())
# 三维归一化测试vol = torch.randint(0, 4096, (128, 256, 256), dtype=torch.uint16, device='cuda')norm = custom_image_ops.normalize_volume(vol, 4095.0)print(norm.min(), norm.max()) # 0.0 ~ 1.0支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
CUDA学习之路[6]:PyTorch CUDA 扩展完全指南
https://dlog.com.cn/posts/cuda06/cuda_with_pytorch/ 相关文章 智能推荐
1
CUDA学习之路[0]——祛魅
CUDA学习之路 你在什么时候才会用到CUDA呢?
2
CUDA学习之路[4]——CUDA全局坐标计算
CUDA学习之路 从一维到三维,彻底理清CUDA线程索引的映射逻辑。
3
CUDA学习之路[5]——逐元素操作算子
CUDA学习之路 最基础的一系列算子:element-wise。
4
CUDA学习之路[1]——速通环境配置
CUDA学习之路 如果说深度学习是星辰大海,那环境配置就是暗礁浅滩。我不允许你永远在入门!
5
CUDA学习之路[2]——你需要哪些C/C++的知识呢?
CUDA学习之路 你学Java/Python忽视的知识点,反而是在CUDA编程中最需要的。
随机文章 随机推荐