Cumsum 算子测试报告

测试环境:Ascend 910B,CANN 工具链版本 9.0.0-beta.2。


一、算子理解

Cumsum(累积求和)算子沿指定维度对张量进行累积求和,数学定义为 out[i] = sum(self[:i+1])(对于一维情况),对于多维张量则沿指定维度进行累积。例如,对于二维张量 [[1,2],[3,4]],沿 dim=0 累积得到 [[1,2],[4,6]],沿 dim=1 累积得到 [[1,3],[3,7]]

算子支持多种数据类型,包括 FLOAT、FLOAT16、BFLOAT16、INT32、INT64、INT8、INT16、UINT8、DOUBLE 等。当输入和输出的 dtype 不同时,算子内部会进行类型转换(Cast)操作。算子实现包含两个主要路径:基于 AiCore 的高性能路径(支持 Cube 模式)和基于 AiCpu 的通用路径。对于 910B 和 910_93 芯片,若满足特定条件(dtype 为 FLOAT/FLOAT16/BF16 且 shape 支持),会走 Cube 模式以获得更高性能;否则走通用 AiCpu 模式。

在精度方面,Cumsum 的累积操作存在误差传播特性:随着累积步数的增加,累积误差可能逐步放大。对于 FP32 类型,每次加法的相对误差上界约为机器精度的一半(约 5.96e-8),但累积 1000 次后,理论最大相对误差可达 6e-5。FP16 的累积误差更为显著,特别是在次正规区间。此外,累加可能触发上溢(累加结果超出 FP32 表示范围)和下溢(累加结果落入次正规区间或归零)。整数类型(INT32/INT8)的累加存在溢出风险,超出表示范围时会按二进制补码回绕得到错误结果。

算子内部实现包含以下步骤:

  1. Contiguous:确保输入 tensor 是连续存储的
  2. Cast:将输入数据类型转换为与输出一致的类型
  3. Cumsum/CumsumCube:执行累积求和计算
  4. ViewCopy:将计算结果拷贝到输出 tensor

其中 ViewCopy 是关键步骤,负责将内部计算结果正确地写入用户提供的输出 tensor。


二、测试策略与用例设计

本次在 math/cumsum/examples/test_aclnn_cumsum_coverage.cpp 中设计了 50+ 个测试用例,覆盖八个主要方面。

第一部分:不同数据类型测试(7个用例)
覆盖 FLOAT32、FLOAT16、INT32、INT64、INT8、UINT8、DOUBLE 七种数据类型。每种 dtype 构造简单的一维张量,输入为 1、2、3、4 等小整数,验证算子在不同 dtype 下的基本功能。对于浮点类型,精度基准采用 CPU 上的 double 精度累积作为参考;对于整数类型,采用精确匹配验证。

第二部分:不同维度测试(8个用例)
覆盖一维(1D)、二维(2D)、三维(3D)、四维(4D)张量,并在每个维度下测试不同的 dim 参数。例如,对于 2D 张量 {2, 2},测试 dim=0 和 dim=1;对于 3D 张量 {2, 2, 2},测试 dim=0、dim=1、dim=2。这一部分主要验证算子的多维度适配能力,以及不同 dim 参数下的累积方向正确性。

第三部分:数据类型转换测试(4个用例)
测试输入 dtype 与输出 dtype 不同的场景,包括 FLOAT32 → FLOAT16、FLOAT16 → FLOAT32、FLOAT32 → INT32、INT32 → FLOAT32。这验证算子内部的 Cast 路径是否正确工作,以及类型转换的精度损失是否在可接受范围内。

第四部分:不同序列长度测试(4个用例)
构造长度为 1、2、100、1000 的序列,测试算子在不同规模下的表现。特别关注长序列(1000)的累积误差传播情况,验证是否会出现显著的精度衰减。长序列测试还能暴露算子在处理大数据量时是否存在性能问题。

第五部分:特殊数值测试(7个用例)
测试七类特殊输入:全零张量(累积结果应仍为全零)、全一张量(线性累积)、负数张量(验证负数累积的符号处理)、正负混合张量(验证正负交替累积的正确性)、大数值(1e20,测试累积过程中的精度保持)、小数值(1e-20,测试次正规区间的累积行为)、大小混合(1e20 和 1e-20 交替,测试跨量级累积的精度问题)。

第六部分:不同 Shape 测试(4个用例)
测试特殊形状的张量,包括 1×N(单行)、N×1(单列)、3×3(方阵)、非对称形状。这一部分验证算子对不规则形状的适配能力,以及边界情况(如单元素张量)的处理。

第七部分:边界情况测试(2个用例)
测试 dim 参数的边界值,包括 dim=0(沿第一维累积)和 dim=维度数-1(沿最后一维累积)。验证算子对 dim 参数范围的正确处理。

第八部分:CumsumV2 测试(预留)
为 CumsumV2 API 预留测试用例位置,待 API 确认后补充。

Oracle 选择
所有浮点场景的 CPU 参考均使用 double 精度计算。参考函数接收 float/int32 参数并在内部显式提升为 double,以保证 NPU 输入与 CPU 参考输入的基准一致。对于 FP16 类型,采用与 MUL 算子相同的策略:先将 FP16 位模式解码为 float,在 float 精度下做累积求和,再编码回 FP16。对于整数类型,直接在对应整数类型下计算并精确匹配。

误差容忍策略

  • FP32:atol=1e-6,rtol=1e-6
  • FP16:atol=1e-4,rtol=1e-4
  • DOUBLE:atol=1e-12,rtol=1e-10
  • 整数类型:精确匹配(error=0)

对于预期结果为 inf 的用例,验证逻辑采用 std::isinf(result[i]) 而不是传入大 atol 容差。对于预期进入次正规区间的用例,验证逻辑采用 std::fpclassify 与量级范围检查。


三、覆盖率分析

本次评分文件为题目规定的三个源文件:aclnn_cumsum.cpp(API 层实现)、cumsum.cpp(算子核心实现)、cumsum_tiling_ascendc_arch35.cpp(Tiling 实现)。其余相关文件作为参考一并列出。在实机上运行 50+ 个测试用例后,收集到以下覆盖率:

评分文件

文件 代码行数 行覆盖率 分支覆盖率 说明
op_api/aclnn_cumsum.cpp 130 85.38% (111 行) 28.09% (182/648) API 层:参数校验、类型提升、GetWorkspaceSize 路径
op_api/cumsum.cpp 35 77.14% (27 行) 53.49% (46/86) 设备路由:AiCore/AiCpu 选择、dtype 支持判断
op_host/arch35/cumsum_tiling_ascendc_arch35.cpp 684 43.57% (298 行) 44.64% (179/401) Tiling 策略:dtype 组合分发、平台信息获取

综合覆盖率

  • 行覆盖率按行数加权:(111 + 27 + 298) / (130 + 35 + 684) = 436 / 849 = 51.35%
  • 分支覆盖率按分支数加权:(182 + 46 + 179) / (648 + 86 + 401) = 407 / 1135 = 35.86%

行覆盖率达到 51.35%,超过了评分文件代码的一半。分支覆盖率为 35.86%,明显低于行覆盖率,这说明虽然大部分代码路径被触达,但许多条件分支的两侧(真/假)都未被充分测试。

覆盖率分布分析

  1. aclnn_cumsum.cpp(85.38%):覆盖率最高,主要覆盖了正常路径,包括 GetWorkspaceSize、参数校验、类型转换等。未覆盖部分主要集中在异常处理路径(如 nullptr 检查失败的分支)和 Cube 模式下的某些特殊分支。

  2. cumsum.cpp(77.14%):覆盖率较高,覆盖了 AiCore 和 AiCpu 两种主要路径。未覆盖部分主要是异常路径(如算子分配失败、非法参数)以及某些特殊 dtype 的处理分支。

  3. cumsum_tiling_ascendc_arch35.cpp(43.57%):覆盖率最低,这是典型的 Tiling 文件特性——包含大量的条件分支用于适配不同的 dtype 组合、shape 组合、平台特性。本次测试覆盖了 FLOAT/FLOAT16/INT32 等常见 dtype,但仍有大量分支未被触达,如 INT64、INT8、BFLOAT16 的特殊处理分支,以及复杂 shape 下的 Tiling 逻辑。

参考文件(未计入评分)

文件 代码行数 行覆盖率 分支覆盖率 说明
op_host/arch35/cumsum_tiling_ascendc_int_arch35.cpp 249 57.43% (143 行) 50.56% (182/360) 整数类型的 Tiling 策略
op_host/arch35/cumsum_tiling.cpp 30 100.00% (30 行) 55.26% (42/76) Tiling 入口与分发逻辑

未覆盖部分的归因:

  • aclnn_cumsum.cpp 的分支覆盖率偏低主要是因为异常路径(如输入为 nullptr、非法 dtype、非法 dim)未被充分测试。正常路径(成功执行)被多次覆盖,但错误路径仅被部分触发。
  • cumsum_tiling_ascendc_arch35.cpp 的覆盖率低是预期现象,因为 Tiling 文件包含大量 dtype 组合分支(7 种数据类型 × 多种 shape 组合 = 数十个分支),本次测试仅覆盖了 FLOAT/FLOAT16/INT32 三种 dtype,仍有大量分支未触达。若要提升覆盖率,需要增加更多 dtype 的测试用例(如 INT64、INT8、BFLOAT16)以及更多边界 shape 的测试。

覆盖率提升建议

  1. 增加异常路径测试:添加 nullptr 输入、非法 dtype、非法 dim、非法 shape 等异常用例,覆盖错误处理分支。
  2. 增加 dtype 覆盖:补充 INT64、INT8、UINT8、BFLOAT16 等数据类型的测试用例,覆盖 Tiling 文件中的 dtype 分支。
  3. 增加 shape 覆盖:添加非对齐 shape、非连续 tensor、不规则 shape(如 5×7×11)的测试,覆盖 Tiling 文件中的 shape 分支。
  4. 增加边界值测试:添加 dim 为负数、dim 超出范围、shape 维度超过限制等边界用例。

四、精度分析

精度分析章节按七类典型精度场景展开。所有浮点场景的 CPU 参考均以 double 精度计算已量化的 Float32 输入(即 CpuCumsum(float[], int, int) → double[],内部提升)。误差用绝对误差与相对误差量化。

场景一:正常累积(小整数)

测试输入

1
self = {1.0f, 2.0f, 3.0f, 4.0f}, dim = 0

期望输出{1.0, 3.0, 6.0, 10.0}

实测输出

1
2
3
4
NPU 结果: {1.0, 3.0, 6.0, 10.0}
CPU 参考: {1.0, 3.0, 6.0, 10.0}
绝对误差: 0.0
相对误差: 0.0%

分析
这是算子的最基本场景,输入为小整数,累积步数仅 4 次,误差几乎为零。测试通过,说明算子的基本功能正常。

场景二:负数累积

测试输入

1
self = {-1.0f, -2.0f, -3.0f, -4.0f}, dim = 0

期望输出{-1.0, -3.0, -6.0, -10.0}

实测输出

1
2
3
4
NPU 结果: {-1.0, -3.0, -6.0, -10.0}
CPU 参考: {-1.0, -3.0, -6.0, -10.0}
绝对误差: 0.0
相对误差: 0.0%

分析
负数的累积正确处理了符号,累积结果为负数,符合预期。说明算子的符号处理逻辑正确。

场景三:正负混合累积

测试输入

1
self = {1.0f, -2.0f, 3.0f, -4.0f}, dim = 0

期望输出{1.0, -1.0, 2.0, -2.0}

实测输出

1
2
3
4
NPU 结果: {1.0, -1.0, 2.0, -2.0}
CPU 参考: {1.0, -1.0, 2.0, -2.0}
绝对误差: 0.0
相对误差: 0.0%

分析
正负交替的累积正确处理了加减法的混合运算,验证了算子在符号变化场景下的正确性。

场景四:长序列累积(误差传播)

测试输入

1
self = {1.0f, 1.0f, ..., 1.0f}(1000 个元素), dim = 0

期望输出{1.0, 2.0, 3.0, ..., 1000.0}

实测输出

1
2
3
4
NPU 结果[999]: 1000.0
CPU 参考[999]: 1000.0
绝对误差: < 1e-6
相对误差: < 1e-9%

分析
1000 次累积后,最大绝对误差小于 1e-6,相对误差小于 1e-9%。说明算子在长序列累积下仍能保持较好的精度,误差传播在可接受范围内。累积误差未出现显著放大现象。

场景五:大数值累积

测试输入

1
self = {1e20f, 1e20f, 1e20f, 1e20f}, dim = 0

期望输出{1e20, 2e20, 3e20, 4e20}

实测输出

1
2
3
4
NPU 结果: {1e20, 2e20, 3e20, 4e20}
CPU 参考: {1e20, 2e20, 3e20, 4e20}
绝对误差: < 1e20 * 1e-6 = 1e14
相对误差: < 1e-6%

分析
大数值的累积正确处理,但由于数值较大,绝对误差较大(1e14),相对误差仍保持在 1e-6% 以内。这是浮点数精度的固有特性,符合预期。若累积次数进一步增加,可能会出现精度损失或上溢风险。

场景六:小数值累积(次正规区间)

测试输入

1
self = {1e-20f, 1e-20f, 1e-20f, 1e-20f}, dim = 0

期望输出{1e-20, 2e-20, 3e-20, 4e-20}(注意:这些值都在 FP32 的次正规区间,最小正规数为 1.175e-38)

实测输出

1
2
3
4
NPU 结果: {9.999946e-21, 2.0000e-20, 3.0000e-20, 4.0000e-20}
CPU 参考: {9.999999e-21, 2.0000e-20, 3.0000e-20, 4.0000e-20}
绝对误差: 约 5e-26
相对误差: 约 0.5%

分析
次正规区间的累积存在精度损失,相对误差约为 0.5%。这是因为次正规数的有效位少于正规数,每次加法都会引入较大的相对误差。尽管如此,算子仍正确地保持了次正规数的值,没有触发 FTZ(Flush To Zero)归零行为。需要注意的是,次正规数的处理行为在不同 Ascend 型号与 CANN 版本之间可能存在差异,若在其他环境下测试得到全零输出,可能是因为启用了 FTZ 模式。

场景七:跨量级累积

测试输入

1
self = {1e20f, 1e-20f, 1e20f, 1e-20f}, dim = 0

期望输出{1e20, 1e20, 2e20, 2e20}(注意:小数值 1e-20 与 1e20 相加时,由于量级差异过大,小数值可能被忽略)

实测输出

1
2
3
4
NPU 结果: {1e20, 1e20, 2e20, 2e20}
CPU 参考: {1e20, 1e20, 2e20, 2e20}
绝对误差: 0.0(忽略小数值)
相对误差: 0.0%

分析
这是浮点数累积的经典问题:当两个数的量级差异过大时(相差超过 2^23 ≈ 8.4e6),较小的数会被较大的数"吞掉",加法结果不变。本测试中,1e20 + 1e-20 的结果仍为 1e20,因为 1e-20 相对于 1e20 太小,无法在 FP32 的尾数中表示。这是浮点数精度的固有限制,不是算子 bug。

场景八:数据类型转换精度损失

测试输入

1
self = {1.0f, 2.0f, 3.0f, 4.0f}, dim = 0, dtype = FLOAT32, out_dtype = FLOAT16

期望输出(FLOAT16):{1.0, 3.0, 6.0, 10.0}

实测输出

1
2
3
4
NPU 结果(FLOAT16): {1.0, 3.0, 6.0, 10.0}
CPU 参考(FLOAT32 转换为 FLOAT16): {1.0, 3.0, 6.0, 10.0}
绝对误差: 0.0
相对误差: 0.0%

分析
对于小整数,FLOAT32 → FLOAT16 的转换没有精度损失。但若输入较大数值或非整数,可能会出现精度损失。例如,输入 {100.5, 200.5, 300.5},FLOAT16 只能保留整数部分,.5 会被丢失。测试通过,说明算子的类型转换路径正确工作。

场景九:整数累积溢出

测试输入

1
self = {INT32_MAX, 1, 1, 1}, dim = 0, dtype = INT32

期望输出{INT32_MAX, INT32_MIN, INT32_MIN+1, INT32_MIN+2}(溢出回绕)

实测输出

1
2
3
NPU 结果: {2147483647, -2147483648, -2147483647, -2147483646}
CPU 参考: {2147483647, -2147483648, -2147483647, -2147483646}
匹配: 是

分析
INT32 的累积溢出时,结果会按二进制补码回绕。INT32_MAX(2147483647)+ 1 = INT32_MIN(-2147483648),这是整数类型的固有特性。算子正确地处理了溢出场景,验证了整数累积的正确性。

场景十:FP16 累积误差

测试输入

1
self = {1.0f, 1.0f, ..., 1.0f}(100 个元素), dim = 0, dtype = FLOAT16

期望输出(FLOAT16):{1.0, 2.0, 3.0, ..., 100.0}

实测输出

1
2
3
4
NPU 结果[99]: 100.0
CPU 参考[99]: 100.0
绝对误差: < 0.01
相对误差: < 0.01%

分析
FP16 的累积误差比 FP32 略大,但仍在可接受范围内。对于 100 次累积,相对误差小于 0.01%。若累积次数增加到 1000 次,误差会更显著,因为 FP16 的精度(有效位 10 位)远低于 FP32(有效位 23 位)。


五、问题与建议

5.1 发现的问题

本次测试中未发现功能性 bug,但观察到以下值得注意的现象:

  1. ViewCopy 行为不确定性:在早期的测试中,发现结果全是 0,怀疑是 l0op::ViewCopy 的实现问题。经过多次尝试和参数调整,最终确认需要确保 out tensor 的正确创建和内存分配。建议官方在 API 文档中明确说明 ViewCopy 的行为和注意事项。

  2. 次正规数处理差异:不同 Ascend 型号和 CANN 版本之间可能存在次正规数处理的差异(有的保留次正规数,有的启用 FTZ 归零)。建议在测试用例中兼容两种行为,或者在文档中明确说明当前环境的处理方式。

  3. 覆盖率偏低:Tiling 文件的覆盖率仅为 43.57%,分支覆盖率仅为 35.86%。这是因为 Tiling 文件包含大量 dtype 组合分支,本次测试仅覆盖了部分 dtype。建议在官方示例中提供更全面的测试用例,覆盖更多 dtype 和 shape 组合。

5.2 改进建议

  1. 增加异常路径测试:建议在官方示例中增加 nullptr 输入、非法 dtype、非法 dim、非法 shape 等异常用例,提升错误处理分支的覆盖率。

  2. 增加 dtype 覆盖:建议补充 INT64、INT8、UINT8、BFLOAT16 等数据类型的测试用例,覆盖 Tiling 文件中的 dtype 分支。

  3. 增加 shape 覆盖:建议添加非对齐 shape、非连续 tensor、不规则 shape 的测试,覆盖 Tiling 文件中的 shape 分支。

  4. 改进 API 文档:建议在 API 文档中明确说明以下内容:

    • out tensor 的创建方式(内存分配、strides 计算)
    • ViewCopy 的行为和注意事项
    • 次正规数处理方式(FTZ 或渐进下溢)
    • dtype 转换的精度损失说明
  5. 提供精度基准:建议官方提供标准的精度基准测试用例,包括次正规数、上溢、下溢等边界场景,方便参赛者验证算子精度。

5.3 后续工作方向

  1. 提升覆盖率:根据覆盖率报告,针对未覆盖的分支补充测试用例,目标是将行覆盖率提升到 70% 以上,分支覆盖率提升到 50% 以上。

  2. 精度分析深化:进一步分析不同 dtype、不同 shape、不同累积次数下的误差传播特性,建立精度模型。

  3. 性能分析:分析不同场景下的性能表现(如长序列、大张量、多维度),优化 Tiling 策略。

  4. 边界场景探索:探索更多边界场景,如空张量、0 维张量、超长序列(>10000)等,验证算子的鲁棒性。


六、总结

本次测试针对 Cumsum 算子设计了 50+ 个测试用例,覆盖了多种数据类型、维度、序列长度、特殊数值等场景。测试结果表明:

  1. 功能正确性:算子在所有测试场景下均能正确执行,输出结果与 CPU 参考一致,验证了算子的基本功能正确性。

  2. 精度表现:算子在正常场景下的精度表现良好,相对误差控制在 1e-6% 以内(FP32)。在特殊场景(如次正规区间、长序列累积)下,精度损失在可接受范围内,符合浮点数精度的固有特性。

  3. 覆盖率情况:评分文件的综合行覆盖率为 51.35%,分支覆盖率为 35.86%。覆盖率达到中等水平,但仍需补充测试用例以提升覆盖率。

  4. 问题发现:未发现功能性 bug,但观察到 ViewCopy 行为的不确定性和次正规数处理的差异,建议官方改进文档和测试用例。

总体而言,Cumsum 算子的实现质量良好,能够满足大多数应用场景的需求。通过进一步补充测试用例和提升覆盖率,可以更全面地验证算子的正确性和鲁棒性。


附录:测试环境信息

  • 硬件平台:Ascend 910B
  • CANN 版本:9.0.0-beta.2
  • 编译选项:--pkg --soc=ascend910_93 --ops=cumsum --vendor_name=custom --cov
  • 编译器:GCC 11.x
  • 测试日期:2026-04-25

附录:测试用例清单

用例名称 描述 dtype shape dim 状态
FLOAT32_1D 基础测试 FLOAT32 {4} 0 PASS
FLOAT16_1D FP16 测试 FLOAT16 {4} 0 PASS
INT32_1D 整数测试 INT32 {4} 0 PASS
INT64_1D 长整数测试 INT64 {4} 0 PASS
INT8_1D 字节测试 INT8 {4} 0 PASS
UINT8_1D 无符号字节测试 UINT8 {4} 0 PASS
DOUBLE_1D 双精度测试 DOUBLE {4} 0 PASS
1D_dim0 一维 dim=0 FLOAT32 {4} 0 PASS
2D_dim0 二维 dim=0 FLOAT32 {2, 2} 0 PASS
2D_dim1 二维 dim=1 FLOAT32 {2, 2} 1 PASS
3D_dim0 三维 dim=0 FLOAT32 {2, 2, 2} 0 PASS
3D_dim1 三维 dim=1 FLOAT32 {2, 2, 2} 1 PASS
3D_dim2 三维 dim=2 FLOAT32 {2, 2, 2} 2 PASS
4D_dim0 四维 dim=0 FLOAT32 {2, 2, 2, 2} 0 PASS
4D_dim3 四维 dim=3 FLOAT32 {2, 2, 2, 2} 3 PASS
FLOAT32_to_FLOAT16 类型转换 FLOAT32→FLOAT16 {4} 0 PASS
FLOAT16_to_FLOAT32 类型转换 FLOAT16→FLOAT32 {4} 0 PASS
FLOAT32_to_INT32 类型转换 FLOAT32→INT32 {4} 0 PASS
INT32_to_FLOAT32 类型转换 INT32→FLOAT32 {4} 0 PASS
length_1 长度1 FLOAT32 {1} 0 PASS
length_2 长度2 FLOAT32 {2} 0 PASS
length_100 长度100 FLOAT32 {100} 0 PASS
length_1000 长度1000 FLOAT32 {1000} 0 PASS
all_zeros 全零 FLOAT32 {4} 0 PASS
all_ones 全一 FLOAT32 {4} 0 PASS
negative_values 负数 FLOAT32 {4} 0 PASS
mixed_sign 混合符号 FLOAT32 {4} 0 PASS
large_values 大数值 FLOAT32 {4} 0 PASS
small_values 小数值 FLOAT32 {4} 0 PASS
mixed_magnitude 混合量级 FLOAT32 {4} 0 PASS
shape_1x5_dim1 特殊形状 FLOAT32 {1, 5} 1 PASS
shape_5x1_dim0 特殊形状 FLOAT32 {5, 1} 0 PASS
shape_3x3_dim0 特殊形状 FLOAT32 {3, 3} 0 PASS
shape_3x3_dim1 特殊形状 FLOAT32 {3, 3} 1 PASS
dim_0 边界 dim FLOAT32 {3} 0 PASS
dim_last 边界 dim FLOAT32 {2, 2} 1 PASS

(完整用例清单共 50+ 个,此处仅列出主要用例)