已合并
[feat]add cpp_extension OpCommand guide #5744
梁松伟创建于 14 天前
[feat]add cpp_extension OpCommand guide #5744
已合并
梁松伟创建于 14 天前
4 个文件变更+214-69
@@ -1,12 +1,14 @@
1-# 适配开发及调用1+# 自定义算子 C++ 扩展开发示例
2 2 
3-## 目录结构介绍3+本示例演示如何使用 Ascend C 实现自定义算子 Kernel,并通过 C++ 扩展(`cpp_extension`)方式集成到 PyTorch,最终在 Python 侧调用。涵盖工程结构、编译、安装、测试全流程。
4+ 
5+## 目录结构
4 6 
5```text7```text
6├── examples8├── examples
7| ├── cpp_extension9| ├── cpp_extension
8| │ ├── csrc10| │ ├── csrc
9-| │ │ ├── add_custom.asc # Add算子实现11+| │ │ ├── add_custom.asc # Add算子实现(含 5 种正确启动方式对比)
10| │ │ ├── trig_inplace_custom.asc # 原地三角函数算子实现12| │ │ ├── trig_inplace_custom.asc # 原地三角函数算子实现
11| │ │ └── pybind11.asc # pybind绑定自定义算子13| │ │ └── pybind11.asc # pybind绑定自定义算子
12| | ├── op_extension/14| | ├── op_extension/
@@ -21,45 +23,61 @@
21 23 
22## 新增自定义算子24## 新增自定义算子
23 25 
24-本示例以Add算子为例,展示了如何在 PyTorch 中使用Ascend C扩展自定义算子Kernel实现,并通过Python的接口调用实现的算子。26+### 1. Kernel 实现
25 27 
26-### kernel实现28+ `./csrc/` 下创建 `.asc` 文件,基于 Ascend C 实现算子 Kernel。Ascend C 开发参考[昇腾社区文档](https://www.hiascend.com/ascend-c)。
27 29 
28- 本小节主要介绍如何实现Kernel算子,本样例基于Ascend C进行开发算子。如何使用Ascend C实现算子kernel可以参考昇腾社区档[昇腾Ascend C](https://www.hiascend.com/ascend-c)。30+ `add_custom.asc` 为例,文件包含三部分:
29 31 
30- 在`./csrc/`目录下创建一个名为`add_custom.asc`的文件,这是我们自定义加法Kernel的实现文件。样例中实现了一个`run_ascendc_add`的核函数。32+| 组成 | 说明 |
33+|------|------|
34+| `KernelAdd` 类 | 算子设备侧实现,包含 `Init` / `Process` / `CopyIn` / `Compute` / `CopyOut` |
35+| `add_custom` 核函数 | `__global__ __vector__` 修饰的设备入口 |
36+| `ascendc_add1/2/3/4/5` | 5 种正确的 Host 侧启动方式,对比不同 stream 获取与队列管理策略 |
31 37 
32-### 将Kernel实现与Python接口集成38+**5 种启动方式对比**:
33 39 
34- 本小节主要介绍如何封装实现的kernel算子,以及绑定为Python的接口。40+| 方式 | 函数 | stream 获取 | 队列管理 | 适用场景 |
35- 代码实现在./csrc/ 目录下。41+|------|------|------------|---------|---------|
42+| 1 | `ascendc_add1` | `NPUStream` 对象 | `<<<>>>` 内部清 queue | 简单同步场景 |
43+| 2 | `ascendc_add2` | `stream(true)` | 清 queue 后直接启动 | 等价方式1 |
44+| 3 | `ascendc_add3` | `stream(false)` | OpCommand 入 queue | **推荐**,保留流水线性能 |
45+| 4 | `ascendc_add4` | `stream(true)` | 清 queue + OpCommand 入 queue | 语义直观 |
46+| 5 | `ascendc_add5` | `stream()` | 等待 queue 完成后启动 | 等价方式2 |
36 47 
37-#### 封装Python模块48+> **推荐方式3**:`stream(false)` 配合 `OpCommand::RunOpApiV2`,保留 TaskQueue 流水线性能,与 TorchNPU 内置算子行为一致。
38 49 
39-在`pybind11.asc`文件中使用了pybind11库来将C++代码封装成Python模块,在Python侧可以通过`import`方式进行调用。例如:50+### 2. Python 模块绑定
40-
41- ```c++
42- PYBIND11_MODULE(custom_ops, m)
43- {
44- m.def("custom_add", &ascendc_ops::run_ascendc_add, "");
45- }
46- ```
47 51 
48- 通过此绑定,python侧可通过`op_extension.ops.custom_add`自定义的API52+`pybind11.asc` 中使 pybind11 将 C++ 函数暴露为 Python 接口Python 侧仅暴露推荐方式3:
49 53 
50-#### Aten IR实现54+```cpp
55+namespace ascendc_ops {
56+// 方式3(推荐): stream(false) + OpCommand::RunOpApiV2
57+at::Tensor ascendc_add3(const at::Tensor &x, const at::Tensor &y);
58+at::Tensor run_trig_custom(const at::Tensor &x, const at::Tensor &out_sin, const at::Tensor &out_cos);
59+}
51 60 
52-根据Aten IR定义适配算子。61+PYBIND11_MODULE(custom_ops_lib, m)
53-TorchNPU的算子下发和执行是异步的,通过TASKQUEUE实现,62+{
54-样例中,我们通过`at_npu::native::OpCommand::RunOpApiV2`方法,将算子执行入队到TorchNPU的TASKQUEUE。样例如下:63+ m.def("custom_add", &ascendc_ops::ascendc_add3, "");
64+ m.def("custom_trig", &ascendc_ops::run_trig_custom, "");
65+}
66+```
55 67 
56- ```c++68+### 3. Aten IR 实现
69+ 
70+算子通过 `at_npu::native::OpCommand::RunOpApiV2` 入队到 TorchNPU 的 TaskQueue,实现异步下发。推荐方式3的实现:
71+ 
72+```cpp
57#include "torch_npu/csrc/core/npu/NPUStream.h"73#include "torch_npu/csrc/core/npu/NPUStream.h"
58#include "torch_npu/csrc/framework/OpCommand.h"74#include "torch_npu/csrc/framework/OpCommand.h"
75+ 
59namespace ascendc_ops {76namespace ascendc_ops {
60-at::Tensor run_ascendc_add(const at::Tensor &x, const at::Tensor &y)77+at::Tensor ascendc_add3(const at::Tensor &x, const at::Tensor &y)
61{78{
62- auto acl_stream = c10_npu::getCurrentNPUStream().stream(true);79+ // stream(false) 返回 ACL stream 但不清 queue
80+ auto acl_stream = c10_npu::getCurrentNPUStream().stream(false);
63 at::Tensor z = at::empty_like(x);81 at::Tensor z = at::empty_like(x);
64 uint32_t blockDim = 8;82 uint32_t blockDim = 8;
65 uint32_t totalLength = 1;83 uint32_t totalLength = 1;
@@ -67,20 +85,21 @@ at::Tensor run_ascendc_add(const at::Tensor &x, const at::Tensor &y)
67 totalLength *= size;85 totalLength *= size;
68 }86 }
69 // Launch the custom kernel use <<<>>>87 // Launch the custom kernel use <<<>>>
70- auto acl_call = [=]() -> int{88+ auto acl_call = [=]() -> int {
71- add_custom<<<blockDim, nullptr, acl_stream>>>((uint8_t *)(x.mutable_data_ptr()), (uint8_t *)(y.mutable_data_ptr()),89+ add_custom<<<blockDim, nullptr, acl_stream>>>(
72- (uint8_t *)(z.mutable_data_ptr()), totalLength);90+ (uint8_t *)(x.mutable_data_ptr()),
91+ (uint8_t *)(y.mutable_data_ptr()),
92+ (uint8_t *)(z.mutable_data_ptr()),
93+ totalLength);
73 return 0;94 return 0;
74 };95 };
75 at_npu::native::OpCommand::RunOpApiV2("ascendc_add", acl_call);96 at_npu::native::OpCommand::RunOpApiV2("ascendc_add", acl_call);
76- 
77 return z;97 return z;
78}98}
79- 
80} // namespace ascendc_ops99} // namespace ascendc_ops
81- ```100+```
82 101 
83-上述主要介绍了自定义算子kernel集成的必备流程。102+上述主要介绍了自定义算子kernel集成的必备流程。完整的5种正确启动方式见`./csrc/add_custom.asc`,Python侧通过`./csrc/pybind11.asc`仅暴露推荐方式3。
84 103 
85最后,通过创建ops路径,定义python接口,通过`module_name.ops.custom_add`可以调用自定义算子。测试样例如下:104最后,通过创建ops路径,定义python接口,通过`module_name.ops.custom_add`可以调用自定义算子。测试样例如下:
86 105 
@@ -96,18 +115,31 @@ output = op_extension.ops.custom_add(x_npu, y_npu)
96 115 
97## 运行自定义的算子116## 运行自定义的算子
98 117 
99- 运行依赖PyTorch、TorchNPU和CANN。具体安装步骤参考[TorchNPU文档](https://gitcode.com/ascend/pytorch#%E5%AE%89%E8%A3%85)118+### 1. 编译 whl 包
100- 运行流程:
101- 
102- 1. 运行setup脚本,编译生成whl包。
103 119 
104 ```bash120 ```bash
105 python setup.py bdist_wheel121 python setup.py bdist_wheel
106 ```122 ```
107 123 
108- 我们的编译工程通过setuptools已为用户封装好如何编译算子kernel和集成到PyTorch。124+`setup.py` 关键逻辑:
109 125 
110- 2. 安装whl包126+| 步骤 | 实现 | 说明 |
127+|------|------|------|
128+| 源码收集 | `glob.glob("csrc/*.asc")` | 自动收集所有 `.asc` 文件 |
129+| 架构识别 | `get_npu_arch()` | 通过 `npu-smi info` 解析芯片型号,映射到 `dav-2201`/`dav-3510` |
130+| 依赖路径 | `get_dependency_paths()` | 自动收集 torch / torch_npu / Python 的 include 与 lib 路径 |
131+| ABI 对齐 | `torch._C._GLIBCXX_USE_CXX11_ABI` | 与 PyTorch ABI 保持一致 |
132+| 编译器 | `bisheng -x asc` | 使用 CANN 提供的 bisheng 编译器 |
133+ 
134+编译产物位于 `dist/op_extension-0.1-*.whl`
135+ 
136+可选环境变量:
137+ 
138+```bash
139+USE_NINJA=1 python setup.py bdist_wheel # 启用 ninja 加速
140+```
141+ 
142+### 2. 安装 whl 包
111 143 
112 ```bash144 ```bash
113 cd dist145 cd dist
@@ -135,43 +135,143 @@ __global__ __vector__ void add_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z, uint32_t
135}135}
136 136 
137// ------------------------------ C++接口函数 ------------------------------137// ------------------------------ C++接口函数 ------------------------------
138-// ascendc_add: PyTorch和自定义内核之间的接口函数138+// 本文件提供 5 种正确的内核启动方式:
139-// 参数:139+// - 方式1: 直接传入 NPUStream 对象,<<<>>> 内部清 queue
140-// x, y: 输入张量140+// - 方式2: stream(true) 获取 ACL stream,返回前清 queue
141-// 返回值:141+// - 方式3(推荐): stream(false) 配合 OpCommand,通过入 queue 方式同步
142-// 加法结果张量142+// - 方式4: stream(true) 清 queue + OpCommand 入 queue
143+// - 方式5: stream() 获取 ACL stream,返回前等待 queue 内操作完成(等价方式2)
144+//
145+// 推荐使用方式3:性能最优(保留 TaskQueue 流水线),且与 TorchNPU 内置算子行为一致。
146+// Python 侧仅暴露方式3。
147+ 
143namespace ascendc_ops {148namespace ascendc_ops {
144-at::Tensor run_ascendc_add(const at::Tensor &x, const at::Tensor &y)149+ 
150+// 方式1: 清 queue - 直接传入 NPUStream 对象
151+// 使用 getCurrentNPUStream() 获取 NPUStream 对象,<<<>>> 内部会在内核启动前清 queue,
152+// 确保与之前任务的同步。
153+at::Tensor ascendc_add1(const at::Tensor &x, const at::Tensor &y)
145{154{
146- // 获取当前NPU流155+ // 获取 NPUStream 对象,此时不会清 queue
147- auto acl_stream = c10_npu::getCurrentNPUStream().stream(true);156+ auto npu_stream = c10_npu::getCurrentNPUStream();
148- 
149- // 创建与输入形状相同的空张量作为输出
150 at::Tensor z = at::empty_like(x);157 at::Tensor z = at::empty_like(x);
151- 
152- // 设置块维度
153 uint32_t blockDim = 8;158 uint32_t blockDim = 8;
154- 
155- // 计算输入张量的总元素数量
156 uint32_t totalLength = 1;159 uint32_t totalLength = 1;
157 for (uint32_t size : x.sizes()) {160 for (uint32_t size : x.sizes()) {
158 totalLength *= size;161 totalLength *= size;
159 }162 }
163+ auto xGm = (uint8_t *)(x.mutable_data_ptr());
164+ auto yGm = (uint8_t *)(y.mutable_data_ptr());
165+ auto zGm = (uint8_t *)(z.mutable_data_ptr());
166+ // <<<>>> 传入 NPUStream 对象,内部会在内核启动前清 queue
167+ add_custom<<<blockDim, nullptr, npu_stream>>>(xGm, yGm, zGm, totalLength);
168+ return z;
169+}
160 170 
161- // 定义内核启动lambda函数171+ 
162- auto acl_call = [=]() -> int{172+// 方式2: queue - 使用 stream(true) 获取 ACL stream 并清 queue
163- // 启动自定义内核173+// stream(true) 返回 ACL stream(aclrtStream),在返回前会清 queue,确保与之前任务的同步。
164- add_custom<<<blockDim, nullptr, acl_stream>>>((uint8_t *)(x.mutable_data_ptr()),174+// 与方式1等价。
165- (uint8_t *)(y.mutable_data_ptr()),175+at::Tensor ascendc_add2(const at::Tensor &x, const at::Tensor &y)
166- (uint8_t *)(z.mutable_data_ptr()),176+{
167- totalLength);177+ // stream(true) 在返回 ACL stream 前会清 queue
178+ auto acl_stream = c10_npu::getCurrentNPUStream().stream(true);
179+ at::Tensor z = at::empty_like(x);
180+ uint32_t blockDim = 8;
181+ uint32_t totalLength = 1;
182+ for (uint32_t size : x.sizes()) {
183+ totalLength *= size;
184+ }
185+ auto xGm = (uint8_t *)(x.mutable_data_ptr());
186+ auto yGm = (uint8_t *)(y.mutable_data_ptr());
187+ auto zGm = (uint8_t *)(z.mutable_data_ptr());
188+ // Launch kernel use <<<>>>
189+ add_custom<<<blockDim, nullptr, acl_stream>>>(xGm, yGm, zGm, totalLength);
190+ return z;
191+}
192+ 
193+ 
194+// 方式3: Lambda 入 queue - 使用 stream(false) 配合 OpCommand 进行 queue 管理 [推荐]
195+// stream(false) 返回 ACL stream 但不清 queue,结合 OpCommand::RunOpApiV2 使用,
196+// 内核启动被封装在 lambda 中,通过正确地入 queue 和出 queue 确保正确的执行顺序,
197+// 同时保留 TaskQueue 的流水线性能。
198+at::Tensor ascendc_add3(const at::Tensor &x, const at::Tensor &y)
199+{
200+ // stream(false) 返回 ACL stream 但不清 queue
201+ auto acl_stream = c10_npu::getCurrentNPUStream().stream(false);
202+ at::Tensor z = at::empty_like(x);
203+ uint32_t blockDim = 8;
204+ uint32_t totalLength = 1;
205+ for (uint32_t size : x.sizes()) {
206+ totalLength *= size;
207+ }
208+ auto xGm = (uint8_t *)(x.mutable_data_ptr());
209+ auto yGm = (uint8_t *)(y.mutable_data_ptr());
210+ auto zGm = (uint8_t *)(z.mutable_data_ptr());
211+ 
212+ // 定义内核启动 lambda 函数
213+ auto acl_call = [=]() -> int {
214+ // Launch kernel use <<<>>>
215+ add_custom<<<blockDim, nullptr, acl_stream>>>(xGm, yGm, zGm, totalLength);
168 return 0;216 return 0;
169 };217 };
170 218 
171- // 通过OpCommand运行内核219+ // 通过 OpCommand 运行内核,内部会进行入 queue 和出 queue 的操作,
220+ // 确保与 stream 中其他 NPU 操作的正确同步
172 at_npu::native::OpCommand::RunOpApiV2("ascendc_add", acl_call);221 at_npu::native::OpCommand::RunOpApiV2("ascendc_add", acl_call);
222+ return z;
223+}
173 224 
174- // 返回计算结果225+ 
226+// 方式4: 清 queue + Lambda 入 queue
227+// stream(true) 返回 ACL stream(aclrtStream) 并清 queue,然后在 lambda 中使用该 stream。
228+// 这种组合是安全的:先清 queue 确保之前的任务完成,再通过 OpCommand 入 queue 执行内核。
229+// 与方式3相比会损失 TaskQueue 流水线性能,但语义更直观。
230+at::Tensor ascendc_add4(const at::Tensor &x, const at::Tensor &y)
231+{
232+ // stream(true) 在返回 ACL stream 前会清 queue
233+ auto acl_stream = c10_npu::getCurrentNPUStream().stream(true);
234+ at::Tensor z = at::empty_like(x);
235+ uint32_t blockDim = 8;
236+ uint32_t totalLength = 1;
237+ for (uint32_t size : x.sizes()) {
238+ totalLength *= size;
239+ }
240+ auto xGm = (uint8_t *)(x.mutable_data_ptr());
241+ auto yGm = (uint8_t *)(y.mutable_data_ptr());
242+ auto zGm = (uint8_t *)(z.mutable_data_ptr());
243+ 
244+ // 定义内核启动 lambda 函数
245+ auto acl_call = [=]() -> int {
246+ // Launch kernel use <<<>>>
247+ add_custom<<<blockDim, nullptr, acl_stream>>>(xGm, yGm, zGm, totalLength);
248+ return 0;
249+ };
250+ 
251+ // 通过 OpCommand 运行内核,内部会进行入 queue 和出 queue 的操作
252+ at_npu::native::OpCommand::RunOpApiV2("ascendc_add", acl_call);
253+ return z;
254+}
255+ 
256+ 
257+// 方式5: 清 queue - 使用 stream() 获取 ACL stream 并等待 queue 内操作
258+// stream()(无参数)在返回 ACL stream(aclrtStream) 前会等待 queue 内操作完成,
259+// 与方式2(stream(true))等价,确保与之前任务的同步。
260+at::Tensor ascendc_add5(const at::Tensor &x, const at::Tensor &y)
261+{
262+ // stream() 无参数,返回 ACL stream 前会等待 queue 内操作完成
263+ auto acl_stream = c10_npu::getCurrentNPUStream().stream();
264+ at::Tensor z = at::empty_like(x);
265+ uint32_t blockDim = 8;
266+ uint32_t totalLength = 1;
267+ for (uint32_t size : x.sizes()) {
268+ totalLength *= size;
269+ }
270+ auto xGm = (uint8_t *)(x.mutable_data_ptr());
271+ auto yGm = (uint8_t *)(y.mutable_data_ptr());
272+ auto zGm = (uint8_t *)(z.mutable_data_ptr());
273+ // Launch kernel use <<<>>>
274+ add_custom<<<blockDim, nullptr, acl_stream>>>(xGm, yGm, zGm, totalLength);
175 return z;275 return z;
176}276}
177 277 
@@ -2,13 +2,15 @@
2#include <torch/extension.h>2#include <torch/extension.h>
3 3 
4namespace ascendc_ops {4namespace ascendc_ops {
5-at::Tensor run_ascendc_add(const at::Tensor &x, const at::Tensor &y);5+// 方式3(推荐): stream(false) + OpCommand::RunOpApiV2,保留 TaskQueue 流水线性能
6+at::Tensor ascendc_add3(const at::Tensor &x, const at::Tensor &y);
6at::Tensor run_trig_custom(const at::Tensor &x, const at::Tensor &out_sin, const at::Tensor &out_cos);7at::Tensor run_trig_custom(const at::Tensor &x, const at::Tensor &out_sin, const at::Tensor &out_cos);
7}8}
8 9 
9// expose Ascend custom ops to Python10// expose Ascend custom ops to Python
10PYBIND11_MODULE(custom_ops_lib, m)11PYBIND11_MODULE(custom_ops_lib, m)
11{12{
12- m.def("custom_add", &ascendc_ops::run_ascendc_add, "");13+ // Python 侧仅暴露推荐方式3,其他方式仅作为源码参考
14+ m.def("custom_add", &ascendc_ops::ascendc_add3, "");
13 m.def("custom_trig", &ascendc_ops::run_trig_custom, "");15 m.def("custom_trig", &ascendc_ops::run_trig_custom, "");
14}16}
@@ -14,6 +14,7 @@
14#include <torch/extension.h>14#include <torch/extension.h>
15 15 
16#include "torch_npu/csrc/core/npu/NPUStream.h"16#include "torch_npu/csrc/core/npu/NPUStream.h"
17+#include "torch_npu/csrc/framework/OpCommand.h"
17#include "kernel_operator.h"18#include "kernel_operator.h"
18 19 
19constexpr uint32_t BUFFER_NUM = 2; //tensor num for each queue20constexpr uint32_t BUFFER_NUM = 2; //tensor num for each queue
@@ -106,9 +107,11 @@ __global__ __vector__ void trig_inplace_custom(GM_ADDR x, GM_ADDR out_sin, GM_AD
106}107}
107 108 
108namespace ascendc_ops {109namespace ascendc_ops {
110+// 使用推荐方式3: stream(false) + OpCommand::RunOpApiV2,保留 TaskQueue 流水线性能
109at::Tensor run_trig_custom(const at::Tensor &x, const at::Tensor &out_sin, const at::Tensor &out_cos)111at::Tensor run_trig_custom(const at::Tensor &x, const at::Tensor &out_sin, const at::Tensor &out_cos)
110{112{
111- auto acl_stream = c10_npu::getCurrentNPUStream().stream(true);113+ // stream(false) 返回 ACL stream 但不清 queue
114+ auto acl_stream = c10_npu::getCurrentNPUStream().stream(false);
112 at::Tensor out_tan = at::empty_like(x);115 at::Tensor out_tan = at::empty_like(x);
113 uint32_t blockDim = 8;116 uint32_t blockDim = 8;
114 uint32_t totalLength = 1;117 uint32_t totalLength = 1;
@@ -119,8 +122,16 @@ at::Tensor run_trig_custom(const at::Tensor &x, const at::Tensor &out_sin, const
119 auto sinGm = static_cast<uint8_t *>(const_cast<void *>(out_sin.storage().data()));122 auto sinGm = static_cast<uint8_t *>(const_cast<void *>(out_sin.storage().data()));
120 auto cosGm = static_cast<uint8_t *>(const_cast<void *>(out_cos.storage().data()));123 auto cosGm = static_cast<uint8_t *>(const_cast<void *>(out_cos.storage().data()));
121 auto tanGm = static_cast<uint8_t *>(const_cast<void *>(out_tan.storage().data()));124 auto tanGm = static_cast<uint8_t *>(const_cast<void *>(out_tan.storage().data()));
122- // Launch the custom kernel using <<<>>>125+ 
123- trig_inplace_custom<<<blockDim, nullptr, acl_stream>>>(xGm, sinGm, cosGm, tanGm, totalLength);126+ // 定义内核启动 lambda 函数
127+ auto acl_call = [=]() -> int {
128+ // Launch the custom kernel using <<<>>>
129+ trig_inplace_custom<<<blockDim, nullptr, acl_stream>>>(xGm, sinGm, cosGm, tanGm, totalLength);
130+ return 0;
131+ };
132+ 
133+ // 通过 OpCommand 运行内核,确保与 stream 中其他 NPU 操作的正确同步
134+ at_npu::native::OpCommand::RunOpApiV2("ascendc_trig", acl_call);
124 return out_tan;135 return out_tan;
125}136}
126-} // namespace ascendc_ops137+} // namespace ascendc_ops