Intro 如何将写好的kernel优雅地嵌入Python流程中。 Pytorch提供了 torch.utils.cpp_extension 工具箱,让我们能够以极小代价将C++/CUDA代码编译为Python模块。这里会梳理三种主流方式,并解释为什么要自定义算子,如何避免环境坑以及如何让自定义kernel无缝支持 autograd
核心概念:
概念
作用
典型位置
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 即时编译
什么时候用:在notebook中快速验证小kernel或者写一次性实验脚本 C++编译需要ninja,需要提前安装1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import torchfrom torch.utils.cpp_extension import load_inlinecpp_src = """ torch::Tensor add_cuda(torch::Tensor a, torch::Tensor b); """ 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; } """ 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) print (torch.allclose(c, a + b))
优点 : 无需进行任何文件操作,代码与结果同屏,调试只管
缺点 :每次运行都要重新编译且错误定位较难
方式二:load与预编译扩展 load 是 load_inline的文件版,适合脚本中编译已有的 .cu/.cpp 文件
1 2 3 4 5 6 7 8 9 10 11 12 from torch.utils.cpp_extension import loadvector_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_extension 下生成预编译产物。第二次运行将跳过编译直接加载
常见的坑:
CUDA版本不匹配,load 会检测nvcc版本,若与torch.version.cuda 不一致则报错。
重复编译耗时:代码量较大时建议使用setup.py预编译
load 和 load_inline参数解释 这两者都是PyTorch提供的JIT编译工具,核心参数基本一致 - verbose(bool): 控制是否打印详细的编译日志(False 为静默编译,True输出完整的编译指令) - name(str): 生成对应的python模块名,也是编译中间文件的目录名 - sources:需要指定编译的源文件 - functions(仅load_inline): 指定需要暴露给Python的C++函数名列表。绑定的Python 方法名与C++函数名相同 - extra_cuda_cflags/ extra_cxxflags: 向nvcc或者C++传递额外的编译选项
1 2 extra_cuda_cflags = ["-O3" , "-arch=sm_80" ] extra_cxxflags = ["-O3" ]
常用选项:-O3 (最高优化), -g (调试符号), -arch=sm_xx(指定GPU计算能力)
方式三:setup.py 与 CUDAExtension 这是正式项目的首选,只需要写好setup.py,运行pip install . 即可将苦熬站永久安装到当前Python环境
1. 项目结构 1 2 3 4 custom_ops/ ├── setup.py ├── cuda_add/ │ └── vector_add.cu
2. 算子编写注意事项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <torch/extension.h> #include <cuda_runtime.h> __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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from setuptools import setupfrom torch.utils.cpp_extension import CUDAExtension, BuildExtensionsetup( 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" ], )
安装与使用
1 2 3 4 5 6 import torchimport cuda_adda = torch.randn(10_000_000 , device='cuda' ) b = torch.randn(10_000_000 , device='cuda' ) c = cuda_add.add(a, b) c_no_grad = cuda_add.add_forward(a, b)
为什么推荐预编译?
一次编译,随处import:省去JIT等待时间
依赖管理清晰:可指定pytorch版本,打包上传PyPI
错误提示友好:编译失败会生成完整日志
自动微分 案例:RGB转灰度与三维归一化 1. 算子定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <torch/extension.h> #include <cuda_runtime.h> 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) { 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 ); 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)" ); }
Python 测试代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import torchimport cv2import numpy as npimport custom_image_ops img = cv2.imread("input.jpg" ) img_tensor = torch.from_numpy(img).cuda() gray = 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 ())