自定义 C++ 和 CUDA 运算符#
创建日期:2024 年 6 月 18 日 | 最后更新:2025 年 1 月 28 日 | 最后验证:2024 年 11 月 5 日
作者: Richard Zou
如何将用 C++/CUDA 编写的自定义运算符集成到 PyTorch 中
如何使用
torch.library.opcheck测试自定义运算符
PyTorch 2.4 或更高版本
对 C++ 和 CUDA 编程的基本理解
注意
本教程在 AMD ROCm 上也能正常工作,无需额外修改。
PyTorch 提供了大量的运算符库,可以作用于 Tensor(例如 torch.add、torch.sum 等)。然而,您可能希望为 PyTorch 添加新的自定义运算符。本教程将演示编写自定义 C++/CUDA 运算符的推荐方法。
在本教程中,我们将演示如何编写一个组合了 PyTorch 子系统的融合乘加 C++ 和 CUDA 运算符。该操作的语义如下:
def mymuladd(a: Tensor, b: Tensor, c: float):
return a * b + c
您可以在 此处 找到本教程的端到端工作示例。
设置构建系统#
如果您正在开发自定义 C++/CUDA 代码,则必须对其进行编译。请注意,如果您要与已经绑定到预编译 C++/CUDA 代码的 Python 库进行交互,则可以考虑编写自定义 Python 运算符(请参阅 自定义 Python 运算符)。
使用 torch.utils.cpp_extension 来编译 C++/CUDA 自定义代码,以供 PyTorch 使用。C++ 扩展可以“提前”使用 setuptools 进行构建,也可以“即时”通过 load_inline 进行构建;我们将重点关注“提前”构建。
使用 cpp_extension 就像编写以下 setup.py 一样简单:
from setuptools import setup, Extension
from torch.utils import cpp_extension
setup(name="extension_cpp",
ext_modules=[
cpp_extension.CppExtension(
"extension_cpp",
["muladd.cpp"],
# define Py_LIMITED_API with min version 3.9 to expose only the stable
# limited API subset from Python.h
extra_compile_args={"cxx": ["-DPy_LIMITED_API=0x03090000"]},
py_limited_api=True)], # Build 1 wheel across multiple Python versions
cmdclass={'build_ext': cpp_extension.BuildExtension},
options={"bdist_wheel": {"py_limited_api": "cp39"}} # 3.9 is minimum supported Python version
)
如果您需要编译 CUDA 代码(例如 .cu 文件),请使用 torch.utils.cpp_extension.CUDAExtension。有关如何设置的示例,请参阅 extension-cpp。
上述示例代表我们所称的 CPython 无关的 wheel,这意味着我们构建了一个单一的 wheel,可以跨多个 CPython 版本运行(类似于纯 Python 包)。CPython 无关性对于最大限度地减少自定义库需要支持和发布的 wheel 数量是可取的。我们希望支持的最低版本是 3.9,因为它是当前支持的最早版本,因此我们在整个设置代码中使用相应的十六进制代码和规范符。我们建议在您希望支持的最低 CPython 版本相同的环境中构建扩展,以最大限度地减少未知行为,因此,在此处,我们在 CPython 3.9 环境中构建扩展。构建后,此单一 wheel 将可在任何 CPython 环境 3.9+ 中运行。为了实现这一点,有三个关键行需要注意。
第一个是在 extra_compile_args 中指定 Py_LIMITED_API,以支持您希望支持的最低 CPython 版本。
extra_compile_args={"cxx": ["-DPy_LIMITED_API=0x03090000"]},
定义 Py_LIMITED_API 标志有助于验证扩展实际上只使用了 CPython 稳定有限 API,这是构建 CPython 无关 wheel 的要求。如果未满足此要求,则可能构建一个看起来 CPython 无关但会在其他 CPython 环境中崩溃,甚至更糟的是,会产生静默错误效果的 wheel。请注意避免使用不稳定的 CPython API,例如来自 libtorch_python(特别是 pytorch/python 绑定)的 API,并且仅使用来自 libtorch(ATen 对象、运算符和调度器)的 API。我们强烈建议定义 Py_LIMITED_API 标志,以帮助确定扩展作为 CPython 无关 wheel 是合规且安全的。请注意,定义此标志并不能完全保证构建的 wheel 是 CPython 无关的,但它比“狂野西部”要好。Python 文档中有几个注意事项 (Python docs),您应该自行测试和验证 wheel 是否真正与相关 CPython 版本无关。
指定 py_limited_api 的第二行和第三行通知 setuptools,您打算构建一个 CPython 无关的 wheel,并将相应地影响 wheel 的命名。
setup(name="extension_cpp",
ext_modules=[
cpp_extension.CppExtension(
...,
py_limited_api=True)], # Build 1 wheel across multiple Python versions
...,
options={"bdist_wheel": {"py_limited_api": "cp39"}} # 3.9 is minimum supported Python version
)
有必要将 py_limited_api=True 指定为 CppExtension/CUDAExtension 的参数,并且也作为 "bdist_wheel" 命令(使用最低支持的 CPython 版本,在此例中为 3.9)的选项。因此,我们教程中的 setup 将构建一个命名正确的 wheel,该 wheel 可以安装在多个 CPython 版本 >=3.9 上。
如果您的扩展使用 CPython API(超出稳定有限集),那么您无法构建 CPython 无关的 wheel!您应该为每个 CPython 版本构建一个 wheel,如下所示:
from setuptools import setup, Extension
from torch.utils import cpp_extension
setup(name="extension_cpp",
ext_modules=[
cpp_extension.CppExtension(
"extension_cpp",
["muladd.cpp"])],
cmdclass={'build_ext': cpp_extension.BuildExtension},
)
定义自定义运算符并添加后端实现#
首先,让我们编写一个计算 mymuladd 的 C++ 函数:
at::Tensor mymuladd_cpu(at::Tensor a, const at::Tensor& b, double c) {
TORCH_CHECK(a.sizes() == b.sizes());
TORCH_CHECK(a.dtype() == at::kFloat);
TORCH_CHECK(b.dtype() == at::kFloat);
TORCH_INTERNAL_ASSERT(a.device().type() == at::DeviceType::CPU);
TORCH_INTERNAL_ASSERT(b.device().type() == at::DeviceType::CPU);
at::Tensor a_contig = a.contiguous();
at::Tensor b_contig = b.contiguous();
at::Tensor result = torch::empty(a_contig.sizes(), a_contig.options());
const float* a_ptr = a_contig.data_ptr<float>();
const float* b_ptr = b_contig.data_ptr<float>();
float* result_ptr = result.data_ptr<float>();
for (int64_t i = 0; i < result.numel(); i++) {
result_ptr[i] = a_ptr[i] * b_ptr[i] + c;
}
return result;
}
为了从 PyTorch 的 Python 前端使用它,我们需要使用 TORCH_LIBRARY API 将其注册为 PyTorch 运算符。这将自动将运算符绑定到 Python。
运算符注册是一个分步过程:
定义运算符 — 此步骤确保 PyTorch 已知晓新的运算符。
注册后端实现 — 在此步骤中,将 CPU 和 CUDA 等各种后端的实现与运算符相关联。
定义运算符#
要定义运算符,请按照以下步骤操作:
选择一个运算符的命名空间。我们建议命名空间为您的顶级项目名称;在本教程中,我们将使用“extension_cpp”。
提供一个模式字符串,该字符串指定运算符的输入/输出类型以及输入 Tensor 是否会被修改。除了 Tensor 和 float,我们还支持更多类型;有关更多详细信息,请参阅 自定义运算符手册。
如果您要编写可以修改其输入 Tensor 的运算符,请参阅此处(创建可修改运算符)了解如何指定。
TORCH_LIBRARY(extension_cpp, m) {
// Note that "float" in the schema corresponds to the C++ double type
// and the Python float type.
m.def("mymuladd(Tensor a, Tensor b, float c) -> Tensor");
}
这将使该运算符可以通过 Python 访问,路径为 torch.ops.extension_cpp.mymuladd。
为运算符注册后端实现#
使用 TORCH_LIBRARY_IMPL 为运算符注册后端实现。
TORCH_LIBRARY_IMPL(extension_cpp, CPU, m) {
m.impl("mymuladd", &mymuladd_cpu);
}
如果您也有 myaddmul 的 CUDA 实现,可以在单独的 TORCH_LIBRARY_IMPL 块中注册它:
__global__ void muladd_kernel(int numel, const float* a, const float* b, float c, float* result) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < numel) result[idx] = a[idx] * b[idx] + c;
}
at::Tensor mymuladd_cuda(const at::Tensor& a, const at::Tensor& b, double c) {
TORCH_CHECK(a.sizes() == b.sizes());
TORCH_CHECK(a.dtype() == at::kFloat);
TORCH_CHECK(b.dtype() == at::kFloat);
TORCH_INTERNAL_ASSERT(a.device().type() == at::DeviceType::CUDA);
TORCH_INTERNAL_ASSERT(b.device().type() == at::DeviceType::CUDA);
at::Tensor a_contig = a.contiguous();
at::Tensor b_contig = b.contiguous();
at::Tensor result = torch::empty(a_contig.sizes(), a_contig.options());
const float* a_ptr = a_contig.data_ptr<float>();
const float* b_ptr = b_contig.data_ptr<float>();
float* result_ptr = result.data_ptr<float>();
int numel = a_contig.numel();
muladd_kernel<<<(numel+255)/256, 256>>>(numel, a_ptr, b_ptr, c, result_ptr);
return result;
}
TORCH_LIBRARY_IMPL(extension_cpp, CUDA, m) {
m.impl("mymuladd", &mymuladd_cuda);
}
为运算符添加 torch.compile 支持#
要为运算符添加 torch.compile 支持,我们必须添加一个 FakeTensor 内核(也称为“元内核”或“抽象实现”)。FakeTensor 是具有元数据(如形状、dtype、设备)但没有数据的 Tensor:运算符的 FakeTensor 内核指定了给定输入 Tensor 元数据的输出 Tensor 的元数据如何计算。FakeTensor 内核应返回您选择的具有正确 Tensor 元数据(形状/步幅/dtype/设备)的虚拟 Tensor。
我们建议通过 Python 使用 torch.library.register_fake API 来完成此操作,但也可以从 C++ 完成(有关更多详细信息,请参阅 自定义运算符手册)。
# Important: the C++ custom operator definitions should be loaded first
# before calling ``torch.library`` APIs that add registrations for the
# C++ custom operator(s). The following import loads our
# C++ custom operator definitions.
# Note that if you are striving for Python agnosticism, you should use
# the ``load_library(...)`` API call instead. See the next section for
# more details.
from . import _C
@torch.library.register_fake("extension_cpp::mymuladd")
def _(a, b, c):
torch._check(a.shape == b.shape)
torch._check(a.dtype == torch.float)
torch._check(b.dtype == torch.float)
torch._check(a.device == b.device)
return torch.empty_like(a)
设置混合 Python/C++ 注册#
在本教程中,我们定义了 C++ 自定义运算符,在 C++ 中添加了 CPU/CUDA 实现,并在 Python 中添加了 FakeTensor 内核和后向公式。这些注册的加载(或导入)顺序很重要(以错误的顺序导入将导致错误)。
要使用混合 Python/C++ 注册的自定义运算符,我们必须先加载包含自定义运算符定义的 C++ 库,然后调用 torch.library 注册 API。这可以通过三种方式完成:
加载包含自定义运算符定义的 C++ 库的第一种方法是为 _C 定义一个虚拟 Python 模块。然后,在 Python 中,当您使用
import _C导入模块时,将加载对应于扩展的.so文件,并且TORCH_LIBRARY和TORCH_LIBRARY_IMPL静态初始化程序将运行。可以使用PYBIND11_MODULE创建一个虚拟 Python 模块,如下所示,但您会注意到它与Py_LIMITED_API不兼容,因为pybind11不承诺仅使用稳定的有限 CPython API!使用以下代码,您很遗憾无法为您的扩展构建 CPython 无关的 wheel!(剧透:我想知道第二种方法是什么 ;) )。
// in, say, not_agnostic/csrc/extension_BAD.cpp
#include <pybind11/pybind11.h>
PYBIND11_MODULE("_C", m) {}
# in, say, extension/__init__.py
from . import _C
在本教程中,由于我们重视能够跨多个 CPython 版本构建单个 wheel,我们将使用稳定的 API 调用替换不稳定的
PYBIND11调用。以下代码使用-DPy_LIMITED_API=0x03090000编译,并成功创建了我们的_C扩展的虚拟 Python 模块,以便可以从 Python 导入它。有关更多详细信息,请参阅 extension_cpp/__init__.py 和 extension_cpp/csrc/muladd.cpp。
#include <Python.h>
extern "C" {
/* Creates a dummy empty _C module that can be imported from Python.
The import from Python will load the .so consisting of this file
in this extension, so that the TORCH_LIBRARY static initializers
below are run. */
PyObject* PyInit__C(void)
{
static struct PyModuleDef module_def = {
PyModuleDef_HEAD_INIT,
"_C", /* name of module */
NULL, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
NULL, /* methods */
};
return PyModule_Create(&module_def);
}
}
# in, say, extension/__init__.py
from . import _C
如果您想完全避免在 C++ 自定义运算符中使用
Python.h,您可以使用torch.ops.load_library("/path/to/library.so")在 Python 中加载从扩展编译的.so文件。请注意,使用此方法,不会为扩展创建_CPython 模块,因此您无法从 Python 调用import _C。而不是依赖 import 语句来触发自定义运算符的注册,torch.ops.load_library("/path/to/library.so")将起到作用。那么挑战就转移到理解.so文件在哪里,以便您加载它们,这并不总是容易的。
import torch
from pathlib import Path
so_files = list(Path(__file__).parent.glob("_C*.so"))
assert (
len(so_files) == 1
), f"Expected one _C*.so file, found {len(so_files)}"
torch.ops.load_library(so_files[0])
from . import ops
为运算符添加训练(autograd)支持#
使用 torch.library.register_autograd 为运算符添加训练支持。优先使用它而不是直接使用 Python torch.autograd.Function 或 C++ torch::autograd::Function;您必须以非常特定的方式使用它们,以避免静默错误(有关更多详细信息,请参阅 自定义运算符手册)。
def _backward(ctx, grad):
a, b = ctx.saved_tensors
grad_a, grad_b = None, None
if ctx.needs_input_grad[0]:
grad_a = grad * b
if ctx.needs_input_grad[1]:
grad_b = grad * a
return grad_a, grad_b, None
def _setup_context(ctx, inputs, output):
a, b, c = inputs
saved_a, saved_b = None, None
if ctx.needs_input_grad[0]:
saved_b = b
if ctx.needs_input_grad[1]:
saved_a = a
ctx.save_for_backward(saved_a, saved_b)
# This code adds training support for the operator. You must provide us
# the backward formula for the operator and a `setup_context` function
# to save values to be used in the backward.
torch.library.register_autograd(
"extension_cpp::mymuladd", _backward, setup_context=_setup_context)
请注意,后向必须是 PyTorch 理解的运算符的组合。如果您希望在后向传递中使用另一个自定义 C++ 或 CUDA 内核,它必须被包装成一个自定义运算符。
如果我们有自己的自定义 mymul 内核,我们就需要将其包装成一个自定义运算符,然后从后向调用它:
// New! a mymul_cpu kernel
at::Tensor mymul_cpu(const at::Tensor& a, const at::Tensor& b) {
TORCH_CHECK(a.sizes() == b.sizes());
TORCH_CHECK(a.dtype() == at::kFloat);
TORCH_CHECK(b.dtype() == at::kFloat);
TORCH_CHECK(a.device().type() == at::DeviceType::CPU);
TORCH_CHECK(b.device().type() == at::DeviceType::CPU);
at::Tensor a_contig = a.contiguous();
at::Tensor b_contig = b.contiguous();
at::Tensor result = torch::empty(a_contig.sizes(), a_contig.options());
const float* a_ptr = a_contig.data_ptr<float>();
const float* b_ptr = b_contig.data_ptr<float>();
float* result_ptr = result.data_ptr<float>();
for (int64_t i = 0; i < result.numel(); i++) {
result_ptr[i] = a_ptr[i] * b_ptr[i];
}
return result;
}
TORCH_LIBRARY(extension_cpp, m) {
m.def("mymuladd(Tensor a, Tensor b, float c) -> Tensor");
// New! defining the mymul operator
m.def("mymul(Tensor a, Tensor b) -> Tensor");
}
TORCH_LIBRARY_IMPL(extension_cpp, CPU, m) {
m.impl("mymuladd", &mymuladd_cpu);
// New! registering the cpu kernel for the mymul operator
m.impl("mymul", &mymul_cpu);
}
def _backward(ctx, grad):
a, b = ctx.saved_tensors
grad_a, grad_b = None, None
if ctx.needs_input_grad[0]:
grad_a = torch.ops.extension_cpp.mymul.default(grad, b)
if ctx.needs_input_grad[1]:
grad_b = torch.ops.extension_cpp.mymul.default(grad, a)
return grad_a, grad_b, None
def _setup_context(ctx, inputs, output):
a, b, c = inputs
saved_a, saved_b = None, None
if ctx.needs_input_grad[0]:
saved_b = b
if ctx.needs_input_grad[1]:
saved_a = a
ctx.save_for_backward(saved_a, saved_b)
# This code adds training support for the operator. You must provide us
# the backward formula for the operator and a `setup_context` function
# to save values to be used in the backward.
torch.library.register_autograd(
"extension_cpp::mymuladd", _backward, setup_context=_setup_context)
测试运算符#
使用 torch.library.opcheck 来测试自定义运算符是否已正确注册。请注意,此函数不测试梯度是否在数学上正确——计划为此编写单独的测试,手动测试或使用 torch.autograd.gradcheck。
def sample_inputs(device, *, requires_grad=False):
def make_tensor(*size):
return torch.randn(size, device=device, requires_grad=requires_grad)
def make_nondiff_tensor(*size):
return torch.randn(size, device=device, requires_grad=False)
return [
[make_tensor(3), make_tensor(3), 1],
[make_tensor(20), make_tensor(20), 3.14],
[make_tensor(20), make_nondiff_tensor(20), -123],
[make_nondiff_tensor(2, 3), make_tensor(2, 3), -0.3],
]
def reference_muladd(a, b, c):
return a * b + c
samples = sample_inputs(device, requires_grad=True)
samples.extend(sample_inputs(device, requires_grad=False))
for args in samples:
# Correctness test
result = torch.ops.extension_cpp.mymuladd(*args)
expected = reference_muladd(*args)
torch.testing.assert_close(result, expected)
# Use opcheck to check for incorrect usage of operator registration APIs
torch.library.opcheck(torch.ops.extension_cpp.mymuladd.default, args)
创建可修改运算符#
您可能希望编写一个修改其输入的自定义运算符。使用 Tensor(a!) 为模式中的每个可修改 Tensor 指定;否则,将导致未定义的行为。如果存在多个修改的 Tensor,请为每个可修改 Tensor 使用不同的名称(例如,Tensor(a!)、Tensor(b!)、Tensor(c!))。
让我们编写一个 myadd_out(a, b, out) 运算符,它将 a+b 的内容写入 out。
// An example of an operator that mutates one of its inputs.
void myadd_out_cpu(const at::Tensor& a, const at::Tensor& b, at::Tensor& out) {
TORCH_CHECK(a.sizes() == b.sizes());
TORCH_CHECK(b.sizes() == out.sizes());
TORCH_CHECK(a.dtype() == at::kFloat);
TORCH_CHECK(b.dtype() == at::kFloat);
TORCH_CHECK(out.dtype() == at::kFloat);
TORCH_CHECK(out.is_contiguous());
TORCH_INTERNAL_ASSERT(a.device().type() == at::DeviceType::CPU);
TORCH_INTERNAL_ASSERT(b.device().type() == at::DeviceType::CPU);
TORCH_INTERNAL_ASSERT(out.device().type() == at::DeviceType::CPU);
at::Tensor a_contig = a.contiguous();
at::Tensor b_contig = b.contiguous();
const float* a_ptr = a_contig.data_ptr<float>();
const float* b_ptr = b_contig.data_ptr<float>();
float* result_ptr = out.data_ptr<float>();
for (int64_t i = 0; i < out.numel(); i++) {
result_ptr[i] = a_ptr[i] + b_ptr[i];
}
}
在定义运算符时,我们必须在模式中指定它修改了 out Tensor:
TORCH_LIBRARY(extension_cpp, m) {
m.def("mymuladd(Tensor a, Tensor b, float c) -> Tensor");
m.def("mymul(Tensor a, Tensor b) -> Tensor");
// New!
m.def("myadd_out(Tensor a, Tensor b, Tensor(a!) out) -> ()");
}
TORCH_LIBRARY_IMPL(extension_cpp, CPU, m) {
m.impl("mymuladd", &mymuladd_cpu);
m.impl("mymul", &mymul_cpu);
// New!
m.impl("myadd_out", &myadd_out_cpu);
}
注意
不要将任何修改的 Tensor 作为运算符的输出返回,因为这会导致与 torch.compile 等 PyTorch 子系统不兼容。
结论#
在本教程中,我们回顾了将自定义 C++ 和 CUDA 运算符集成到 PyTorch 中的推荐方法。 TORCH_LIBRARY/torch.library API 相当底层。有关如何使用该 API 的更多信息,请参阅 自定义运算符手册。