| name | external-gitcode-ascend-ascendc-operator-precision-debug |
| description | AscendC 算子精度问题调试与根因定位。当算子精度测试失败(allclose 不通过、结果偏差、输出全零/NaN 等)时使用。流程:误差分布分析 → 代码易错点审查 → 实验隔离 → printf/DumpTensor 插桩 → 修复验证。关键词:精度调试、精度问题、结果不一致、误差定位、allclose 失败、输出偏差、NaN、全零、precision debug。 |
| original-name | ascendc-operator-precision-debug |
| synced-from | https://gitcode.com/Ascend/agent-skills |
| synced-date | 2026-05-26 |
| synced-commit | 1f7666e7768a0ceb21bb1d40ce4b5179fcb6f1d6 |
| license | UNKNOWN |
AscendC 算子精度调试
按「由浅入深」五阶段定位根因:先看数据分布,再查代码易错点,然后实验隔离,最后插桩定位。
Phase 1: 误差分析 → Phase 2: 代码审查 → Phase 3: 实验隔离 → Phase 4: 插桩定位 → Phase 5: 修复验证
Phase 1:误差分析
原则:先看数据,再看代码。 先搞清楚「错在哪、错多少、错成什么样」。
收集失败用例的 shape、dtype、MaxAbsErr/MeanAbsErr/CosineSim,然后基于 scripts/debug_precision_template.py 创建 csrc/ops/<op_name>/test/debug_<op_name>_precision.py(替换占位符后运行),自动分析:
- 误差统计:MaxAbsErr、MeanAbsErr、MaxRelErr
- 首个错误元素:多维坐标 + 线性下标 + NPU 值 vs 参考值
- 错误分布:错误元素数量/占比、错误间隔是否呈周期性
- 特殊值:输出是否全零、含 NaN/Inf
- 自动对照:固定输入 vs 随机输入、缩小 shape 二分
误差特征 → 初步判断
| 现象 | 最可能原因 | 下一步 |
|---|
| FP16 失败,FP32 通过 | 未升精度到 FP32 计算 | Phase 2 查 Cast |
| 输出全零 | CopyOut 未执行 / GM 偏移错 | Phase 2 查 CopyOut |
| 输出含 NaN/Inf | 除零 / log 负数 / 溢出 | Phase 2 查 Compute |
| 全部偏差,CosineSim≈1 | 系统性精度损失 | Phase 2 查升精度 |
| 周期性/条纹状错误 | tile 边界 / 搬运偏移 | Phase 3 实验 |
| 仅尾部元素错 | 尾 tile 长度 / 对齐 | Phase 2 查尾 tile |
| 多次运行结果不同 | 异步同步不足 | Phase 3 实验 B |
| 小 shape 过、大 shape 挂 | 多核/tiling 边界 | Phase 3 实验 A |
| 固定输入过、随机挂 | 地址/stride/偏移错 | Phase 3 实验 C |
Phase 2:代码审查
MANDATORY:读取 op_host/<op_name>.cpp、op_kernel/<op_name>.cpp、design.md(若存在),按以下清单由浅入深排查。
第一层:基本正确性(最高频)
第二层:搬运与对齐
第三层:Tiling 与多核
第四层:API 陷阱
第五层:边界情况
检查点:输出审查报告——疑似问题列表(按可能性排序)。若已锁定根因,跳到 Phase 5;否则进入 Phase 3。
Phase 3:实验隔离
Phase 2 无法直接锁定根因时,通过控制变量实验缩小范围。每次只改一个变量。
实验 A:block_dim → 1(多核隔离)
在 op_host 临时硬编码 blockDim = 1,重编译测试。可配合缩小 shape。
| 结果 | 结论 |
|---|
| 单核过、多核挂 | 核间问题:GM 区间重叠 / tiling 映射 / 核间同步 |
| 单核也挂 | 非多核问题 → 实验 B |
实验 B:PipeBarrier<PIPE_ALL>(同步隔离)
将 kernel Process 中所有同步临时替换为 AscendC::PipeBarrier<PIPE_ALL>()(CopyIn / Compute / CopyOut 之间各加一个)。
| 结果 | 结论 |
|---|
| 全屏障后过 | 核内同步不足 → 逐步恢复细粒度同步定位 |
| 仍失败 | 非同步问题 → 实验 C |
PIPE_ALL 仅用于实验隔离,绝不可作为最终方案。
实验 C:固定/规律输入(地址隔离)
分别用全 1、等差序列(torch.arange)、随机输入测试。
| 结果 | 结论 |
|---|
| 全 1 过、等差/随机挂 | 地址/偏移/stride 错误(常数输入掩盖了偏移问题) |
| 全都挂 | 计算逻辑或全局 tiling 错误 |
| 全都过 | 特定数值范围触发精度问题 → 查边界值/极值 |
实验 D:缩小 shape(边界隔离)
shape=(32,) → (tileLength,) → (tileLength*2,) → 原始 shape,定位恰好开始失败的分界点,反推 tile/核边界。
首错下标 + tiling 反推
首错线性下标 → 第几个 tile → 哪个核 → 该核 GM 起始偏移 → 搬运预期字节数
周期 = tileLength → 搬运/偏移问题;周期 = 向量宽度 → 计算流程问题;与核边界对齐 → 多核/offset 问题。
Phase 4:插桩定位
问题范围已收敛到某阶段/某 tile 后,用 AscendC::printf 和 AscendC::DumpTensor 精确定位。
核心规则
- 仅 0 核打印:每个核计算逻辑一致时,加
if (AscendC::GetBlockIdx() == 0) 减少输出量。
- 同步后再读:在
DeQue / PipeBarrier 之后才能读 LocalTensor,否则读到未完成搬运的脏数据。
- FP16 先转 float:
AscendC::printf("v=%.6f\n", static_cast<float>(tensor.GetValue(idx)));,直接打 half 会乱码。
- 用 desc 区分阶段:DumpTensor 的 desc 参数(0=CopyIn 后, 1=Compute 中间, 2=CopyOut 前)。
- 小量起步:DumpTensor 的 dumpSize 从小值开始,过大会导致缓冲满或截断。
printf vs DumpTensor 选择
| 场景 | 工具 |
|---|
| 标量、分支判断、单个下标 | AscendC::printf |
| 连续一段 tensor 快速扫 | AscendC::DumpTensor(tensor, desc, dumpSize) |
| 全量逐元素对比 | 不在 kernel 内做 — Host 读 GM + Python 脚本 |
插桩策略
在 Compute 函数内 DeQue 之后,逐步骤插桩,与 Python 侧用相同输入手算的中间结果逐步对比。第一个出现偏差的步骤即为根因所在。
if (AscendC::GetBlockIdx() == 0 && progress == 0) {
AscendC::printf("[step1] tmp[0]=%.6f\n", static_cast<float>(tmp.GetValue(0)));
}
Phase 5:修复验证
常见修复模式
| 根因 | 修复 |
|---|
| FP16 未升精度 | 添加 Cast(fp16→fp32) + 计算 + Cast(fp32→fp16) |
| GM 偏移错 | 修正偏移公式(元素 vs 字节) |
| 尾 tile 长度错 | 计算/搬运用 curTileLength,偏移用 tileLength |
| tiling 参数错 | 修正 host 端 tiling 计算 |
| 同步缺失 | 添加正确的 EnQue/DeQue 或 PipeBarrier |
| ReduceSum 覆盖源 | 先 Adds 备份再 ReduceSum |
| 搬运长度错 | 修正 DataCopyExtParams 的 copyLen |
修复后
- 移除所有调试插桩(printf/DumpTensor),或用
#ifdef DEBUG_PRECISION 包裹
- 重新编译安装
- 运行原失败用例 + 完整精度测试
- 仍失败 → 回到 Phase 1(最多 3 轮),3 轮后仍失败则报告用户
输出要求(MANDATORY)
调试完成后 MUST 在对话中展示:问题摘要、根因分析、修复内容、验证结果、≥2 条关键经验。NEVER 仅回复「已修复」。
典型案例(按需加载)
定位到疑似根因后,加载对应案例了解完整排查过程:
不要一次性加载所有案例。 仅在误差特征匹配时加载对应案例。
反模式(NEVER)
- NEVER 不分析误差分布就直接改代码
- NEVER 在 kernel 中 printf 循环打全量 tensor — 用 DumpTensor 或 Host 侧对比
- NEVER 多核同时大量打印 — 加
GetBlockIdx() == 0 仅 0 核打印
- NEVER 在未同步位置读 LocalTensor — 必须在 DeQue/PipeBarrier 之后
- NEVER 用
PIPE_ALL 作为最终修复 — 仅用于实验隔离
- NEVER 修复后不移除调试代码
- NEVER 仅修复已知失败用例而不跑完整精度测试
- NEVER 超过 3 轮仍失败时继续尝试 — 应报告用户