自定義 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 的更多資訊,請參閱 自定義運算子手冊。