已开启
【代码侦探Challenge01-DivCustom】补全逐元素除法算子实现+PR 经验证可正确运行。 #1839
zhangyatao9创建于 8月2日
【代码侦探Challenge01-DivCustom】补全逐元素除法算子实现+PR 经验证可正确运行。 #1839
已开启
zhangyatao9创建于 8月2日
共 3 个文件变更+398-0
@@ -0,0 +1,10 @@
1+cmake_minimum_required(VERSION 3.16)
2+find_package(ASC REQUIRED)
3+project(kernel_samples LANGUAGES ASC CXX)
4+ 
5+add_executable(div_test
6+ div_custom.asc
7+)
8+target_compile_options(div_test PRIVATE
9+ $<$<COMPILE_LANGUAGE:ASC>:--npu-arch=dav-2201>
10+)
@@ -0,0 +1,379 @@
1+// div_custom.asc
2+//
3+// Challenge01 · DivCustom
4+// 逐元素除法算子: z[i] = x[i] / y[i]
5+//
6+// 整体流程(一句话):
7+// 把 16384 个 float 分成 8 份给 8 个 AI Core,
8+// 每个核再把数据切成小片,用双缓冲流水线完成:
9+// 从全局内存搬到片上内存 → 做除法 → 存回全局内存
10+//
11+// 数据流图:
12+// GM(x,y) --CopyIn--> UB(xLocal,yLocal) --Compute(Div)--> UB(zLocal) --CopyOut--> GM(z)
13+ 
14+#include <cstdint>
15+#include <iostream>
16+#include <vector>
17+#include <algorithm>
18+#include <iterator>
19+#include "acl/acl.h" // AscendCL 运行时库(Host 侧用)
20+#include "kernel_operator.h" // AscendC 算子开发库(Device 侧用)
21+ 
22+// ------------------------------------------------------------
23+// 常量定义
24+// ------------------------------------------------------------
25+// BUFFER_NUM = 2 表示每个队列有 2 个槽位,实现双缓冲(Double Buffer)
26+// 这样第 n 次循环在计算时,第 n+1 次循环可以同时搬运数据,隐藏搬运延迟
27+constexpr uint32_t BUFFER_NUM = 2;
28+ 
29+// ------------------------------------------------------------
30+// Tiling 参数结构体
31+// 作用:告诉 kernel 总共有多少数据、要切成几份来处理
32+// 注意:这个结构体在 Host 侧赋值,然后按值传给 Device 侧 kernel
33+// ------------------------------------------------------------
34+struct DivCustomTilingData
35+{
36+ uint32_t totalLength; // 总数据量(元素个数),这里是 16384
37+ uint32_t tileNum; // 每个核内部再切成几份,这里是 8
38+};
39+ 
40+// ------------------------------------------------------------
41+// KernelDiv 类:算子的核心实现
42+// 职责:管理全局内存、片上内存、队列、搬运和计算
43+// ------------------------------------------------------------
44+class KernelDiv {
45+public:
46+ // 构造函数:什么都不做
47+ __aicore__ inline KernelDiv() {}
48+ 
49+ // --------------------------------------------------------
50+ // Init:初始化函数
51+ // 输入参数:
52+ // x, y, z — 全局内存(GM)上的三个数组首地址
53+ // totalLength — 总数据量
54+ // tileNum — 每个核内部的切分数
55+ //
56+ // 做的事情:
57+ // 1. 算出当前核负责多少数据(总长 ÷ 核数)
58+ // 2. 算出每次循环搬运/计算多少数据
59+ // 3. 让 GlobalTensor 指向当前核负责的那段 GM
60+ // 4. 在片上内存(UB)上分配队列缓冲区
61+ // --------------------------------------------------------
62+ __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z,
63+ uint32_t totalLength, uint32_t tileNum)
64+ {
65+ // step 1: 每个核处理的数据量 = 总数据量 ÷ AI Core 数量
66+ // AscendC::GetBlockNum() 返回当前启用的核数(这里是 8)
67+ this->blockLength = totalLength / AscendC::GetBlockNum();
68+ 
69+ // step 2: 记录核内切分数
70+ this->tileNum = tileNum;
71+ 
72+ // step 3: 每次循环处理的数据量
73+ // 公式:核内数据量 ÷ 切分数 ÷ 双缓冲槽位数
74+ // 例如:2048 ÷ 8 ÷ 2 = 128,每次处理 128 个 float
75+ this->tileLength = this->blockLength / tileNum / BUFFER_NUM;
76+ 
77+ // step 4: 让 GlobalTensor 指向当前核负责的那段全局内存
78+ // AscendC::GetBlockIdx() 返回当前核的编号(0 ~ 7)
79+ // 每个核的起始位置 = 数组首地址 + 当前核编号 × 每个核的数据量
80+ xGm.SetGlobalBuffer(
81+ (__gm__ float*)x + this->blockLength * AscendC::GetBlockIdx(),
82+ this->blockLength);
83+ yGm.SetGlobalBuffer(
84+ (__gm__ float*)y + this->blockLength * AscendC::GetBlockIdx(),
85+ this->blockLength);
86+ zGm.SetGlobalBuffer(
87+ (__gm__ float*)z + this->blockLength * AscendC::GetBlockIdx(),
88+ this->blockLength);
89+ 
90+ // step 5: 在片上内存(Unified Buffer)上分配队列缓冲区
91+ // 每个队列有 BUFFER_NUM 个槽位,每个槽位能放 tileLength 个 float
92+ // pipe.InitBuffer(队列名, 槽位数, 每个槽位的字节数)
93+ pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileLength * sizeof(float));
94+ pipe.InitBuffer(inQueueY, BUFFER_NUM, this->tileLength * sizeof(float));
95+ pipe.InitBuffer(outQueueZ, BUFFER_NUM, this->tileLength * sizeof(float));
96+ }
97+ 
98+ // --------------------------------------------------------
99+ // Process:主处理循环
100+ // 循环次数 = tileNum × BUFFER_NUM
101+ // 为什么是这个数?
102+ // - tileNum = 8,表示把核内数据切成 8 个大块
103+ // - BUFFER_NUM = 2,每个大块再分成 2 份来实现流水
104+ // - 所以总共 8×2 = 16 次循环
105+ // 每次循环执行:CopyIn → Compute → CopyOut
106+ // --------------------------------------------------------
107+ __aicore__ inline void Process()
108+ {
109+ const int32_t loopCount = this->tileNum * BUFFER_NUM;
110+ for (int32_t i = 0; i < loopCount; ++i) {
111+ CopyIn(i); // 从 GM 搬到 UB
112+ Compute(i); // 在 UB 上做除法
113+ CopyOut(i); // 从 UB 搬回 GM
114+ }
115+ }
116+ 
117+private:
118+ // --------------------------------------------------------
119+ // CopyIn:数据搬入
120+ // 作用:从全局内存(GM)把当前进度对应的数据块搬到片上内存(UB)
121+ // progress:当前是第几次循环(0 ~ 15)
122+ //
123+ // 步骤:
124+ // 1. 从输入队列分配一块 UB 空间(AllocTensor)
125+ // 2. 把 GM 上对应位置的数据拷贝过来(DataCopy)
126+ // 3. 把这块 UB 空间入队,通知下游“数据准备好了”(EnQue)
127+ // --------------------------------------------------------
128+ __aicore__ inline void CopyIn(int32_t progress)
129+ {
130+ // 从 x 输入队列分配一块 UB 空间
131+ AscendC::LocalTensor<float> xLocal = inQueueX.AllocTensor<float>();
132+ // 从 y 输入队列分配一块 UB 空间
133+ AscendC::LocalTensor<float> yLocal = inQueueY.AllocTensor<float>();
134+ 
135+ // 把 GM 上第 progress 块数据拷贝到 UB
136+ // xGm[progress * this->tileLength] 是 GM 上的起始位置
137+ // this->tileLength 是要拷贝的元素个数
138+ AscendC::DataCopy(xLocal, xGm[progress * this->tileLength], this->tileLength);
139+ AscendC::DataCopy(yLocal, yGm[progress * this->tileLength], this->tileLength);
140+ 
141+ // 入队:告诉 Compute 阶段数据已就绪
142+ inQueueX.EnQue(xLocal);
143+ inQueueY.EnQue(yLocal);
144+ }
145+ 
146+ // --------------------------------------------------------
147+ // Compute:计算
148+ // 作用:从队列取出输入数据,执行逐元素除法,结果放入输出队列
149+ //
150+ // 步骤:
151+ // 1. 从输入队列取出 x 和 y 的 UB 数据(DeQue)
152+ // 2. 从输出队列分配一块 UB 空间存放结果
153+ // 3. 执行 Div 指令:zLocal = xLocal / yLocal
154+ // 4. 把结果入队(EnQue)
155+ // 5. 释放输入数据的 UB 空间(FreeTensor),供后续循环复用
156+ // --------------------------------------------------------
157+ __aicore__ inline void Compute(int32_t progress)
158+ {
159+ // 从输入队列取出已经搬运好的数据
160+ AscendC::LocalTensor<float> xLocal = inQueueX.DeQue<float>();
161+ AscendC::LocalTensor<float> yLocal = inQueueY.DeQue<float>();
162+ 
163+ // 从输出队列分配一块空间存放计算结果
164+ AscendC::LocalTensor<float> zLocal = outQueueZ.AllocTensor<float>();
165+ 
166+ // 执行逐元素除法:zLocal[i] = xLocal[i] / yLocal[i]
167+ // 第三个参数是元素个数(this->tileLength)
168+ AscendC::Div(zLocal, xLocal, yLocal, this->tileLength);
169+ 
170+ // 把计算结果入队,通知 CopyOut 阶段可以搬出了
171+ outQueueZ.EnQue(zLocal);
172+ 
173+ // 释放输入数据的 UB 空间
174+ // 如果不释放,下次循环就没空间分配了
175+ inQueueX.FreeTensor(xLocal);
176+ inQueueY.FreeTensor(yLocal);
177+ }
178+ 
179+ // --------------------------------------------------------
180+ // CopyOut:结果搬出
181+ // 作用:从输出队列取出计算结果,搬回全局内存
182+ //
183+ // 步骤:
184+ // 1. 从输出队列取出结果数据(DeQue)
185+ // 2. 把 UB 上的结果拷贝回 GM 上对应的位置(DataCopy)
186+ // 3. 释放 UB 空间(FreeTensor)
187+ // --------------------------------------------------------
188+ __aicore__ inline void CopyOut(int32_t progress)
189+ {
190+ // 从输出队列取出计算结果
191+ AscendC::LocalTensor<float> zLocal = outQueueZ.DeQue<float>();
192+ 
193+ // 把结果从 UB 搬回 GM
194+ // 注意:DataCopy 的第一个参数是目标地址,第二个是源地址
195+ AscendC::DataCopy(zGm[progress * this->tileLength], zLocal, this->tileLength);
196+ 
197+ // 释放 UB 空间
198+ outQueueZ.FreeTensor(zLocal);
199+ }
200+ 
201+private:
202+ // --------------------------------------------------------
203+ // 硬件资源(这些是 AscendC 的核心概念)
204+ // --------------------------------------------------------
205+ 
206+ // TPipe:管道,统一管理片上内存(UB)的分配和回收
207+ AscendC::TPipe pipe;
208+ 
209+ // TQue:数据队列
210+ // 模板参数1:队列绑定的硬件位置
211+ // VECIN — 向量计算单元的输入侧
212+ // VECOUT — 向量计算单元的输出侧
213+ // 模板参数2:队列深度(槽位数),这里是 2(双缓冲)
214+ // inQueueX / inQueueY:输入数据的队列(GM → UB)
215+ // outQueueZ:输出结果的队列(UB → GM)
216+ AscendC::TQue<AscendC::TPosition::VECIN, BUFFER_NUM> inQueueX;
217+ AscendC::TQue<AscendC::TPosition::VECIN, BUFFER_NUM> inQueueY;
218+ AscendC::TQue<AscendC::TPosition::VECOUT, BUFFER_NUM> outQueueZ;
219+ 
220+ // GlobalTensor:全局内存张量,用来读写 GM 上的数据
221+ // 相当于一个“带偏移的指针”,知道从哪里读、读多长
222+ AscendC::GlobalTensor<float> xGm; // 输入 x
223+ AscendC::GlobalTensor<float> yGm; // 输入 y
224+ AscendC::GlobalTensor<float> zGm; // 输出 z
225+ 
226+ // 以下是 Init 阶段算出来的参数
227+ uint32_t blockLength; // 当前核负责的总元素数(2048)
228+ uint32_t tileNum; // 核内切分数(8)
229+ uint32_t tileLength; // 每次循环处理的元素数(128)
230+};
231+ 
232+// ------------------------------------------------------------
233+// div_custom:kernel 入口函数
234+// 这是 Device 侧运行的函数,8 个核各跑一份
235+// 参数:
236+// x, y, z — GM 上的数组首地址(类型是 GM_ADDR = __gm__ uint8_t*)
237+// tiling — 按值传递的 Tiling 参数
238+//
239+// 执行流程:
240+// 1. 实例化 KernelDiv 对象
241+// 2. 调用 Init 完成初始化
242+// 3. 调用 Process 进入主循环
243+// ------------------------------------------------------------
244+__global__ __aicore__ void div_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z,
245+ DivCustomTilingData tiling)
246+{
247+ KernelDiv op;
248+ op.Init(x, y, z, tiling.totalLength, tiling.tileNum);
249+ op.Process();
250+}
251+ 
252+// ------------------------------------------------------------
253+// kernel_div:Host 侧的封装函数
254+// 作用:完成 Host → Device 的数据搬运、启动 kernel、Device → Host 的结果回拷
255+//
256+// 完整流程:
257+// 1. 初始化 AscendCL 运行环境
258+// 2. 在 Device 上申请三段全局内存(x, y, z)
259+// 3. 把输入数据从 Host 拷贝到 Device
260+// 4. 组装 Tiling 参数,以 8 个核启动 kernel
261+// 5. 等待 kernel 执行完成
262+// 6. 把结果从 Device 拷回 Host
263+// 7. 释放资源,清理环境
264+// ------------------------------------------------------------
265+std::vector<float> kernel_div(std::vector<float>& x, std::vector<float>& y)
266+{
267+ // 并行参数:8 个核,每个核内部切 8 份
268+ constexpr uint32_t blockDim = 8;
269+ constexpr uint32_t tileNum = 8;
270+ const uint32_t totalLength = static_cast<uint32_t>(x.size());
271+ 
272+ // -------- step 1: 初始化 AscendCL 运行环境 --------
273+ aclInit(nullptr); // 初始化 ACL
274+ aclrtSetDevice(0); // 使用 0 号设备
275+ aclrtStream stream = nullptr;
276+ aclrtCreateStream(&stream); // 创建 Stream(任务队列)
277+ 
278+ // -------- step 2: 在 Device 上申请全局内存 --------
279+ const uint32_t dataSize = totalLength * sizeof(float); // 总字节数
280+ 
281+ // 保持 uint8_t* 类型以匹配 GM_ADDR(Device 侧是 __gm__ uint8_t*)
282+ uint8_t* xDevice = nullptr;
283+ uint8_t* yDevice = nullptr;
284+ uint8_t* zDevice = nullptr;
285+ 
286+ // aclrtMalloc 的参数是 void**,所以需要显式转型
287+ aclrtMalloc(reinterpret_cast<void**>(&xDevice), dataSize, ACL_MEM_MALLOC_HUGE_FIRST);
288+ aclrtMalloc(reinterpret_cast<void**>(&yDevice), dataSize, ACL_MEM_MALLOC_HUGE_FIRST);
289+ aclrtMalloc(reinterpret_cast<void**>(&zDevice), dataSize, ACL_MEM_MALLOC_HUGE_FIRST);
290+ 
291+ // -------- step 3: Host → Device 拷贝输入数据 --------
292+ aclrtMemcpy(xDevice, dataSize, x.data(), dataSize, ACL_MEMCPY_HOST_TO_DEVICE);
293+ aclrtMemcpy(yDevice, dataSize, y.data(), dataSize, ACL_MEMCPY_HOST_TO_DEVICE);
294+ 
295+ // -------- step 4: 组装 Tiling 参数,启动 kernel --------
296+ DivCustomTilingData tiling{totalLength, tileNum};
297+ 
298+ // <<<blockDim, nullptr, stream>>> 的含义:
299+ // blockDim = 8 — 启用 8 个 AI Core
300+ // nullptr — 不使用共享内存
301+ // stream — 在这个 Stream 上执行
302+ // 直接把 uint8_t* 传给 GM_ADDR 参数,类型天然匹配
303+ div_custom<<<blockDim, nullptr, stream>>>(xDevice, yDevice, zDevice, tiling);
304+ 
305+ // 等待 kernel 执行完成
306+ aclrtSynchronizeStream(stream);
307+ 
308+ // -------- step 5: Device → Host 拷贝结果 --------
309+ std::vector<float> z(totalLength);
310+ aclrtMemcpy(z.data(), dataSize, zDevice, dataSize, ACL_MEMCPY_DEVICE_TO_HOST);
311+ 
312+ // -------- step 6: 释放资源 --------
313+ aclrtFree(xDevice);
314+ aclrtFree(yDevice);
315+ aclrtFree(zDevice);
316+ aclrtDestroyStream(stream);
317+ aclrtResetDevice(0);
318+ aclFinalize();
319+ 
320+ return z;
321+}
322+ 
323+// ------------------------------------------------------------
324+// VerifyResult:验证函数
325+// 比较 output 和 golden 是否一致,打印前 20 个值
326+// ------------------------------------------------------------
327+uint32_t VerifyResult(std::vector<float>& output, std::vector<float>& golden)
328+{
329+ // 打印前 20 个值的 lambda 函数
330+ auto printTensor = [](std::vector<float>& tensor, const char* name) {
331+ constexpr size_t maxPrintSize = 20;
332+ std::cout << name << ": ";
333+ std::copy(tensor.begin(),
334+ tensor.begin() + std::min(tensor.size(), maxPrintSize),
335+ std::ostream_iterator<float>(std::cout, " "));
336+ if (tensor.size() > maxPrintSize) {
337+ std::cout << "...";
338+ }
339+ std::cout << std::endl;
340+ };
341+ 
342+ printTensor(output, "Output");
343+ printTensor(golden, "Golden");
344+ 
345+ // 逐元素比较
346+ if (std::equal(golden.begin(), golden.end(), output.begin())) {
347+ std::cout << "[Success] Case accuracy is verification passed." << std::endl;
348+ return 0;
349+ } else {
350+ std::cout << "[Failed] Case accuracy is verification failed!" << std::endl;
351+ return 1;
352+ }
353+}
354+ 
355+// ------------------------------------------------------------
356+// main 函数
357+// 1. 构造 16384 个 1.2 和 2.3
358+// 2. 调用 kernel_div 执行逐元素除法
359+// 3. 用 golden(1.2/2.3 = 0.521739)验证结果
360+// ------------------------------------------------------------
361+int32_t main(int32_t argc, char* argv[])
362+{
363+ constexpr uint32_t totalLength = 8 * 2048; // 16384
364+ constexpr float valueX = 1.2f;
365+ constexpr float valueY = 2.3f;
366+ 
367+ // 构造输入数据:16384 个 1.2 和 16384 个 2.3
368+ std::vector<float> x(totalLength, valueX);
369+ std::vector<float> y(totalLength, valueY);
370+ 
371+ // 调用 kernel 得到输出
372+ std::vector<float> output = kernel_div(x, y);
373+ 
374+ // 构造期望结果:16384 个 1.2/2.3 = 0.521739
375+ std::vector<float> golden(totalLength, valueX / valueY);
376+ 
377+ // 验证并返回
378+ return VerifyResult(output, golden);
379+}
@@ -0,0 +1,9 @@
1+#!/bin/bash
2+# 激活cann环境,可根据实际情况修改,在线环境一般不用修改,
3+source "$ASCEND_TOOLKIT_HOME/set_env.sh"
4+mkdir -p build
5+export ASC_DIR="$ASCEND_HOME_PATH/aarch64-linux/tikcpp/ascendc_kernel_cmake/"
6+cd build/ && \
7+cmake .. && \
8+make && \
9+./div_test