DCU 自定义算子工程化:从 HIP 源码、PyTorch Binding 到在线加载
本文只介绍通用算子开发流程,示例为普通逐元素运算,不包含任何未结束项目(博主的先导杯还没结束)的真实算子、shape、调度参数或性能结果。
摘要
写出一个能运行的 HIP kernel 并不难,难的是把它变成可维护的 PyTorch 扩展:输入检查要完整、目标架构要明确、编译缓存要可控、错误要能定位,还要有正确性和端到端性能门禁。
本文以一个通用 y = x × scale + bias 逐元素算子为例,说明 DCU 自定义算子的工程结构、C++ binding、HIP kernel、在线编译、缓存管理和测试方法。示例只演示基础流程,不对应任何实际项目的优化实现。
关键词:DCU、HIP、PyTorch Extension、C++ Binding、在线编译、算子开发
1. 什么时候值得写自定义算子
自定义kernel有长期成本:需要维护dtype、shape、设备、连续性、编译器版本和数值行为。满足以下条件时才值得考虑:
- Profile确认它在端到端关键路径;
- shape或dtype稳定,专用实现能利用先验;
- 多个相邻算子反复读写同一个大tensor;
- 现有后端没有自动融合;
- 替换后的正确性边界可清楚定义;
- 微基准收益足以覆盖launch、转换和编译成本。
如果只是单个通用 GEMM 或卷积,优先使用框架和数学库。自定义算子更适合稳定的逐元素融合、布局转换和小型专用reduction。
2. 推荐的目录结构
一个最小扩展可以组织为:
my_dcu_op/
├── __init__.py
├── loader.py
├── binding.cpp
├── op.h
└── op_kernel.hip
职责分离:
| 文件 | 职责 |
|---|---|
loader.py |
架构检查、编译参数、缓存目录、Python接口 |
binding.cpp |
Tensor契约检查、pybind导出、调用HIP入口 |
op.h |
C++声明 |
op_kernel.hip |
kernel和launch配置 |
不要把所有逻辑写在一个 .hip 文件中。输入契约和设备实现分开后,错误更容易定位。
3. 先定义Tensor契约
以逐元素 affine 为例:
输入x:DCU tensor、FP16、contiguous
scale:标量float
bias:标量float
输出:与x同shape、同dtype、同device
C++ binding 应在 launch 前检查:
TORCH_CHECK(x.is_cuda(), "x must be on accelerator");
TORCH_CHECK(x.scalar_type() == at::kHalf, "x must be FP16");
TORCH_CHECK(x.is_contiguous(), "x must be contiguous");
如果支持多输入,还要检查device一致、元素数匹配和禁止非法alias。把错误挡在C++入口,比让kernel非法访存安全得多。
4. 一个通用HIP kernel骨架
以下代码只演示工程形式:
#include <hip/hip_runtime.h>
#include <hip/hip_fp16.h>
__global__ void affine_half_kernel(
const __half* input,
__half* output,
float scale,
float bias,
long numel) {
long index = static_cast<long>(blockIdx.x) * blockDim.x + threadIdx.x;
if (index >= numel) return;
float value = __half2float(input[index]);
output[index] = __float2half(value * scale + bias);
}
launch函数:
void launch_affine_half(
const at::Tensor& input,
at::Tensor& output,
float scale,
float bias) {
constexpr int threads = 256;
const auto numel = input.numel();
const int blocks = static_cast<int>((numel + threads - 1) / threads);
hipLaunchKernelGGL(
affine_half_kernel,
dim3(blocks), dim3(threads), 0, 0,
reinterpret_cast<const __half*>(input.data_ptr<at::Half>()),
reinterpret_cast<__half*>(output.data_ptr<at::Half>()),
scale, bias, numel);
}
生产实现还应使用当前PyTorch stream,并在需要时检查 launch error。上例省略了版本相关接口,避免把某一环境的写法误当作通用标准。
5. C++ Binding只做契约和调度
#include <torch/extension.h>
#include "op.h"
torch::Tensor affine_half(
const torch::Tensor& input,
double scale,
double bias) {
TORCH_CHECK(input.is_cuda(), "input must be on accelerator");
TORCH_CHECK(input.scalar_type() == at::kHalf, "input must be FP16");
TORCH_CHECK(input.is_contiguous(), "input must be contiguous");
auto output = torch::empty_like(input);
launch_affine_half(input, output,
static_cast<float>(scale),
static_cast<float>(bias));
return output;
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, module) {
module.def("affine_half", &affine_half, "FP16 affine example");
}
binding层不应偷偷执行dtype转换或 .contiguous()。隐式转换会产生额外tensor,使微基准看起来正常、端到端却变慢。调用方需要转换时,应在Python层显式完成并纳入测量。
6. Python在线编译与加载
from pathlib import Path
import torch
from torch.utils.cpp_extension import load
def load_extension():
if torch.version.hip is None:
raise RuntimeError("This extension requires a HIP PyTorch build")
root = Path(__file__).resolve().parent
return load(
name="my_dcu_op_ext",
sources=[
str(root / "binding.cpp"),
str(root / "op_kernel.hip"),
],
extra_cflags=["-O3", "-std=c++17"],
extra_cuda_cflags=[
"-O3",
"--offload-arch=gfx936",
],
verbose=True,
)
虽然接口参数名是 extra_cuda_cflags,HIP版PyTorch通常会将其传递给HIP编译链。是否支持、具体参数如何解释,应以当前PyTorch/DTK版本为准。
在线编译适用于不能预装二进制扩展、但允许在目标机编译源码的环境。它能确保目标架构一致,代价是首次启动时间较长。
7. 编译缓存如何管理
PyTorch extension会缓存编译产物。缓存键至少受以下因素影响:
- 扩展名称;
- 源码内容;
- 编译参数;
- Python与PyTorch版本;
- 编译器和运行时;
- 目标架构。
开发时如果修改源码却仍加载旧 .so,可以使用新的扩展名或干净缓存目录验证。不要在生产程序里每次启动都删除缓存,这会强制重复编译。
提交源码时通常不应携带:
.torch_extensions/
*.so
*.o
__pycache__/
*.pyc
预编译二进制还可能与评测机的PyTorch ABI和运行时不匹配。
8. 三层验证缺一不可
8.1 正确性
reference = input * scale + bias
actual = extension.affine_half(input, scale, bias)
torch.testing.assert_close(
actual,
reference,
rtol=1e-3,
atol=1e-3,
)
测试应覆盖:
- 空tensor和小tensor;
- 元素数不是block整数倍;
- 多维连续tensor;
- 极值、零和负数;
- 错误dtype、CPU输入、非连续输入;
- 多次调用和不同stream。
8.2 微基准
def bench(fn, iterations=100):
for _ in range(10):
fn()
torch.cuda.synchronize()
start = time.perf_counter()
for _ in range(iterations):
fn()
torch.cuda.synchronize()
return (time.perf_counter() - start) / iterations
reference和candidate必须使用相同输入、同步方式和重复次数。
8.3 端到端
即使kernel微基准更快,也要放回完整应用检查:
- 是否增加输入转换;
- 是否破坏原有融合;
- 是否增加常驻显存;
- 是否引入同步;
- 是否在真实shape上被调用;
- 业务指标是否保持。
9. 数值语义比公式等价更严格
设备优化中常见的误区是“公式相同,所以结果相同”。实际还要考虑:
- FP16与FP32中间值;
- reduction顺序;
- FMA与分步乘加;
- 原地写入时机;
- 广播和舍入点;
- NaN、Inf和有符号零。
如果原路径在两个操作之间写回FP16,融合kernel在FP32中连续计算就可能改变结果。是否允许这种差异,应由业务正确性门禁决定,而不是凭肉眼判断公式。
10. 常见编译和运行错误
10.1 找不到头文件
确认当前Python使用的PyTorch与编译环境一致,并检查扩展编译日志中的include目录。
10.2 架构不匹配
确认 rocminfo 显示的目标架构与 --offload-arch 一致。不要把其他机器生成的code object直接复制过来。
10.3 undefined symbol
通常是ABI或动态库版本不一致。使用 ldd 检查扩展实际链接到的库,并确认没有混用多个DTK环境。
10.4 非法访存
优先检查:边界条件、元素数类型、tensor连续性、dtype、device、stream和输出buffer大小。先用极小shape复现,再扩大输入。
11. 工程化发布清单
- loader明确检查HIP环境和设备架构;
- binding检查device、dtype、shape和contiguous;
- kernel覆盖非整除边界;
- 使用当前框架stream;
- 有reference正确性测试;
- 有错误输入测试;
- 有微基准和端到端验证;
- 编译发生在正式计时边界之外;
- 提交只包含源码,不包含缓存和二进制;
- README写清环境、编译链和运行方法;
- 不在公开文章中披露未结束项目的真实kernel和调参数据。
参考与依赖说明
本文示例使用 PyTorch C++ Extension、HIP Runtime 和通用 C++17 接口,代码仅用于展示工程流程。实际项目应以对应版本的PyTorch、DTK和设备官方文档为准。文章不包含任何项目专有算子、模型、权重、shape或性能数据。
更多推荐




所有评论(0)