1. 精度异常现象
在调用 linspace 算子时,当参数 dim 设置为 69 时,出现精度异常情况。尽管差异极小,但多次输入相同参数后,部分输出位置的数值存在微小波动。
torch.linspace(-dim, dim, steps=dim, device="npu")
这些偏差虽不显著,但在推理任务中可能对最终结果产生潜在影响。具体错误出现在索引位置 61 和 62。
3.8147e-06

2. 假设、实验与观察
2.1 多次运行偶现不一致
在重复执行过程中,该异常并非每次都会触发,但一旦发生,出错位置固定,初步判断可能是内存访问越界或数据覆盖所致。更值得注意的是,浮点值差异非常细微,容易误判为硬件层面(如芯片)问题。然而,在多台设备上复现该现象后,确认问题根源在于算子实现本身,属于软件层 bug。
2.2 出错索引特征分析
linspace 算子用于一维插值,生成指定数量 steps 的等差序列。测试发现,当 steps 取值为 8、16、24、32、40 等时无异常;但取 41、69、70 时则出现精度偏差,且错误均集中在输出数组末尾区域。
dim
代码审查发现,为满足硬件 32 字节对齐要求,系统对最后一个计算核(尾核)采用反向地址回退策略:从数据末端逆序计算偏移并完成加载、运算与存储操作。此机制可能导致倒数第二个核与尾核在数据处理范围上重叠,造成部分元素被重复计算。

3. 根源分析:精度误差来源
以 FP32 数据类型为例,每个元素占 4 字节,32 字节对齐需连续 8 个元素。当 dim = 69 时,总索引范围为 0 到 68。按照对齐规则,尾核起始位置设为 64,需处理 64 至 71 的数据段,但实际最大索引仅为 68,无法满足对齐长度。
为解决该问题,系统采取“地址回退”策略:从索引 68 开始向前回退 8 个元素,即处理区间 61–68。虽然技术上可行,但导致索引 61–63 区域被尾核和倒数第二个核共同计算,形成重复写入。
倒数第二个核的计算逻辑如下:

尾核的计算流程如下:

尽管两段逻辑在数学上等价——例如索引 61 处分别由以下两种方式计算:
stop-5*s-2*s
和
stop-7*s
但由于浮点数在芯片中的表示和运算存在舍入误差,不同核心的执行结果可能略有差异。此外,因核间并行执行,结果写回顺序不确定,导致索引 61 的最终值可能来自任一核,从而引发多次运行结果不一致。
base0 = stop - 5*s
4. 解决方案
为避免区域重叠带来的重复计算问题,可在倒数第二个核的数据搬运阶段使用非对齐搬运 API:
DataCopyPad
通过设置
DataCopyExtParams
参数,仅搬运索引 56–60 的数据,将 61–68 的完整处理交由尾核统一完成。如此可彻底消除计算交集,确保每次运行输出一致。
优化后的结果表现为确定性输出:
if (blockIdx == m_tilingData.realCoreNum - 2) {
int64_t alignNum =
outLen - (this->CeilDiv(m_tilingData.tailNum, elementPerBlock) * elementPerBlock - m_tilingData.tailNum);
LocalTensor<T> outLocal = outQueue.DeQue<T>();
DataCopyExtParams copyParams{1, static_cast<uint32_t>(alignNum * sizeof(T)), 0, 0, 0};
DataCopyPad(outputGm[gmOutOffset], outLocal, copyParams);
outQueue.FreeTensor(outLocal);
} else {
LocalTensor<T> outLocal = outQueue.DeQue<T>();
DataCopy(outputGm[gmOutOffset], outLocal, outLen);
outQueue.FreeTensor(outLocal);
}
5. 经验总结
在算子开发过程中,开发者常出于性能优化或实现便捷性考虑,采用数学等价变换(如对齐优化、分段计算)。然而,此类变换可能引入不可忽视的精度波动。尤其当多次运行结果不一致时,应视为严重缺陷。
本案例表明,即便理论计算等价,硬件执行层面的浮点误差与并行调度不确定性仍可能导致输出波动。此类行为在推理场景下不可接受,必须作为 bug 进行修复。


雷达卡


京公网安备 11010802022788号







