HarmonyOS AscendC 算子用 NPU 实现图像边缘检测什么是 AscendC 算子前面我们介绍了很多图形渲染相关的技术这篇来看看 AI 领域的东西。AscendC 是华为提供的一种 NPU神经网络处理器编程框架让你可以自己写算子在 NPU 上运行。算子是什么简单说就是一种计算操作。比如加法是一个算子乘法也是一个算子。在 AI 和图像处理领域有很多复杂的算子比如卷积、池化等。AscendC 让你可以自己实现这些算子然后在 NPU 上高效运行。这篇我们用一个实际的例子来说明图像边缘检测。边缘检测是图像处理中最基础的操作之一用 Sobel 算子来实现。边缘检测的结果可以用于物体识别、图像分割等场景。为什么要用 NPU你可能会问图像处理用 CPU 不就行了为什么要用 NPU答案是性能。NPU 是专门为 AI 和图像处理设计的硬件它有大量的计算单元可以并行处理很多数据。对于 Sobel 这种需要对每个像素做相同操作的任务NPU 的优势非常明显。举个例子CPU 就像一个很聪明的人什么都能做但一次只能做一件事NPU 就像一群不太聪明但很勤奋的人每个人只做很简单的事但大家一起做速度就快多了。环境搭建硬件要求设备类型请参考 CANN Kit 开发指南的约束与限制HarmonyOS 系统HarmonyOS 5.0.5 Release 及以上软件要求DevEco Studio 版本DevEco Studio 6.1.0 Release 及以上HarmonyOS SDK 版本HarmonyOS 6.1.0 Release SDK 及以上开发环境要求AscendC 的开发环境比较特殊需要在 Linux 上搭建Ubuntu 版本22.04 及以上仅支持 x86Python 版本3.7 到 3.10 之间GCC/G 版本7.0 及以上CMake 版本3.22.1 及以上搭建步骤安装 DevEco Studio去华为开发者官网下载安装下载 DDK Tools下载 DDK_tools_5.1.1.0并在 Linux 环境上解压下载平台插件下载需要的平台插件包解压后拷贝到ddk_external/tools/platform目录下安装工具进入ddk_external/tools/tools_ascendc目录执行安装脚本设置环境变量执行set_ascendc_env.sh设置环境变量安装 Python 依赖安装 toml、jinja2、numpy、torch 等依赖库项目结构├── build.sh // 编译入口脚本 ├── build_devices.sh // 编译devices侧交付件脚本 ├── cmake │ ├── config.cmake │ └── util // 算子工程编译所需脚本及公共编译文件存放目录 ├── CMakeLists.txt // 算子工程的CMakeLists.txt ├── CMakePresets.json // 编译配置项 ├── framework // 算子插件实现文件目录 ├── op_host // host侧实现文件 │ ├── sobel_custom_tiling.h // 算子Tiling定义文件 │ ├── sobel_custom.cpp // 算子原型注册、shape推导、信息库、tiling实现等内容文件 │ └── CMakeLists.txt ├── op_kernel // Kernel侧实现文件 │ ├── CMakeLists.txt │ ├── sobel_custom_base.h // 算子代码定义文件 │ └── sobel_custom.cpp // 算子代码实现文件 └── scripts // 自定义算子工程打包相关脚本所在目录这个项目结构和前面的图形渲染项目很不一样。主要分为两部分op_hostHost 侧代码负责算子的注册、shape 推导、Tiling 策略等op_kernelKernel 侧代码负责在 NPU 上实际执行的计算逻辑边缘检测算法原理在看代码之前先了解一下 Sobel 边缘检测的原理。处理流程整个处理流程分四步RGB 转灰度把彩色图像转成灰度图像Sobel X 方向滤波检测水平方向的边缘Sobel Y 方向滤波检测垂直方向的边缘XY 方向融合把两个方向的边缘信息合并Sobel 滤波核Sobel 算子使用 3x3 的滤波核。X 方向的滤波核是-1 0 1 -2 0 2 -1 0 1Y 方向的滤波核是-1 -2 -1 0 0 0 1 2 1简单说X 方向的滤波核会检测左右像素的差异Y 方向的滤波核会检测上下像素的差异。XY 方向融合融合的方式很简单用曼哈顿距离 G |Gx| |Gy| 来替代欧几里得距离。这样计算更快效果也差不多。第一步实现 RGB 转灰度RGB 转灰度的公式是Gray 0.299 * R 0.587 * G 0.114 * B。在 AscendC 中这个操作可以用向量指令高效完成。第二步实现 Sobel 滤波这是核心部分。Sobel 滤波需要对每个像素用它周围的 3x3 邻域和滤波核做卷积。使用 Gather 操作由于数据不是 32 字节对齐的需要用Gather操作来获取特定位置的数据。// 生成index: [2, 3, 4, ..., w - 1]AscendC::CreateVecIndex(index,int16_t(2),w-2);AscendC::Muls(index,index,int16_t(2),w-2);// index:int16-uint32AscendC::Cast(newindex,index,AscendC::RoundMode::CAST_NONE,w-2);这段代码生成一个索引数组用来获取每个像素右侧第 2 个位置的数据。CreateVecIndex创建一个连续的索引序列Muls把索引乘以 2因为每个像素有两个字节Cast把索引转成 uint32 类型。for(inti0;ih-2;i){// 将data[i][2,3,4,...,w-1]存储到tmpBuf0[0, 1, 2, ..., w-2]AscendC::Gather(tmpBuf0,data[i*w],newindex,0,w-2);AscendC::Gather(tmpBuf1,data[(i1)*w],newindex,0,w-2);AscendC::Gather(tmpBuf2,data[(i2)*w],newindex,0,w-2);对每一行用Gather获取右侧第 2 个位置的像素值。这样就得到了 Sobel 滤波核中 “j2” 位置的数据。// part1AscendC::Muls(dx[i*w],data[i*w],half(-1),w-2);AscendC::Muls(tmpBuf3,data[(i1)*w],half(-2),w-2);AscendC::Muls(tmpBuf4,data[(i2)*w],half(-1),w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf3,w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf4,w-2);计算 Sobel X 方向的第一部分左侧列的加权和。对应滤波核的 [-1, -2, -1] 这一列。// part2AscendC::Muls(tmpBuf1,tmpBuf1,half(2),w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf0,w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf1,w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf2,w-2);}计算 Sobel X 方向的第二部分右侧列的加权和。对应滤波核的 [1, 2, 1] 这一列。两部分加起来就是完整的 Sobel X 方向滤波结果。第三步实现 XY 方向融合// G |Gx| |Gy|AscendC::Abs(dx,dx,totalSize);AscendC::Abs(dy,dy,totalSize);AscendC::Add(result,dx,dy,totalSize);先对 Gx 和 Gy 取绝对值然后相加。这就是曼哈顿距离融合。第四步归一化// 归一化为[0,255]AscendC::Cast(output,result,AscendC::RoundMode::CAST_NONE,totalSize);把 half 类型的结果转成 uint8 类型输出范围 [0, 255]。Vector 编程范式AscendC 使用 Vector 编程范式把算子的实现分为三个基本任务CopyIn把输入数据从 Global Memory 搬运到 Local MemoryCompute在 Local Memory 上进行计算CopyOut把计算结果从 Local Memory 搬运回 Global Memory__aicore__inlinevoidProcess(){cntHSobelCustom::CeilDiv(this-H,h);cntWSobelCustom::CeilDiv(this-W,w);for(int32_ti0;icntH;i){for(int32_tj0;jcntW;j){CopyIn(i,j);Compute(i,j);CopyOut(i,j);}}}整个处理过程是把图像分成很多小块tile对每个小块依次执行 CopyIn、Compute、CopyOut。内存管理AscendC 使用 TQue 和 TBuf 来管理内存TQue队列用于 CopyIn 和 CopyOut 的数据传输TBuf缓冲区用于计算过程中的临时数据constexprint32_tBUFFER_NUM2;constuint32_th9;constuint32_tw256;pipe.InitBuffer(inQueueX,BUFFER_NUM,tileLength*sizeof(T));pipe.InitBuffer(outQueueY,BUFFER_NUM,tileLength*sizeof(T));这里使用 double bufferBUFFER_NUM 2可以在计算当前块的同时搬运下一块的数据提高效率。Tiling 策略Tiling 是把大任务分成小块的策略。每个 tile 的大小是 h x w其中w 必须是 32 的倍数对齐约束h * w * 34 w * 12 120KBUB 大小限制选 h9, w256 是一个合理的值。数据搬运__aicore__inlinevoidCopyIn(int32_ti,int32_tj){LocalTensorTxLocalinQueueX.AllocTensorT();DataCopyExtParams dataCopyParams;// ... 设置参数DataCopyPadExtParamsTpadParams(false,0,0,0);DataCopyPad(xLocal,xGm[offset],dataCopyParams,padParams);inQueueX.EnQue(xLocal);}DataCopyPad是一个支持非对齐搬运的接口。因为图像的宽度不一定是 32 的倍数所以需要用这个接口来处理边界情况。dataCopyParams设置了搬运的参数blockCount搬运多少行blockLen每行搬运多少字节srcStride源地址的行间距dstStride目标地址的行间距编译和运行创建算子工程msopgen gen-i./SobelCustom.json-cai_core-kirin9020-fONNX-out./SobelCustom用msopgen工具根据配置文件自动创建算子工程。编译算子cdSobelCustom/SobelCustom ./build.sh编译成功后会显示 “Install the project…Build and install success”。调试算子# NPU 仿真调试ascendebug kernel--backendsimulator --repo-type customize --json-file../ascendebug/temp.json --core-type AiCore --chip-version kirin9020 --work-dir../ascendebug/myWorkDir/ --block-num1--rel-err-thd0.5# NPU 实际调试ascendebug kernel--backendnpu --repo-type customize --json-file../ascendebug/temp.json --core-type AiCore --chip-version kirin9020 --work-dir../ascendebug/myWorkDir/ --block-num1--rel-err-thd0.5可以先在仿真器上调试再在实际 NPU 上调试。调试成功后会生成.omc文件这就是编译好的算子。集成到应用把生成的.omc文件放到应用的resources/rawfile目录下就可以在应用中调用了。AscendC 的优势性能在 NPU 上运行比 CPU 快很多可复用端侧和云侧的算子代码可以复用可调试提供了仿真器和实际 NPU 两种调试方式标准化使用统一的编程范式学习成本低适用场景AscendC 算子开发适合以下场景图像处理边缘检测、滤波、变换等AI 推理自定义的神经网络层信号处理FFT、滤波器等科学计算矩阵运算、数值计算等注意事项环境搭建AscendC 的开发环境比较复杂需要在 Linux 上搭建对齐约束数据宽度必须是 32 的倍数否则需要特殊处理内存管理UB 大小有限120KB要合理规划 Tiling 策略调试工具建议先用仿真器调试再在实际 NPU 上调试平台插件不同芯片需要不同的平台插件要确保下载正确核心流程图Sobel 边缘检测算法的处理流程渲染错误:Mermaid 渲染失败: Parse error on line 7: ... -- G[XY 方向融合: G |Gx| |Gy|] F - -----------------------^ Expecting SQE, TAGEND, UNICODE_TEXT, TEXT, TAGSTART, got PIPEAscendC 算子开发的完整流程否是搭建 Linux 开发环境下载 DDK Tools 和平台插件使用 msopgen 创建算子工程实现 Host 侧: 算子注册与 Tiling 策略实现 Kernel 侧: CopyIn / Compute / CopyOut配置 Tiling 参数编译算子工程仿真器调试调试是否通过?修复问题实际 NPU 调试生成 .omc 文件集成到应用总结AscendC 是一个强大的 NPU 编程框架让你可以自己实现算子在 NPU 上运行。核心流程搭建开发环境Linux DDK Tools设计算法RGB 转灰度、Sobel 滤波、XY 融合实现算子CopyIn、Compute、CopyOut配置 Tiling 策略编译、调试、集成如果你的应用需要高性能的图像处理或 AI 推理AscendC 是一个值得学习的技术。
鸿蒙开发--CANNKit-AscendC-sobel
HarmonyOS AscendC 算子用 NPU 实现图像边缘检测什么是 AscendC 算子前面我们介绍了很多图形渲染相关的技术这篇来看看 AI 领域的东西。AscendC 是华为提供的一种 NPU神经网络处理器编程框架让你可以自己写算子在 NPU 上运行。算子是什么简单说就是一种计算操作。比如加法是一个算子乘法也是一个算子。在 AI 和图像处理领域有很多复杂的算子比如卷积、池化等。AscendC 让你可以自己实现这些算子然后在 NPU 上高效运行。这篇我们用一个实际的例子来说明图像边缘检测。边缘检测是图像处理中最基础的操作之一用 Sobel 算子来实现。边缘检测的结果可以用于物体识别、图像分割等场景。为什么要用 NPU你可能会问图像处理用 CPU 不就行了为什么要用 NPU答案是性能。NPU 是专门为 AI 和图像处理设计的硬件它有大量的计算单元可以并行处理很多数据。对于 Sobel 这种需要对每个像素做相同操作的任务NPU 的优势非常明显。举个例子CPU 就像一个很聪明的人什么都能做但一次只能做一件事NPU 就像一群不太聪明但很勤奋的人每个人只做很简单的事但大家一起做速度就快多了。环境搭建硬件要求设备类型请参考 CANN Kit 开发指南的约束与限制HarmonyOS 系统HarmonyOS 5.0.5 Release 及以上软件要求DevEco Studio 版本DevEco Studio 6.1.0 Release 及以上HarmonyOS SDK 版本HarmonyOS 6.1.0 Release SDK 及以上开发环境要求AscendC 的开发环境比较特殊需要在 Linux 上搭建Ubuntu 版本22.04 及以上仅支持 x86Python 版本3.7 到 3.10 之间GCC/G 版本7.0 及以上CMake 版本3.22.1 及以上搭建步骤安装 DevEco Studio去华为开发者官网下载安装下载 DDK Tools下载 DDK_tools_5.1.1.0并在 Linux 环境上解压下载平台插件下载需要的平台插件包解压后拷贝到ddk_external/tools/platform目录下安装工具进入ddk_external/tools/tools_ascendc目录执行安装脚本设置环境变量执行set_ascendc_env.sh设置环境变量安装 Python 依赖安装 toml、jinja2、numpy、torch 等依赖库项目结构├── build.sh // 编译入口脚本 ├── build_devices.sh // 编译devices侧交付件脚本 ├── cmake │ ├── config.cmake │ └── util // 算子工程编译所需脚本及公共编译文件存放目录 ├── CMakeLists.txt // 算子工程的CMakeLists.txt ├── CMakePresets.json // 编译配置项 ├── framework // 算子插件实现文件目录 ├── op_host // host侧实现文件 │ ├── sobel_custom_tiling.h // 算子Tiling定义文件 │ ├── sobel_custom.cpp // 算子原型注册、shape推导、信息库、tiling实现等内容文件 │ └── CMakeLists.txt ├── op_kernel // Kernel侧实现文件 │ ├── CMakeLists.txt │ ├── sobel_custom_base.h // 算子代码定义文件 │ └── sobel_custom.cpp // 算子代码实现文件 └── scripts // 自定义算子工程打包相关脚本所在目录这个项目结构和前面的图形渲染项目很不一样。主要分为两部分op_hostHost 侧代码负责算子的注册、shape 推导、Tiling 策略等op_kernelKernel 侧代码负责在 NPU 上实际执行的计算逻辑边缘检测算法原理在看代码之前先了解一下 Sobel 边缘检测的原理。处理流程整个处理流程分四步RGB 转灰度把彩色图像转成灰度图像Sobel X 方向滤波检测水平方向的边缘Sobel Y 方向滤波检测垂直方向的边缘XY 方向融合把两个方向的边缘信息合并Sobel 滤波核Sobel 算子使用 3x3 的滤波核。X 方向的滤波核是-1 0 1 -2 0 2 -1 0 1Y 方向的滤波核是-1 -2 -1 0 0 0 1 2 1简单说X 方向的滤波核会检测左右像素的差异Y 方向的滤波核会检测上下像素的差异。XY 方向融合融合的方式很简单用曼哈顿距离 G |Gx| |Gy| 来替代欧几里得距离。这样计算更快效果也差不多。第一步实现 RGB 转灰度RGB 转灰度的公式是Gray 0.299 * R 0.587 * G 0.114 * B。在 AscendC 中这个操作可以用向量指令高效完成。第二步实现 Sobel 滤波这是核心部分。Sobel 滤波需要对每个像素用它周围的 3x3 邻域和滤波核做卷积。使用 Gather 操作由于数据不是 32 字节对齐的需要用Gather操作来获取特定位置的数据。// 生成index: [2, 3, 4, ..., w - 1]AscendC::CreateVecIndex(index,int16_t(2),w-2);AscendC::Muls(index,index,int16_t(2),w-2);// index:int16-uint32AscendC::Cast(newindex,index,AscendC::RoundMode::CAST_NONE,w-2);这段代码生成一个索引数组用来获取每个像素右侧第 2 个位置的数据。CreateVecIndex创建一个连续的索引序列Muls把索引乘以 2因为每个像素有两个字节Cast把索引转成 uint32 类型。for(inti0;ih-2;i){// 将data[i][2,3,4,...,w-1]存储到tmpBuf0[0, 1, 2, ..., w-2]AscendC::Gather(tmpBuf0,data[i*w],newindex,0,w-2);AscendC::Gather(tmpBuf1,data[(i1)*w],newindex,0,w-2);AscendC::Gather(tmpBuf2,data[(i2)*w],newindex,0,w-2);对每一行用Gather获取右侧第 2 个位置的像素值。这样就得到了 Sobel 滤波核中 “j2” 位置的数据。// part1AscendC::Muls(dx[i*w],data[i*w],half(-1),w-2);AscendC::Muls(tmpBuf3,data[(i1)*w],half(-2),w-2);AscendC::Muls(tmpBuf4,data[(i2)*w],half(-1),w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf3,w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf4,w-2);计算 Sobel X 方向的第一部分左侧列的加权和。对应滤波核的 [-1, -2, -1] 这一列。// part2AscendC::Muls(tmpBuf1,tmpBuf1,half(2),w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf0,w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf1,w-2);AscendC::Add(dx[i*w],dx[i*w],tmpBuf2,w-2);}计算 Sobel X 方向的第二部分右侧列的加权和。对应滤波核的 [1, 2, 1] 这一列。两部分加起来就是完整的 Sobel X 方向滤波结果。第三步实现 XY 方向融合// G |Gx| |Gy|AscendC::Abs(dx,dx,totalSize);AscendC::Abs(dy,dy,totalSize);AscendC::Add(result,dx,dy,totalSize);先对 Gx 和 Gy 取绝对值然后相加。这就是曼哈顿距离融合。第四步归一化// 归一化为[0,255]AscendC::Cast(output,result,AscendC::RoundMode::CAST_NONE,totalSize);把 half 类型的结果转成 uint8 类型输出范围 [0, 255]。Vector 编程范式AscendC 使用 Vector 编程范式把算子的实现分为三个基本任务CopyIn把输入数据从 Global Memory 搬运到 Local MemoryCompute在 Local Memory 上进行计算CopyOut把计算结果从 Local Memory 搬运回 Global Memory__aicore__inlinevoidProcess(){cntHSobelCustom::CeilDiv(this-H,h);cntWSobelCustom::CeilDiv(this-W,w);for(int32_ti0;icntH;i){for(int32_tj0;jcntW;j){CopyIn(i,j);Compute(i,j);CopyOut(i,j);}}}整个处理过程是把图像分成很多小块tile对每个小块依次执行 CopyIn、Compute、CopyOut。内存管理AscendC 使用 TQue 和 TBuf 来管理内存TQue队列用于 CopyIn 和 CopyOut 的数据传输TBuf缓冲区用于计算过程中的临时数据constexprint32_tBUFFER_NUM2;constuint32_th9;constuint32_tw256;pipe.InitBuffer(inQueueX,BUFFER_NUM,tileLength*sizeof(T));pipe.InitBuffer(outQueueY,BUFFER_NUM,tileLength*sizeof(T));这里使用 double bufferBUFFER_NUM 2可以在计算当前块的同时搬运下一块的数据提高效率。Tiling 策略Tiling 是把大任务分成小块的策略。每个 tile 的大小是 h x w其中w 必须是 32 的倍数对齐约束h * w * 34 w * 12 120KBUB 大小限制选 h9, w256 是一个合理的值。数据搬运__aicore__inlinevoidCopyIn(int32_ti,int32_tj){LocalTensorTxLocalinQueueX.AllocTensorT();DataCopyExtParams dataCopyParams;// ... 设置参数DataCopyPadExtParamsTpadParams(false,0,0,0);DataCopyPad(xLocal,xGm[offset],dataCopyParams,padParams);inQueueX.EnQue(xLocal);}DataCopyPad是一个支持非对齐搬运的接口。因为图像的宽度不一定是 32 的倍数所以需要用这个接口来处理边界情况。dataCopyParams设置了搬运的参数blockCount搬运多少行blockLen每行搬运多少字节srcStride源地址的行间距dstStride目标地址的行间距编译和运行创建算子工程msopgen gen-i./SobelCustom.json-cai_core-kirin9020-fONNX-out./SobelCustom用msopgen工具根据配置文件自动创建算子工程。编译算子cdSobelCustom/SobelCustom ./build.sh编译成功后会显示 “Install the project…Build and install success”。调试算子# NPU 仿真调试ascendebug kernel--backendsimulator --repo-type customize --json-file../ascendebug/temp.json --core-type AiCore --chip-version kirin9020 --work-dir../ascendebug/myWorkDir/ --block-num1--rel-err-thd0.5# NPU 实际调试ascendebug kernel--backendnpu --repo-type customize --json-file../ascendebug/temp.json --core-type AiCore --chip-version kirin9020 --work-dir../ascendebug/myWorkDir/ --block-num1--rel-err-thd0.5可以先在仿真器上调试再在实际 NPU 上调试。调试成功后会生成.omc文件这就是编译好的算子。集成到应用把生成的.omc文件放到应用的resources/rawfile目录下就可以在应用中调用了。AscendC 的优势性能在 NPU 上运行比 CPU 快很多可复用端侧和云侧的算子代码可以复用可调试提供了仿真器和实际 NPU 两种调试方式标准化使用统一的编程范式学习成本低适用场景AscendC 算子开发适合以下场景图像处理边缘检测、滤波、变换等AI 推理自定义的神经网络层信号处理FFT、滤波器等科学计算矩阵运算、数值计算等注意事项环境搭建AscendC 的开发环境比较复杂需要在 Linux 上搭建对齐约束数据宽度必须是 32 的倍数否则需要特殊处理内存管理UB 大小有限120KB要合理规划 Tiling 策略调试工具建议先用仿真器调试再在实际 NPU 上调试平台插件不同芯片需要不同的平台插件要确保下载正确核心流程图Sobel 边缘检测算法的处理流程渲染错误:Mermaid 渲染失败: Parse error on line 7: ... -- G[XY 方向融合: G |Gx| |Gy|] F - -----------------------^ Expecting SQE, TAGEND, UNICODE_TEXT, TEXT, TAGSTART, got PIPEAscendC 算子开发的完整流程否是搭建 Linux 开发环境下载 DDK Tools 和平台插件使用 msopgen 创建算子工程实现 Host 侧: 算子注册与 Tiling 策略实现 Kernel 侧: CopyIn / Compute / CopyOut配置 Tiling 参数编译算子工程仿真器调试调试是否通过?修复问题实际 NPU 调试生成 .omc 文件集成到应用总结AscendC 是一个强大的 NPU 编程框架让你可以自己实现算子在 NPU 上运行。核心流程搭建开发环境Linux DDK Tools设计算法RGB 转灰度、Sobel 滤波、XY 融合实现算子CopyIn、Compute、CopyOut配置 Tiling 策略编译、调试、集成如果你的应用需要高性能的图像处理或 AI 推理AscendC 是一个值得学习的技术。