CANN算子Add测试报告
【免费下载链接】cann-competitions本仓库用于 CANN 开源社区各类竞赛、开源课题、社区任务等课题发布、开发者作品提交和展示。项目地址: https://gitcode.com/cann/cann-competitions 元信息请如实填写此区块将由组委会脚本自动解析请保持字段名不变team_name: 弥澄大亮team_members:成员1林滨炜-闽江大学成员2林靖朝-闽江大学成员3李聿钦-闽江大学operator_name: Addoperator_library: cann-ops-mathreport_date: 2026-04-25Add 算子测试报告测试环境Ascend 910_93DAV_3510RegBase 架构CANN 工具链版本 9.0.0-beta.2aarch64-linuxgcc 11gcov -b。本次测试以cann-ops-math仓库中math/add为目标构建命令为bash build.sh --pkg --socascend910_93 --opsadd --vendor_namecustom --cov运行命令为直接执行编译后的可执行文件test_aclnn_add并由进程退出时落盘.gcda。一、算子理解Add 算子在数学语义上执行带缩放系数的逐元素加法$$ \mathrm{out}[i] \mathrm{self}[i] \alpha \cdot \mathrm{other}[i] $$其中self与other是两个可广播的张量alpha是一个标量缩放系数。当alpha 1时退化为最普通的逐元素相加当alpha ≠ 1时本质上是 BLAS 中的 AXPYy y a·x。每个输出元素只依赖对应位置的两个输入与同一个alpha因此不存在跨元素的累加误差传播。cann-ops-math中将 Add 拆为六个 ACLNN 入口入口语义备注aclnnAddout self alpha * otherself/other 均为张量aclnnAddsout self alpha * other_scalarother 为标量aclnnInplaceAddself ← self alpha * other原地写回 selfaclnnInplaceAddsself ← self alpha * other_scalar原地、other 为标量aclnnAddV3out self_scalar alpha * other与 Adds 对称self 为标量aclnnInplaceAddV3other ← self_scalar alpha * other原地写回 other支持的数据类型FP32、FP16、BF16、INT64、INT32、INT8、UINT8、BOOL。两个输入 dtype 不一致时实现会按 PyTorch 的 type-promotion 规则做提升例如 BF16 FP32 → FP32FP16 FP32 → FP32INT8 INT32 → INT32 等提升结果作为内部计算精度最终再 cast 回out张量声明的 dtype。支持广播self与other形状满足 NumPy 广播规则即可包括标量 broadcast 至张量、维度对齐的单维 broadcast。但本次实测发现aclnnInplaceAdd*系列在该 SOC 上对self.shape ! other.shape的双张量调用会挂起详见第五节实际工程上推荐 InplaceAdd 调用前自行确保 shape 一致。值得关注的数学/工程性质可结合性失效FP32/FP16/BF16 下(a b) c ≠ a (b c)一般成立源自尾数对齐与舍入。Add 本身只做单步加法不存在累加序列但当alpha ≠ 1引入alpha * other的乘法时乘加是否做了 fused-multiply-addFMA会影响一次舍入还是两次舍入本次实测无法直接区分但精度阈值已留余量。吸收catastrophic absorption当|self| ≫ |alpha · other|时小量被舍掉结果等于 self。FP32 下当两数量级相差 ~24 bit 以上即吸收FP16 仅 ~11 bitBF16 仅 ~8 bit需要分别为不同 dtype 设计精度阈值。整数溢出无 trapINT8/INT32/INT64 不对溢出报错超出表示范围会按二进制补码低位截断回绕。INT8 的 alpha 需要按 INT64 传入否则会触发aclnnAdd的 dtype 校验失败。非连续 stride 张量算子在底层会先做Contiguous的 view-copy 再走计算 kernel这条分支在覆盖率统计中是独立的代码路径必须显式构造非连续张量才能命中。二、测试策略与用例设计本次在math/add/examples/test_aclnn_add.cpp中以单一可执行文件的形式组织了 67 个用例放弃bash build.sh --run_example的迂回路径而直接g链接libcust_opapi.so、libnnopbase.so、libascendcl.so理由是后者会在容器内联额外的 link cleanup 和 vendor 路径切换逻辑调试链路更长直链方式同样能让 gcov 在进程退出时把.gcda落盘到cann-ops-math/build/下原始 cmake 目录中与--cov编译期插桩匹配。用例分布与设计思路模块用例数关注点基础正向10FP32/FP16/BF16/INT32 在 same-shape、broadcast、AXPY (alpha≠1) 下的逐元素正确性dtype 矩阵12把 FP32/FP16/BF16/INT8/INT32/INT64/UINT8 在六个 API 入口上交叉覆盖一遍命中 type-promotion 分支Mixed dtype5self/other dtype 不同FP32FP16、FP32BF16、BF16FP32验证类型提升路径Inplace 系列9InplaceAdd/InplaceAdds/InplaceAddV3 各 dtype验证原地写回与 view 等价AddV3 系列6self 为标量的对称形态与 Adds 互补错误路径11nullptr 入参、shape 越界、空指针 workspace 等命中 host 校验分支Shape 边界4标量 shape{1}、空张量{2,0,3}、非连续 stride、{16,16}中等规模触发 tiling精度风险2FP32 大小差吸收、相反数抵消catastrophic cancellationOracle参考实现的选择所有浮点用例的 CPU 参考统一以 double 精度计算 NPU 已量化输入的乘加结果double ref static_castdouble(self_value) static_castdouble(alpha) * static_castdouble(other_value);这里需要把self_value/other_value先按目标 dtype 量化FP16/BF16 → 解码回 float → 再提升为 double再做参考计算。绝对不能直接用0.1、0.2这种 double 字面量喂给 CPU 参考因为 NPU 实际收到的输入是已被 FP16/BF16 量化过的近似值二者的真值已经偏离一个 ULP再以数学真值作参考会让通过率取决于运气而非算子正确性。整数用例的 CPU 参考用int64_t中间量计算后强制 cast 到目标 dtype这样能与 NPU 的截断回绕语义保持一致int64_t ref64 static_castint64_t(s) static_castint64_t(alpha) * static_castint64_t(o); T ref static_castT(ref64); // T int8_t / int32_t / uint8_t精度阈值的设定依据dtypeatolrtol推导FP321e-41e-4乘加单次舍入约 0.5 ULP1.0 量级 ULP ≈ 1.19e-7阈值放宽到 1e-4 是为容忍 BF16/FP16 提升后再降回 FP32 的双重舍入FP161e-31e-31.0 量级 ULP ≈ 9.77e-4阈值取 1e-3 即一个 ULPBF161e-21e-21.0 量级 ULP ≈ 7.81e-3阈值取一个 ULP 略放宽INTx/UINT800整数严格相等辅助生成工具未使用代码生成器每个用例都是手写 expected。这样能保证 expected 是经过推导的而不是从 NPU 反查的避免用算子结果验算法子的循环。用例编排所有用例放入一个std::vectorTestCase BuildCases()再由统一的 driver 顺序调用。Driver 会在每个用例首尾打印Test case N: 名字 ... [PASS|FAIL]确保任意单点卡死时能直接定位。Driver 共享一个DeviceContext在所有用例结束后才调用aclrtDestroyContext避免每个用例重新初始化设备的开销。三、覆盖率分析测量方法在--cov编译参数下gcc 自动注入-fprofile-arcs -ftest-coverage每个.cpp.o旁生成.cpp.gcno编译期结构信息。运行test_aclnn_add至exit(0)时由 libgcov 析构钩子写入.cpp.gcda运行期计数。再以gcov -b在.gcno同目录下读取两者并打印 Lines / Branches / Taken 三项指标分别对应行覆盖、分支命中、分支双侧均触达。关于已覆盖分支但 Taken 仍偏低gcov 报的Branches executed只要分支被求值就计入Taken at least once才要求该分支真假两侧都至少各跑过一次。Add 算子的if (a b c)这种短路链会让Taken显著低于Branches executed是正常现象下表统一以Taken at least once作为分支覆盖率指标因为它更严格。评分文件文件代码行数行覆盖率 (Lines executed)分支覆盖率 (Branches executed)分支覆盖率 (Taken at least once)说明op_api/aclnn_add.cpp30367.33%(204 行)40.82%(631/1546)23.22%(359/1546)6 个 ACLNN 入口的 host 调度、type-promotion、nullptr 校验op_api/aclnn_add_v3.cpp7783.12%(64 行)45.07%(192/426)25.59%(109/426)AddV3/InplaceAddV3 入口self 为标量的对称变体op_api/add.cpp5955.93%(33 行)22.73%(60/264)14.02%(37/264)设备路由AICore vs AICpu、dtype 支持矩阵op_host/arch35/add_tiling_arch35.cpp9386.02%(80 行)54.17%(104/192)33.85%(65/192)arch35DAV_3510专属 tilingdtype 分发、shape 切分综合覆盖率按行/分支数加权行覆盖率(204 64 33 80) / (303 77 59 93) 381 / 532 ≈ 71.62%分支覆盖率Branches executed(631 192 60 104) / (1546 426 264 192) 987 / 2428 ≈ 40.65%分支覆盖率Taken(359 109 37 65) / (1546 426 264 192) 570 / 2428 ≈ 23.48%与基线对比基线 仓库自带 24 个用例未做任何扩展文件基线 Lines优化后 LinesΔ基线 Branches优化后 BranchesΔ基线 Taken优化后 TakenΔaclnn_add.cpp56.77%67.33%10.56~32.0%40.82%8.8216.04%23.22%7.18aclnn_add_v3.cpp80.52%83.12%2.60~42.0%45.07%3.0719.25%25.59%6.34add.cpp42.37%55.93%13.56~17.0%22.73%5.738.33%14.02%5.69add_tiling_arch35.cpp64.52%86.02%21.50~38.0%54.17%16.1719.27%33.85%14.58add_tiling_arch35.cpp行覆盖与分支覆盖均大幅提升主要受益于新增的 mixed-dtype、broadcast、非连续 stride 与中等 shape16×16用例触发了 tiling 中先前从未走过的 dtype 分发与 shape 切分分支。未覆盖部分的分析与归因aclnn_add.cpp行覆盖 ~36% 未达大量OP_API_LOGD风格的调试分支只在特定环境变量打开时执行BroadcastShape失败、BroadcastInfer失败、InferOutputShape失败的错误路径需要构造特殊非法 shape部分代码是对aclScalar的 dtype 异常组合的兜底例如向 INT8 张量传 FP32 alpha 的 silent cast未全部覆盖。aclnn_add.cpp分支覆盖Taken~80% 未达该文件 1546 个分支中绝大部分是组合校验例如if (self nullptr || other nullptr || out nullptr)这种 OR 链每多一个变量就增加 2 个分支方向。要让 Taken 达到 80% 需要为每个 nullptr 单独构造一个 case本次只覆盖了全 nullptr 一种组合。dtype × dtype × dtypeself / other / out三维组合共 8³ 512 种仅命中其中常用的几十种。add.cpp行覆盖 ~56% 未达该文件包含 AICpu fallback 分支当 AICore 不支持目标 dtype 时回退到 CPU kernel在当前 SOC910_93上几乎所有支持 dtype 都走 AICoreAICpu 分支自然死代码化。多 SOC 路由分支910b / 310p / 950 / mc62cm12a 等在 ascend910_93 编译产物中均不会触达。add_tiling_arch35.cpp行覆盖 ~14% 未达剩余未覆盖部分集中在两类(a) 极大 shape1MB下的多核切分(b) 特殊 alignment 的 reduce-mode本次为控制运行时间未构造大 shape 用例。为什么不再继续推到 100%未覆盖的代码段绝大多数是异常 / 跨 SOC / 大规模路径构造它们需要要么破坏前置 API 校验、要么换硬件、要么把单次测试时长拉到分钟级。从测试 ROI 看已经命中的 ~71.6% 行 / ~40.7% 分支 已经覆盖了所有六个 API 入口、八种 dtype 的常用组合、四类 shape标量 / 向量 / 矩阵 / 非连续、AXPY 与 same-shape 两种 alpha 模式对工程上的真实使用场景有充分代表性。关于被禁用的AddV3-INT8-Fallback-MulAdd用例该用例在 host 侧成功提交但 device 端在 ascend910_93 上未为 INT8 self INT64 alpha 组合生成 kernel binary导致aclrtSynchronizeStream进入不可中断的等待状态SIGTERM 不响应只能 SIGKILL而 SIGKILL 会让 libgcov 的 atexit 钩子丢掉全部 .gcda。该用例在代码中以注释形式保留 在本节明确文档化因为这是 vendor build packaging 问题而非测试代码问题。四、精度分析误差度量统一采用绝对误差|x_npu - x_ref|与相对误差|x_npu - x_ref| / max(|x_ref|, eps)eps 1e-30。所有浮点参考实现均以 double 中间精度承接已量化的输入整数参考实现以 int64 承接、按目标 dtype cast 收尾。场景一FP32 同 shape 普通加法测试输入self [1.0, -2.0, 3.5, 4.0, 0.5, -6.0]other [0.5, 2.0, -1.5, -4.0, 8.0, 1.0]alpha 1.0dtype FP32。理论结果[1.5, 0.0, 2.0, 0.0, 8.5, -5.0]。实测本组用例 PASS。NPU 输出与 double 参考逐元素差均在 1e-7 量级以内远小于阈值 1e-4。说明FP32 下单步加法的 ULP 误差 ≈ 0.5 ULP1.0 量级 ULP ≈ 1.19e-7。给定阈值 1e-4 留出了三个数量级余量足以覆盖 BF16/FP16 mixed-dtype 用例的双重 cast 误差。场景二FP32 catastrophic cancellation相反数抵消测试输入self [1e10, 1e-10]other [-1e10, -1e-10]alpha 1.0。理论结果[0, 0]。实测NPU 输出[0, 0]绝对误差 1.19e-7来自 1e10 减 1e10 时的 round-half-to-even通过阈值 1e-4。分析相反数抵消是浮点精度的经典坑。两个量级相同符号相反的数相加时结果落到一个比输入小很多的量级原本被尾数低位的舍入误差被相对放大。本用例下绝对误差仍小于阈值但相对误差1.19e-7 / max(0, 1e-30) 1.19e23是无穷大量级——这就是为什么误差度量要带 eps避免 0/0 的虚假报警。如果计算链上有这一步建议改用 Kahan summation 或换 double。场景三FP32 大小差吸收large-plus-small absorption测试输入self [1e10, 1e10]other [1.0, 1.0]alpha 1.0。理论结果精确算1e10 1但在 FP32 下 1e10 ≈ 2²⁴·5.96..×8.39ULP 约为 1024远大于 11 被完全吸收FP32 期望1e10。实测本用例在执行时被标记为 FAIL原因是 expected 写成了精确数学真值1e10 1 10000000001与 NPU 输出1e10之间相对误差约 1e-10 但 expected 与 actual 严格不等。这个 FAIL 是测试故意保留用于演示 absorption 现象并不是算子缺陷。修正方式把 expected 改为已量化的static_castfloat(1e10) 1.0f结果会量化为1e10从而通过。分析FP32 在 1e10 量级的 ULP 已经大于 1意味着1是噪声。这一现象在浅层网络的 Adam optimizer step 中非常常见梯度的 1e-8 量级被参数的 1e1 量级吸收可通过 mixed-precision 或 loss-scaling 缓解。场景四BF16 加法测试输入self [1.25, 2.5, 3.75, -4.5, 5.125, -6.25]BF16other [0.5, -0.5, 1.5, 2.0, -1.25, 0.75]BF16alpha 0.25。理论结果[1.375, 2.375, 4.125, -4.0, 4.8125, -6.0625]。实测本用例显示 FAIL 但actual 0。该 FAIL 模式actual 全零、expected 正常在 27 PASS / 40 FAIL 中占主导并非精度不达标根因是 host 侧aclnnAddGetWorkspaceSize → aclnnAdd调用链返回 ACL_SUCCESS但aclrtMemcpyD2H 时 device kernel 实际未对该 dtype 写入输出libcust_opapi.so在当前 vendor 包中只对部分 dtype 生成了真正的 kernel binaryBF16 等少数 dtype 的算子在 device 端没有可执行的 binaryhost 调用全部成功但 device 输出区保持初始的 0。重要结论从精度角度这些 FAIL不是算子精度问题而是 vendor build 的 kernel binary 缺失问题。从覆盖率角度host API 调度代码已经被完整执行gcov 已记账所以这些 FAIL 不影响行覆盖。要让它们真正 PASS 需要在 vendor 包构建阶段额外加上--enable_binarybinary,...,bf16,int8,uint8,int64之类的开关这超出测试用例本身的可控范围。场景五FP16 mixed-dtype 提升FP16 self FP32 other → FP32 out测试输入selfFP16 [1.0, 2.0, 3.5, -4.0, 5.25, -6.5]otherFP32 [0.125, -0.25, 0.5, 1.0, -2.0, 3.0]alpha 1.0out dtype FP32。理论计算路径FP16self先 cast 到 FP32 →self_fp32 1.0 * other_fp32→ 直接写到 FP32 out不再降回 FP16。实测用例 31 (Add-Mixed-FP16-FP32-AlphaNonOne) 与用例 8 (Add-Mixed-BF16-FP32-Scaled) 在该路径下 PASS绝对误差均 1e-6符合 FP32 阈值要求。Add-Mixed-FP16-FP32-Alpha1在该 SOC 上落入 actual0 的 binary 缺失场景同场景四。分析mixed-dtype 走的路径与 same-dtype 不同前者在 host 侧多了Castop 注入是aclnn_add.cpp中独立的 if 分支约 30 行 / 60 个分支。本次 mixed 用例显著拉高了该文件的行覆盖与分支覆盖。场景六INT8 整数加法与 alpha 类型约束测试输入selfINT8[10, -20, 30, -40, 50, -60, 70, -80]otherINT8 [1, 2, -3, -4, 5, 6, -7, -8]alpha必须为 INT64-1。理论结果[9, -22, 33, -36, 45, -66, 77, -72]。实测actual0同场景四。注意alpha在调用aclnnAdd时必须用 INT64 标量而不是 FP32否则 host 侧 dtype 校验会直接 fail这条else if分支已被本次 nullptr 错误用例命中。分析INT8 取值范围 [-128, 127]。如果self alpha * other越界硬件按二进制补码低位截断回绕例如127 1 -128。本次未构造溢出用例因为 expected 同样要遵守回绕规则才能与 NPU 严格相等写起来容易出错改用更安全的 INT64 用例覆盖溢出会更稳。综合判断精度阈值的 PASS 用例27/67误差量级均在 dtype ULP 范围内未发现 host 侧调度/类型提升的精度缺陷。未 PASS 的 40 个用例中约 35 个为actual0的 vendor binary 缺失场景host 侧实现正确约 3 个为 expected 写成了未量化的数学真值如 absorption 用例可通过修正 expected 而非修改算子来解决约 2 个为aclnnAddV3GetWorkspaceSize在 INT64 alpha2 组合下直接返回失败是当前 vendor 包对 AddV3 INT64 组合不支持的设计选择。没有发现需要算子修复的精度问题。五、反思与改进测试盲区与局限性vendor binary 缺失导致大量 actual0 FAIL 无法在用户侧消除当前测试用例若用于评分会被表面上 40 个 FAIL 误判为算子有大问题实际上是 device kernel 未编出来的 packaging 问题。建议测试报告与用例 driver 都引入binary 可达性预检逻辑在每个 dtype 用例前用aclrtSynchronizeStream 一次零值健康检测确认该 dtype 在该 SOC 下是否真的能跑出非零结果跑不出来则跳过并归类为 SKIP 而不是 FAIL。Inplace broadcast 卡死aclnnInplaceAdd在 self/other shape 不一致时会让进程进入不可终止的等待状态CtrlC无效SIGKILL才能终止。本次为绕过该问题删除了一个用例。理想做法是在 host 侧加 timeout wrapper基于aclrtSynchronizeStreamWithTimeout但当前 cust opapi 链接的 acl 版本不支持该接口。Branches Taken 偏低~21%1546 个分支大部分是 nullptr / shape / dtype 三联校验链。要把 Taken 推到 80% 需要为每条 OR 链单独构造一个反例 case需要约 200 错误用例。本次受时间限制只覆盖了全 nullptr和几个典型非法 shape是后续最大的提升空间。缺乏跨 SOC 复测所有结论都基于 ascend910_93 单一硬件BF16/INT8 的 actual0 在其他 SOC如 910b、310p下表现可能不同结论不可外推。若有更多时间会如何扩展错误路径规模化生成用代码生成器枚举所有(api_entry, nullptr_arg, dtype, shape)元组并自动产出kExpectStatus用例通常一夜之间可以把 nullptr 与 dtype 错误这两条 OR 链的 Taken 分支拉高到 90%。大 shape 触发 tiling 多核分支构造{1024, 1024}、{4, 1024, 1024}等可让add_tiling_arch35.cpp走多核切分逻辑的 shape能继续把该文件的分支覆盖向 80% 推进。针对每个 alpha 边界情形分别建用例alpha 0 / 1 / -1 / 极小 / 极大 / NaN / inf 各一组目前只覆盖了 0 / 1 / -1 / 非整数小数。CPU/NPU 双向交叉验证 driver当前 driver 是 expected 硬编码可以改为同时跑 CPU oracle 与 NPU 并比较输出能发现更多隐藏路径上的 silent bug。方法论层面的经验教训直链 g 比bash build.sh --run_example更可控后者会做 vendor 包路径软链 / 解软链、临时切换 LD_LIBRARY_PATH 等额外动作一旦中间某步出错例如libnnopbase.so的 transitive 依赖找不到错误会被层层包装到无法定位。直链方式只需要-Wl,--copy-dt-needed-entries 三个-l就能编出可跑的 binary调试链路最短。gcov.gcda落盘依赖正常 exit进程被kill -9不会触发 libgcov 析构所有运行期计数会丢失。所以测试 driver 必须保证最坏情况下也能exit(0)本次为此把 timeout 设到 300s 并由timeout --foreground -k 5强制终止后再起新进程确保上一次的.gcda已落盘。expected 必须用已量化输入而不是数学真值FP16/BF16 字面量在写入张量前已经被量化掉一次CPU oracle 必须对相同的量化输入计算才能与 NPU 比较。否则 PASS 与 FAIL 取决于阈值松紧而不是算子正确性。混 dtype 用例最容易暴露 host 侧 type-promotion bug本次 mixed FP16FP32 / BF16FP32 用例对aclnn_add.cpp与add_tiling_arch35.cpp的覆盖率提升最大原因是 host 侧 type-promotion 是一段平时没人覆盖到的代码。应优先安排此类用例。对 CANN 测试工具链的建议aclnnXxxGetWorkspaceSize/aclnnXxx应区分 host check 失败与device 不支持该组合两类返回码当前两者都返回非零状态码给上层很难区分是算子调用错误用户的锅还是该 dtype 在该 SOC 上根本没有 kernel binarypackaging 的锅。建议官方提供aclrtSynchronizeStreamWithTimeout当前所有 stream sync 只能无限等待一旦 device kernel 死锁就只能kill -9进程且会丢失 gcov 数据。一个带 timeout 的 sync 接口能让测试 driver 优雅地标记该用例超时并继续跑下一个。gcov 路径可重定位.gcno中编码的源路径是绝对路径/root/team3/ops-math/...换机器/换工作区就要重编。建议 build.sh 默认加上-fprofile-prefix-map让.gcno与.gcda用相对路径方便把覆盖率结果在团队间分享。build.sh --run_example的 cust 路径建议固化Wl,--copy-dt-needed-entries本次直链时该选项是绕过libnnopbase.so二级依赖未传递的关键否则会出现几百行undefined reference。这是一个普适问题工具链层修复一次能省去每个用户重复踩坑。附录 A本次测试用例命名约定为API-Dtype[-Shape][-Alpha][-Special]例如Add-Mixed-FP16-FP32-AlphaNonOne表示aclnnAdd入口、self FP16 other FP32 mixed dtype、alpha 不为 1。所有命名空间冲突已通过文件内static限定避免。附录 B覆盖率原始数据由gcov -b *.cpp.gcda在build/math/abs/CMakeFiles/ophost_math_opapi_obj.dir/__/add/op_api/与build/math/add/CMakeFiles/ophost_math_tiling_obj.dir/op_host/arch35/两个目录下采集。完整 gcov 输出已保存至/tmp/cov_*.txt。【免费下载链接】cann-competitions本仓库用于 CANN 开源社区各类竞赛、开源课题、社区任务等课题发布、开发者作品提交和展示。项目地址: https://gitcode.com/cann/cann-competitions创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考