飞桨 x 昇腾生态适配方案:05_ 算子适配流程
适配代码仓介绍
Paddle 针对除 CPU 和 Nvidia GPU 之外的其他硬件的适配代码,均存于PaddleCustomDevice代码仓
以 NPU 适配代码为例,其路径为
PaddleCustomDevice/backends/npu
。在此路径下,有两个目录值得重点关注,分别是 kernels 目录(主要用于算子适配)和 tests 目录(主要用于单元测试)。

确认算子原型
CANN 包中有的算子才可以适配,根据 Paddle 的 API 确认与算子的对应关系是适配的第一步,下边是一些必须的参考资料地址。
Paddle API 地址:API 文档-API文档-PaddlePaddle深度学习平台
aclnn算子地址(优先使用)
aclop算子地址(实在找不到 aclnn 时使用,可直接下载)
op-plugin适配参考(torch 适配代码,可供参考)
适配流程
适配主要分为以下几个步骤:
算子注册
在 PaddleCustomDevice/backends/npu/kernels
路径下,每个 .cc 文件均包含一个或多个算子的适配代码。首先,需要查找待适配的算子是否已经完成注册。若未注册,则需添加注册步骤;若已注册,则要检查该算子所支持的数据类型,此过程可参考 Paddle-cpu或 Paddle GPU 的实现方式。下面以简单的 abs_kernel 为例,该代码文件的最下方为算子注册部分。

上图注册代码的前四个入参位置固定,具体含义如下:
"abs":API 名称
"npu":硬件名称
"ALL_LAYOUT":支持的数据布局(Layout)类型,默认传入 ALL_LAYOUT 即可
"AbsKernel":适配函数名称
注意:
后续入参为该 API 支持的数据类型,顺序不限。数据类型的传递应主要依据 API 的需求来确定。若算子不支持某些数据类型,可选择在内部算子前后添加类型转换(CAST)操作。
注册代码的主体部分主要用于设置输入张量第一个值的数据类型。通常情况下,此部分无需过多关注,可将其置空。
适配函数入参
适配函数部分,其命名通常为 “Kernel 名(首字母大写)+ Kernel”。入参需与 Paddle CPU 保持一致,但 NPU 自定义算子除外,此类算子代码位于 /npu/custom_op
目录下,不在本文介绍范围内。仍以 abs_kernel 为例,代码中上方为 CPU 实现写法,下方为 NPU 实现写法。需注意,入参的作用域可能存在差异,必要时需添加 phi::
前缀进行对应修改。CPU:

NPU:

适配函数主体
适配函数的底层逻辑在于使 Paddle 的 API 参数与 CANN 中算子的参数达成一致。常见的无法对的齐情况主要有以下几种:
Paddle 存在参数缺失或参数无法直接对应;
CANN 存在参数缺失;
部分输入数据类型不被支持;
CANN 中的算子仅支持 NCHW 布局;
需要借助多个 CANN 算子拼接来实现 Paddle 算子的功能等。适配主体中针对参数差异的处理方式,将在
aclnn 算子适配举例章节
展开讨论。
本章节主要聚焦于介绍适配框架,以及部分算子在 Paddle 和 CANN 中的参数、语义、数据类型等情况。部分算子在 Paddle 和 CANN 中的参数及语义、数据类型完全相同,在进行 aclnn 算子适配时,仅需完成基本的三个步骤。下面以 abs_kernel 为例,对 aclnn 算子和 aclop 算子进行说明:
aclnn 算子适配主体
如果适配的算子为 aclnn,则一般包含三个部分:

算子检查宏
DO_COMPATIBILITY
宏用于进行算子检查。由于 aclnn 算子是逐步增加的,在不同的 CANN 版本中支持情况存在差异,因此若没有对应的 aclnn 算子,则使用 aclop 算子。添加该宏的目的在于让 Paddle - NPU 能够兼容更多的 CANN 版本。
适配主体
适配主体部分主要负责参数对齐。其中,必须包含的代码是为输出张量申请内存,即 dev_ctx.template Alloc<T>(out);
。
算子执行宏
EXEC_NPU_CMD
是用于执行 aclnn 算子的宏。其前两个参数默认为 aclnn 算子名称和设备上下文,后续参数为 aclnn 算子的输入参数,使用时需严格保证参数顺序对齐。
aclop 算子适配主体
部分 API 不存在对应的 aclnn 算子,此时可查阅 aclop 文档进行适配。在 Paddle 中,aclop 算子的覆盖范围更广,而 aclnn 算子的覆盖范围相对较窄。因此,当找不到 aclnn 算子时,可调用 aclop 算子进行计算。需要注意的是,aclnn 算子和 aclop 算子的适配方式存在一定差异。在执行 aclop 算子时,需使用类似如下的语句:

NpuOpRunner 的入参通常分为四个部分:
"Abs":算子名称,标识相关操作。
"{x}":输入,按顺序排列的输入数据。
"{*out}":输出,按顺序排列的输出数据。
"{}":属性,按顺序排列的属性,需以键值对形式书写。

aclop 还有一种入参方式,与上述方式等价,但更为直观:

单元测试
完成适配后,需执行重新编译操作,指令为:
编译完成后,进行单元精度验证。此环节主要使用 /npu/tests/unittests
目录下的单元测试。若为新适配的算子,还需增加相应单元测试。
注意:由于 bf16 数据类型在 numpy 中不存在,无法作为精度验证的标杆。因此,针对 bf16 数据类型,采用前后修改数据类型的方式,相关代码另行编写在*_eager.py
文件中。单元测试本身相对简单,重点在于尽可能覆盖更多的应用场景。

运行单元测试时,将以下两个 Python 路径添加至环境变量中,其中{codepath}
代表代码路径:
其余注意事项
在实际的算子适配工作中,常常会遭遇 CANN 包中算子不支持的问题。针对这一情况,最常用的解决方法是利用小算子进行拼接。在极个别场景下,对于某些算子,可将部分功能置于 CPU 上实现,尤其是与随机数相关的功能。然而,不可忽视的是,这些应对方法均会不可避免地导致性能损失。
算子适配,本质上是一项对齐工作,涵盖功能对齐、精度对齐以及性能提升。在算子适配过程中,可以借助单元测试(单测)进行跟踪。一旦出现问题,通过打印 CANN 日志的方式,能够有效地定位问题所在 。
使用上述方式可以把 CANN 的日志重定向到 xxx.log 中,通过搜索ERROR
查看错误信息是定位问题的一个关键手段。
评论