|
| 1 | +# 原理和迁移方式 |
| 2 | + |
| 3 | +## 实现原理 |
| 4 | + |
| 5 | +为了方便 PyTorch 自定义算子快速接入 PaddlePaddle 框架,我们提供了如下图所示的兼容机制: |
| 6 | + |
| 7 | +<figure align="center"> |
| 8 | + <img src="https://github.com/PaddlePaddle/docs/blob/develop/docs/guides/custom_op/cross_ecosystem_custom_op/images/cross-ecosystem-custom-op-compatible.drawio.png?raw=true" width="700" alt='missing' align="center"/> |
| 9 | + <figcaption><center>跨生态自定义算子兼容机制示意图</center></figcaption> |
| 10 | +</figure> |
| 11 | + |
| 12 | +正如图上所示,我们自底向上提供了如下几层支持: |
| 13 | + |
| 14 | +- **C++ API 兼容层**:该层实现了常用 PyTorch C++ API 的兼容接口,用户仍然可以通过调用 PyTorch 风格的 `at::*`、`torch::*`、`c10::*` 等命名空间下的函数和类来实现自定义算子逻辑,从而最大限度地复用现有代码,使迁移工作量降至最低。 |
| 15 | +- **算子注册兼容层**:对于使用 pybind11 进行算子注册的 PyTorch 自定义算子,PaddlePaddle 无需额外修改注册代码;而对于使用 `TORCH_LIBRARY` 宏进行注册并通过 `torch.ops` 调用的算子,我们提供了同名的注册接口,用户无需修改注册代码即可完成迁移。 |
| 16 | +- **Python 接口兼容层**:对于 Python 端自定义算子封装部分,会不可避免地调用一些 PyTorch 内的 Python 组网 API。为此,我们正在致力于提升 Python 端 API 与 PyTorch 的兼容性,力求让用户在迁移过程中无需修改 Python 端代码。 |
| 17 | +- **Python API 代理层**:在 Python 端,即便 API 能够完全兼容,用户仍然需要将 `import torch` 替换为 `import paddle`。为此,我们提供了一个轻量级的代理层,用户只需在迁移后的代码开头添加一行 `import paddle.compat.enable_torch_proxy`,后续的 `torch` 下的模块将被重定向至 `paddle` 下的模块,从而实现无缝迁移。 |
| 18 | + |
| 19 | +通过以上几层兼容机制,用户可以在最大程度上复用现有的 PyTorch 自定义算子代码,从而大幅降低迁移成本。 |
| 20 | + |
| 21 | +此外,对于 TVM FFI 生态的自定义算子,由于我们已经对 TVM FFI 中所需的 DLPack 协议提供了最佳支持,因此用户可以直接将 TVM FFI 生态的自定义算子迁移至 PaddlePaddle 框架,无需额外修改。当然,如果相关算子库在 Python 端调用了 PyTorch 的组网 API,则仍然需要借助上述的 Python API 代理层来完成迁移。 |
| 22 | + |
| 23 | +## 迁移步骤 |
| 24 | + |
| 25 | +下面我们以一个简单的 PyTorch 自定义算子为例,介绍如何将其迁移至 PaddlePaddle 框架。相关代码见 [PFCCLab/cross-ecosystem-custom-op-example](https://github.com/PFCCLab/cross-ecosystem-custom-op-example)。 |
| 26 | + |
| 27 | +### 搭建 PyTorch 运行环境 |
| 28 | + |
| 29 | +在迁移之前,需要先确保 PyTorch 算子能够在本地正确编译和运行。这可以参考具体的仓库说明文档来完成。 |
| 30 | + |
| 31 | +本示例展示的自定义算子可以通过如下方式来成功编译和运行: |
| 32 | + |
| 33 | +```bash |
| 34 | +# 首先安装 PyTorch,具体命令可以参考 https://pytorch.org/get-started/locally/ |
| 35 | +pip install torch |
| 36 | +# 克隆示例代码仓库 |
| 37 | +git clone https://github.com/PFCCLab/cross-ecosystem-custom-op-example.git |
| 38 | +cd cross-ecosystem-custom-op-example |
| 39 | +# 编译自定义算子 |
| 40 | +pip install . --no-build-isolation |
| 41 | +# 运行测试脚本 |
| 42 | +python test.py |
| 43 | +``` |
| 44 | + |
| 45 | +很好,我们已经确保该算子能够在 PyTorch 框架下正确运行,接下来将会介绍如何将其迁移至 PaddlePaddle 框架。 |
| 46 | + |
| 47 | +### 清理 PyTorch 环境 |
| 48 | + |
| 49 | +在迁移之前,建议先卸载 PyTorch 相关的包,或者新建一个干净的虚拟环境来进行迁移工作,以避免潜在的包冲突问题,并安装 PaddlePaddle 框架,具体命令可参考 [PaddlePaddle 安装指南](https://www.paddlepaddle.org.cn/install/quick)。 |
| 50 | + |
| 51 | +### 理解源码结构 |
| 52 | + |
| 53 | +当前示例代码的目录结构很简单,如下所示: |
| 54 | + |
| 55 | +```text |
| 56 | +. |
| 57 | +├── csrc |
| 58 | +│ └── muladd.cc # 自定义算子实现 |
| 59 | +├── extension # 自定义算子 Python package |
| 60 | +│ └── __init__.py # 自定义算子 Python 封装 |
| 61 | +├── pyproject.toml # Python package 配置文件,主要描述 build backend |
| 62 | +├── README.md |
| 63 | +├── setup.py # Python package 构建脚本,用于 build backend setuptools 的调用 |
| 64 | +└── test.py # 测试脚本 |
| 65 | +``` |
| 66 | + |
| 67 | +从构建流程来看,我们主要关注的是: |
| 68 | + |
| 69 | +- `pyproject.toml` 作为 PEP 517/518 标准的配置文件,描述了该 Python package 的构建后端为 `setuptools`,以及相关的元信息。 |
| 70 | + |
| 71 | + ```toml |
| 72 | + [build-system] |
| 73 | + requires = [ |
| 74 | + "setuptools", |
| 75 | + "torch", |
| 76 | + ] |
| 77 | + build-backend = "setuptools.build_meta" |
| 78 | + ``` |
| 79 | + |
| 80 | +- `setup.py` 作为 `setuptools` 的构建脚本,主要负责调用 `torch.utils.cpp_extension` 模块来编译 C++ 源码并生成可供 Python 调用的扩展模块。 |
| 81 | + |
| 82 | + ```python |
| 83 | + from setuptools import setup, find_packages |
| 84 | + from torch.utils import cpp_extension |
| 85 | + |
| 86 | + setup( |
| 87 | + name="extension", |
| 88 | + packages=find_packages(include=['extension']), |
| 89 | + ext_modules=[ |
| 90 | + cpp_extension.CUDAExtension( |
| 91 | + name="extension_cpp", |
| 92 | + sources=["csrc/muladd.cc"], |
| 93 | + ) |
| 94 | + ], |
| 95 | + cmdclass={'build_ext': cpp_extension.BuildExtension}, |
| 96 | + ) |
| 97 | + ``` |
| 98 | + |
| 99 | +- `csrc/muladd.cc` 作为自定义算子的核心实现文件,包含了算子的具体逻辑和注册代码,我们往往可以分为三部分: |
| 100 | + - 框架无关的算子逻辑实现部分,这部分逻辑并不使用 PyTorch 的 API,仅仅使用 C++/CUDA 标准库来实现。 |
| 101 | + |
| 102 | + ```cpp |
| 103 | + template<typename T> |
| 104 | + void muladd_cpu_impl(const T* a_ptr, const T* b_ptr, T c, T* result_ptr, int64_t numel) { |
| 105 | + for (int64_t i = 0; i < numel; i++) { |
| 106 | + result_ptr[i] = a_ptr[i] * b_ptr[i] + c; |
| 107 | + } |
| 108 | + } |
| 109 | + ``` |
| 110 | + - PyTorch C++ API 相关的部分,这部分代码会使用 `at::Tensor` 等 PyTorch C++ API 来进行张量操作和内存管理。 |
| 111 | + ```cpp |
| 112 | + at::Tensor muladd_cpu(at::Tensor a, const at::Tensor& b, double c) { |
| 113 | + TORCH_CHECK(a.sizes() == b.sizes()); |
| 114 | + TORCH_CHECK(a.dtype() == at::kFloat); |
| 115 | + TORCH_CHECK(b.dtype() == at::kFloat); |
| 116 | + TORCH_INTERNAL_ASSERT(a.device().type() == at::DeviceType::CPU); |
| 117 | + TORCH_INTERNAL_ASSERT(b.device().type() == at::DeviceType::CPU); |
| 118 | + at::Tensor a_contig = a.contiguous(); |
| 119 | + at::Tensor b_contig = b.contiguous(); |
| 120 | + at::Tensor result = torch::empty(a_contig.sizes(), a_contig.options()); |
| 121 | + const float* a_ptr = a_contig.data_ptr<float>(); |
| 122 | + const float* b_ptr = b_contig.data_ptr<float>(); |
| 123 | + float* result_ptr = result.data_ptr<float>(); |
| 124 | + muladd_cpu_impl<float>(a_ptr, b_ptr, static_cast<float>(c), result_ptr, result.numel()); |
| 125 | + return result; |
| 126 | + } |
| 127 | + ``` |
| 128 | + - 算子注册部分,这部分代码会使用 PyTorch 提供的注册宏(如 `TORCH_LIBRARY`)来完成算子的注册工作。 |
| 129 | + |
| 130 | + ```cpp |
| 131 | + extern "C" { |
| 132 | + /* Creates a dummy empty _C module that can be imported from Python. |
| 133 | + The import from Python will load the .so consisting of this file |
| 134 | + in this extension, so that the TORCH_LIBRARY static initializers |
| 135 | + below are run. */ |
| 136 | + PyObject* PyInit_extension_cpp(void) { |
| 137 | + static struct PyModuleDef module_def = { |
| 138 | + PyModuleDef_HEAD_INIT, |
| 139 | + "extension_cpp", /* name of module */ |
| 140 | + NULL, /* module documentation, may be NULL */ |
| 141 | + -1, /* size of per-interpreter state of the module, |
| 142 | + or -1 if the module keeps state in global variables. */ |
| 143 | + NULL, /* methods */ |
| 144 | + }; |
| 145 | + return PyModule_Create(&module_def); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + TORCH_LIBRARY(extension_cpp, m) { |
| 150 | + m.def("muladd_cpp(Tensor a, Tensor b, float c) -> Tensor"); |
| 151 | + } |
| 152 | + |
| 153 | + TORCH_LIBRARY_IMPL(extension_cpp, CPU, m) { |
| 154 | + m.impl("muladd_cpp", &muladd_cpu); |
| 155 | + } |
| 156 | + ``` |
| 157 | + |
| 158 | + |
| 159 | +从执行流程来看,在 Python 端调用该自定义算子时,主要经历了如下几个步骤: |
| 160 | + |
| 161 | +- 在 `test.py` 中导入自定义算子 Python package `extension`。 |
| 162 | +- 在 `extension/__init__.py` 中通过 `torch.ops.extension_cpp.muladd_cpp` 来调用 C++ 扩展模块中的自定义算子,从而调用到上面注册的 `muladd_cpp` 算子。 |
| 163 | + |
| 164 | + ```python |
| 165 | + def muladd(a: torch.Tensor, b: torch.Tensor, c: float) -> torch.Tensor: |
| 166 | + return torch.ops.extension_cpp.muladd_cpp(a, b, c) |
| 167 | + ``` |
| 168 | + |
| 169 | +### 调整构建脚本,使用 PaddlePaddle 编译自定义算子 |
| 170 | + |
| 171 | +由于原本的构建脚本是基于 PyTorch 的 `torch.utils.cpp_extension` 模块来完成编译的,因此我们需要将其替换为 PaddlePaddle 提供的自定义算子编译方式。 |
| 172 | + |
| 173 | +由于我们提供了 `paddle.compat.enable_torch_proxy()` 代理层来兼容 PyTorch 的 C++ API,因此我们可以使用该 API 实现 torch API 的一键兼容调用。 |
| 174 | + |
| 175 | +```diff |
| 176 | ++import paddle |
| 177 | ++paddle.compat.enable_torch_proxy() # Enable torch proxy globally |
| 178 | + |
| 179 | +from setuptools import setup, find_packages |
| 180 | +# 如下的 torch extension 已经被 PaddlePaddle 的同等功能替代(即 paddle.utils.cpp_extension) |
| 181 | +# 下面的代码完全不需要修改即可运行 |
| 182 | +from torch.utils import cpp_extension |
| 183 | + |
| 184 | +setup( |
| 185 | + name="extension", |
| 186 | + packages=find_packages(include=['extension']), |
| 187 | + ext_modules=[ |
| 188 | + cpp_extension.CUDAExtension( |
| 189 | + name="extension_cpp", |
| 190 | + sources=["csrc/muladd.cc"], |
| 191 | + ) |
| 192 | + ], |
| 193 | + cmdclass={'build_ext': cpp_extension.BuildExtension}, |
| 194 | +) |
| 195 | +``` |
| 196 | + |
| 197 | +对于本示例来说,仅仅需要在 `setup.py` 中添加上述两行代码即可完成迁移工作,其他代码均无需修改。但是自定义算子代码库一般各式各样,可能还需要根据实际情况进行一些调整,关于更多细节请参考 [`paddle.utils.cpp_extension.setup` 文档](https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/utils/cpp_extension/setup_cn.html)。 |
| 198 | + |
| 199 | +### 尝试编译并修复 |
| 200 | + |
| 201 | +完成构建脚本的修改后,即可尝试编译自定义算子: |
| 202 | + |
| 203 | +```bash |
| 204 | +pip install . --no-build-isolation |
| 205 | +``` |
| 206 | + |
| 207 | +由于我们提供了 PyTorch C++ API 兼容层,因此理想情况下大多数用户的自定义算子代码都可以直接通过编译而无需修改。 |
| 208 | + |
| 209 | +PyTorch C++ API 兼容层本质上是以 PyTorch C++ API 作为用户调用接口,并在底层映射至 PaddlePaddle C++ API 来实现的。以 `at::Tensor` 为例,你所调用的 `at::Tensor` 实际上是一个代理类,该类内部持有一个 `paddle::Tensor` 对象,并将所有对 `at::Tensor` 的操作映射为对 `paddle::Tensor` 的操作。 |
| 210 | + |
| 211 | +```cpp |
| 212 | +// paddle/phi/api/include/compat/ATen/core/TensorBody.h |
| 213 | +namespace at { |
| 214 | +using PaddleTensor = paddle::Tensor; |
| 215 | + |
| 216 | +class Tensor : public TensorBase { |
| 217 | + public: |
| 218 | + Tensor() = default; |
| 219 | + Tensor(const PaddleTensor& tensor) : TensorBase(tensor){}; // NOLINT |
| 220 | + Tensor(const Tensor& tensor) = default; |
| 221 | + Tensor(Tensor&& tensor) = default; |
| 222 | + |
| 223 | + void* data_ptr() const { return const_cast<void*>(tensor_.data()); } |
| 224 | + template <typename T> |
| 225 | + T* data_ptr() const { |
| 226 | + return const_cast<T*>(tensor_.data<T>()); |
| 227 | + } |
| 228 | + |
| 229 | + c10::IntArrayRef sizes() const { |
| 230 | + return compat::_PD_PhiDDimToIntArrayRef(tensor_.dims()); |
| 231 | + } |
| 232 | + |
| 233 | + int64_t numel() const { return tensor_.numel(); } |
| 234 | + |
| 235 | + c10::ScalarType dtype() const { // Should we use `TypeMeta` here? |
| 236 | + return compat::_PD_PhiDataTypeToAtenScalarType(tensor_.dtype()); |
| 237 | + } |
| 238 | + |
| 239 | + c10::Device device() const { return c10::Device(tensor_.place()); } |
| 240 | + |
| 241 | + int64_t dim() const { return tensor_.dims().size(); } |
| 242 | + int64_t ndimension() const { return dim(); } |
| 243 | + |
| 244 | + Tensor& fill_(const at::Scalar& value) const { |
| 245 | + paddle::experimental::fill_(const_cast<PaddleTensor&>(tensor_), value); |
| 246 | + return const_cast<at::Tensor&>(*this); |
| 247 | + } |
| 248 | + |
| 249 | + Tensor& zero_() const { |
| 250 | + paddle::experimental::fill_(const_cast<PaddleTensor&>(tensor_), 0.0); |
| 251 | + return const_cast<at::Tensor&>(*this); |
| 252 | + } |
| 253 | + |
| 254 | + PaddleTensor _PD_GetInner() const { return tensor_; } |
| 255 | + PaddleTensor& _PD_GetInner() { return tensor_; } |
| 256 | +}; |
| 257 | + |
| 258 | +} // namespace at |
| 259 | +namespace torch { |
| 260 | +using at::Tensor; |
| 261 | +} // namespace torch |
| 262 | +``` |
| 263 | +
|
| 264 | +完整的兼容层代码见 [`paddle/phi/api/include/compat`](https://github.com/PaddlePaddle/Paddle/tree/develop/paddle/phi/api/include/compat),我们提供了与 PyTorch C++ API 相同的头文件结构和命名空间,只需按原有方式调用即可。 |
| 265 | +
|
| 266 | +不过目前兼容层还在持续完善中,部分常见 API 尚未覆盖到,此时就会出现编译错误,你可以根据编译错误提示来定位并修复相关代码。 |
| 267 | +
|
| 268 | +以 `torch::empty` 为例,假设算子库中使用了该 API,但 Paddle 没有提供该 API 的兼容实现,就会出现编译错误: |
| 269 | +
|
| 270 | +```text |
| 271 | +/workspace/cross-ecosystem-custom-op-example/csrc/muladd.cc: In function ‘at::Tensor muladd_cpu(at::Tensor, const at::Tensor&, double)’: |
| 272 | +/workspace/cross-ecosystem-custom-op-example/csrc/muladd.cc:54:30: error: ‘empty’ is not a member of ‘torch’ |
| 273 | + 54 | at::Tensor result = torch::empty(a_contig.sizes(), a_contig.options()); |
| 274 | + | ^~~~~ |
| 275 | +``` |
| 276 | + |
| 277 | +此时我们可以选择将 PyTorch 的 structs 转换为 Paddle 的 structs,并用 PaddlePaddle 提供的等效 API 来实现该功能: |
| 278 | + |
| 279 | +即将下面的代码: |
| 280 | + |
| 281 | +```cpp |
| 282 | +// PyTorch 原代码 |
| 283 | +at::Tensor result = torch::empty(a_contig.sizes(), a_contig.options()); |
| 284 | +``` |
| 285 | + |
| 286 | +我们可以将其替换为: |
| 287 | + |
| 288 | +```cpp |
| 289 | +// 替换为 PaddlePaddle 等效实现 |
| 290 | +auto paddle_size = a_contig.sizes()._PD_ToPaddleIntArray(); // 将 PyTorch IntArrayRef 转为 Paddle IntArray |
| 291 | +auto paddle_dtype = compat::_PD_AtenScalarTypeToPhiDataType(a_contig.dtype()); // 将 PyTorch ScalarType 转为 Paddle DataType |
| 292 | +auto paddle_place = a_contig.options()._PD_GetPlace(); // 将 PyTorch Device 转为 Paddle Place |
| 293 | +auto paddle_result = paddle::experimental::empty(paddle_size, paddle_dtype, paddle_place); // 调用 PaddlePaddle 的 empty API |
| 294 | +at::Tensor result(paddle_result); // 将 Paddle Tensor 包装为 PyTorch Tensor |
| 295 | +``` |
| 296 | +
|
| 297 | +更多 PaddlePaddle C++ API 的使用方式可参考 [PaddlePaddle C++ 自定义算子文档](https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/custom_op/new_cpp_op_cn.html)。通过这种方式,你可以逐步修复编译错误,直至自定义算子能够成功编译通过。 |
| 298 | +
|
| 299 | +### 运行测试并修复 |
| 300 | +
|
| 301 | +完成编译后,即可运行测试脚本来验证自定义算子的正确性,由于原本的测试脚本是基于 PyTorch 框架来实现的,因此我们需要改写测试脚本以适配 PaddlePaddle 框架。 |
| 302 | +
|
| 303 | +```python |
| 304 | +import paddle |
| 305 | +paddle.compat.enable_torch_proxy(scope={"extension"}) # 仅启用 extension 包的 torch 代理 |
| 306 | +import extension |
| 307 | +
|
| 308 | +x = paddle.tensor([1.0, 2.0, 3.0]) |
| 309 | +y = paddle.tensor([4.0, 5.0, 6.0]) |
| 310 | +z = 2.0 |
| 311 | +result = extension.muladd(x, y, z) |
| 312 | +print(result) # Expected output: tensor([ 6., 12., 20.]) |
| 313 | +``` |
| 314 | + |
| 315 | +由于 `extension` 包中仍然使用了 `torch` 模块下的 Python API,因此我们需要启用 `torch` 代理来确保这些 API 能够正确映射至 PaddlePaddle 框架。为了避免对其他代码产生影响,我们可以通过 `scope` 参数来限定代理的作用范围。 |
| 316 | + |
| 317 | +当然,与 C++ 端类似,Python 端的兼容层也还在持续完善中,部分常见 API 尚未覆盖到,如果遇到此类错误,你可以尝试参考 [PaddlePaddle Python API 文档](https://www.paddlepaddle.org.cn/documentation/docs/zh/api/index_cn.html) 和 [PyTorch 最新 release 与 Paddle develop API 映射表](https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/model_convert/convert_from_pytorch/pytorch_api_mapping_cn.html)来寻找等效的 PaddlePaddle API 并进行替换,直到运行时不再报错且结果正确为止。 |
| 318 | + |
| 319 | +至此,一个 PyTorch 自定义算子就成功迁移至 PaddlePaddle 框架了! |
| 320 | + |
| 321 | +### 总结 |
| 322 | + |
| 323 | +通过上述步骤,我们介绍了如何将一个简单的 PyTorch 自定义算子迁移至 PaddlePaddle 框架。总体来说,迁移工作主要包括以下几个方面: |
| 324 | + |
| 325 | +- 调整构建脚本,使用 PaddlePaddle 提供的自定义算子编译方式来替换原有的 PyTorch 构建方式。 |
| 326 | +- 修复 C++ 端的编译错误,主要是由于部分 PyTorch C++ API 尚未覆盖到,需要借助 PaddlePaddle C++ API 来实现等效功能。 |
| 327 | +- 改写 Python 端的测试脚本,借助 torch proxy 代理层一键替换 PyTorch Python API,并根据实际情况进行部分 API 替换。 |
| 328 | + |
| 329 | +目前无论是 C++ 端还是 Python 端的兼容层都还在持续完善中,未来我们会不断补充更多常用 API 的兼容实现,从而进一步降低用户的迁移成本。同时我们也非常欢迎社区用户参与到兼容层的建设中来,共同推动跨生态自定义算子的互通与发展! |
0 commit comments