深入理解华为 CANN 的算子 UT 测试体系:从原理到实战的全流程解析

在 Ascend AI 生态中,算子开发是连接框架、编译器与硬件执行的关键环节。然而,一个算子能否真正稳定、正确地工作,并不仅只是“能编译通过”那么简单。算子运行逻辑是否符合设计?边界条件是否处理完善?不同 shape、不同 format、不同 dtype 是否都能正确执行?
这些问题决定了算子的可用性与稳定性,而回答这些问题的核心手段,就是 CANN 提供的 UT(Unit Test,单元测试)框架

本文将系统介绍 CANN 中 Ascend C 算子的 UT 测试体系,分析其设计思路、框架结构与执行流程,并通过一个完整示例展示如何编写、运行和分析算子的 UT 测试结果。

训练营简介

2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。

报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro
在这里插入图片描述

一、为什么算子必须做 UT?

在 CANN 体系中,算子承担着“接收输入 → 运算 → 输出”的基础功能,是 AI 模型在底层运行时的核心结构。相比框架层,算子级别的错误往往更隐蔽,也更容易在训练/推理过程中引发:

  • 精度偏差
  • 数值爆炸
  • 维度推导错误
  • 性能异常
  • 在边界 shape 下崩溃

尤其是在 Ascend C 中,算子通常需要在 多级存储结构、Cube 单元、向量单元、MTE 通道等各种硬件模块之间灵活调度,一旦写法稍有不慎便可能引发不可预期的问题。

因此,UT 测试的核心目标是验证算子代码本身的逻辑 correctness 与稳定性,而不是验证框架或模型
它主要解决:

  1. 输入、输出是否符合预期?
  2. 算法逻辑是否正确?
  3. 全部分支是否覆盖?
  4. 编译是否稳定?是否能跨多个 SoC 正常运行?

UT 的存在,使得算子能够在开发早期即可发现问题,避免在集成到框架后再调试,极大提高迭代效率。
在这里插入图片描述


在这里插入图片描述

二、UT 框架的设计理念:覆盖、可控、可验证

从设计上看,Ascend C 的 UT 框架具备三个特点:

1. 覆盖性(Coverage)

UT 目标是将算子的所有逻辑路径都跑一遍,因此:

  • 不同 input dtype
  • 不同 shape
  • 不同 format(ND / NZ / NC1HWC0)
  • 不同参数组合

都需要至少有一种测试覆盖。

2. 可控性(Controllability)

UT 测试在离线仿真环境中进行,不依赖任何训练框架,完全可控:

  • 输入随机可控(正态、均匀、范围限定)
  • 精度标准可控
  • 环境配置可控(Platform、SoC、仿真模式)
  • 核函数 block_dim 可控

3. 可验证(Verifiable)

通过 calc_expect_func 自动生成期望输出,并与算子运行结果进行比对,精准评估:

  • rtol 相对误差
  • atol 绝对误差
  • Max_atol 最大容忍误差

UT 可逐元素对比,达到 float16 的“千分之一精度”,float32 的“万分之一精度”。


三、UT 测试用例定义文件的结构

UT 的核心是一个 Python 脚本,例如:

test_add_custom_impl.py

该脚本主要完成三件事:

  1. 实例化 UT 测试类:AscendcOpUt
  2. 定义期望输出生成函数 calc_expect_func
  3. 通过 add_precision_case 注册每一个测试用例

一个典型模板如下:

from op_test_frame.ut.ascendc_op_ut import AscendcOpUt
from op_test_frame.common import precision_info

platforms = ["Ascend910B",]

ut_case = AscendcOpUt("add_custom")

这段代码决定了:

  • 当前 UT 绑定的算子类型为 add_custom
  • 测试跑在哪些 Ascend SoC 上

下面逐条拆解其核心组成部分。


四、calc_expect_func:UT 测试最核心的验证逻辑

在 UT 中,算子执行的结果需要与一个“理想输出”进行精度比对,而这个理想输出就是由 calc_expect_func 负责生成的。

其输入格式固定:

  • 每个参数以 dict 形式传入
  • 内部通过 tensor.get(“value”) 获取真实数据
  • 返回一个 list,列表中每个元素对应一个输出 tensor

例如:

def calc_expect_func_infer(x, y, z):
    z = x.get("value") + y.get("value")
    return [z, ]

这意味着:

  • UT 将自动根据 case 定义生成输入 x、y
  • 你的 expect 函数用 NumPy 逻辑生成期望输出
  • UT 框架将算子执行结果与 expect 进行比对

calc_expect_func 是整个 UT 测试中最关键的逻辑验证点,也是 UT 能够自动验证算子 correctness 的核心能力。


五、构建测试用例:add_precision_case 的细节与约束

add_precision_case 用于定义单个测试用例。一个完整 case 包含:

  • 输入/输出参数的定义(shape、dtype、format 等)
  • 精度标准
  • case 名称
  • calc_expect_func

示例:

ut_case.add_precision_case(platforms, {
    'params': [
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'input',
         'shape': [8, 2048], 'distribution': 'normal', 'value_range': [-10, 10]},
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'input',
         'shape': [8, 2048], 'distribution': 'normal', 'value_range': [-10, 10]},
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'output',
         'shape': [8, 2048]}
    ],
    "case_name": "add_custom_1",
    "calc_expect_func": calc_expect_func_infer,
    "precision_standard": precision_info.PrecisionStandard(0.005, 0.005)
})

其中关键约束如下:

  1. input/output 的参数数量必须一致
    例如 format 有 2 个元素,则 output 的 format 也必须有 2 个。

  2. 所有 input 的格式数量要一致
    否则 UT 无法正确生成多 format 场景的输入。

  3. shape 必须与 format 匹配
    例如 ND shape=[8,2048];
    但 NC1HWC0 必须是 5D。

  4. ori_format/ori_shape 为可选,但带参数校验装饰器时必须填写

  5. dtype、shape 的多个可能值数量必须一致

UT 框架对 case 配置要求严格,因为它需要在自动生成数据、参数校验、编译、执行、比对全过程中保持一致性。


六、完整实战:编写 add_custom 的 UT 测试脚本

下面是一个可直接运行的完整测试脚本,已经按照工程实践标准整理:

from op_test_frame.ut.ascendc_op_ut import AscendcOpUt
from op_test_frame.common import precision_info

platforms = ["Ascend910B",]

ut_case = AscendcOpUt("add_custom")

def calc_expect_func_infer(x, y, z):
    result = x.get("value") + y.get("value")
    return [result]

# 测试用例 1
ut_case.add_precision_case(platforms, {
    'params': [
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'input',
         'shape': [8, 2048], 'distribution': 'normal', 'value_range': [-10, 10]},
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'input',
         'shape': [8, 2048], 'distribution': 'normal', 'value_range': [-10, 10]},
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'output',
         'shape': [8, 2048]}
    ],
    "case_name": "add_custom_basic",
    "calc_expect_func": calc_expect_func_infer,
    "precision_standard": precision_info.PrecisionStandard(0.005, 0.005)
})

# 测试用例 2
ut_case.add_precision_case(platforms, {
    'params': [
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'input',
         'shape': [16, 1024], 'distribution': 'normal', 'value_range': [-1, 1]},
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'input',
         'shape': [16, 1024], 'distribution': 'normal', 'value_range': [-1, 1]},
        {'dtype': 'float16', 'format': 'ND', 'param_type': 'output',
         'shape': [16, 1024]}
    ],
    "case_name": "add_custom_small_range",
    "calc_expect_func": calc_expect_func_infer,
    "precision_standard": precision_info.PrecisionStandard(0.001, 0.001)
})

至此,一个算子的 UT 脚本就定义好了。


七、op_ut_run:执行 UT 测试的全流程

执行 UT 测试需要依赖 op_ut_run 工具。常见命令如下:

./op_ut_run \
 --case_files=./test_add_custom_impl.py \
 --data_path=./data \
 --simulator_data_path=./model \
 --simulator_lib_path=/usr/local/Ascend/.../simulator \
 --simulator_mode=ca \
 --soc_version=Ascend910B \
 --case_name=add_custom_basic \
 --ascendc_op_path=./add_custom.cpp \
 --block_dim=8

参数解释重点如下:

参数 说明
--case_files 指向测试脚本
--ascendc_op_path 算子实现文件 cpp
--case_name 测试脚本中定义的 case_name
--soc_version 对应硬件 SoC
--simulator_mode=pv/ca PV 为功能仿真,CA 为性能仿真
--block_dim 算子 kernel 的 block 数

执行结束后,终端会显示:

  • 编译是否成功
  • 精度比对是否通过
  • dump 文件是否生成

八、dump 文件分析与生成算子仿真流水图

UT 运行后,会在指定目录下生成完整 dump:

{model}/ca/add_custom/add_custom_pre_static_test_xxx/
    core0_cube_log.dump
    core0_hwts_log.dump
    core0_icache_log.dump
    core0_mte_log.dump
    ...

这些 dump 代表:

  • MTE 通道搬运行为
  • 向量指令执行
  • Cube 单元执行
  • 流水调度情况
  • cache miss 信息

要转换成可视化流水图,可使用:

./msopgen sim \
    -c core0 \
    -d ./xxx/ca/add_custom/... \
    -out ./output_data \
    -subc cubecore0

最终生成 dump2trace_core0.json,可通过:

chrome://tracing

打开即可查看完整算子执行流水。


九、UT 测试对算子开发的意义:不仅是 correctness 的保障

当你真正写过几个 Ascend C 算子后,会发现 UT 测试的意义远超过“检查是否能跑”。

它能帮助你:

1. 提前发现算子逻辑漏洞

避免在框架集成后花数倍时间定位问题。

2. 保证跨 Shape/Format/Dtype 的一致性

尤其是 NC1HWC0 和 NZ 场景,UT 能迅速暴露格式处理错误。

3. 稳定算子编译

编译失败往往由参数、format、shape 不一致引起,通过 UT 可快速定位。

4. 验证精度稳定性

尤其是涉及 reduce、broadcast、累加的算子。

5. 提供可视化流水图,辅助性能优化

UT dump 是算子性能优化的基础依据。

因此,UT 在 Ascend C 开发流程中并不是一个可选项,而是算子正式投入使用前必须通过的“基础质量门槛”。


结语

Ascend C 的 UT 测试体系,完整覆盖了算子从输入构造、编译、执行到结果验证的全流程,是算子稳定性和可靠性的基础手段。
理解并善用 UT,可以让算子开发过程更加高效,也能帮助开发者深入理解算子在硬件上的执行行为。

希望本文能让你对 CANN 的算子 UT 测试机制有一个系统、深入的理解,并能够将其中的方法运用到你自己的算子开发实践中。

在这里插入图片描述

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐