1
1
DCU技术达人
海光 DCU 实测:BF16 逐位一致为什么仍可能出现文本漂移
2026-10-08 22:27:21
30
# 海光 DCU 实测:BF16 逐位一致为什么仍可能出现文本漂移 ## 摘要 在海光 DCU gfx936 上优化大模型算子时,“误差很小”并不是一个足够明确的正确性结论。即使某个形状下输出逐位一致,也不代表其他真实形状、完整计算图或自回归生成过程一定保持一致。 本文使用 Qwen3.5-27B BF16 推理的本地实测数据,分析一个典型现象:融合 kernel 在 T=4096 的 full-graph 对照中完全逐位一致,并获得 1.92159x 的局部加速;但在完整服务中,10 个请求有 4 个生成文本发生变化,输出 token 总数从 1786 变为 1776。 这不是“BF16 天生不可靠”,而是说明算子正确性需要沿着真实 shape、计算图边界和生成链路逐层验证。 --- ## 1. BF16 的正确性不是一个布尔值 对一个 BF16 kernel,可以设置很多层次的正确性门槛: 1. 输出没有 NaN 或 Inf; 2. 最大绝对误差在容差内; 3. 相对 L2 误差足够小; 4. 输出 BF16 bitwise equal; 5. CUDAGraph 重放结果稳定; 6. 所有生产 shape 都满足相同结论; 7. 完整服务输出长度和文本保持一致。 前几项属于算子层,后几项属于系统层。 问题在于,大模型是自回归系统。某一步 logits 的极小差异,如果恰好出现在两个候选 token 的分数边界附近,就可能改变下一枚 token。下一步的输入随之变化,后续状态和输出轨迹也会完全分叉。 因此: > 单个张量的误差很小,不等于生成行为一定不变;单个 shape 逐位一致,也不等于所有 shape 都逐位一致。 --- ## 2. 为什么归约顺序会改变 BF16 输出 RMSNorm、矩阵乘法和 Attention 都包含归约。以 RMSNorm 为例,核心项可以简化为: ~~~text variance = sum(x[i] * x[i]) / hidden_size output = x * rsqrt(variance + eps) * weight ~~~ 数学上,加法满足结合律;有限精度浮点数并不严格满足。 ~~~text (a + b) + c 可能不等于 a + (b + c) ~~~ 当 kernel 改变 row block、wave 分工或图融合边界时,归约树也可能变化。即使最终仍然写回 BF16,中间 FP32 累加的最低几位也可能不同。 这种差异通常很小,但“很小”和“逐位相同”是两个不同标准。 --- ## 3. 一个看起来已经完全正确的 standalone 结果 本地测试过一条 Q/K RMSNorm、MRoPE 和 gate 提取的融合路径。对 T=4096 的完整链路,显式 full-graph 基线与融合实现结果如下: | 项目 | 基线 | 融合实现 | |---|---:|---:| | 耗时 | 1.235199 ms | 0.642799 ms | | 加速比 | 1.00000x | 1.92159x | | Q mismatch | 0 | 0 | | K mismatch | 0 | 0 | | gate mismatch | 0 | 0 | | 重复运行 | 稳定 | 稳定 | 如果只看到这张表,很容易得出“性能和正确性都已经通过”的结论。 但这里严格证明的其实只有: ~~~text 在 T=4096 在这一份 full-graph 基线下 Q/K/gate 输出逐位一致 ~~~ 它并没有证明所有尾部 token 长度都采用相同归约布局。 --- ## 4. Shape sweep 找到了隐藏的正确性空洞 继续对真实 token 数进行检查后,结果并不是简单的“全部正确”或“全部错误”。 逐位一致的形状包括: ~~~text T = 1 / 17 / 2004 / 2231 / 2992 3712 / 3713 / 4087 / 4095 / 4096 ~~~ 出现小量不一致的形状包括: ~~~text T = 65 / 1024 / 1502 / 1674 / 1777 ~~~ 在 T=1502 上继续扫描 rows-per-block 为 1、2、4、8、16,仍没有找到逐位一致的布局。 这组结果说明,不一致并不是由某一个简单的 block 参数决定。编译器可能针对不同 token 数选择不同 reduction、tile 或融合计划。一个对 T=4096 完全正确的 kernel,不能自动外推到所有 T>1。 --- ## 5. 完整服务给出了最终答案 该实现随后完成了一次固定 10 请求的本地服务测试: | 指标 | 参考配置 | 融合配置 | |---|---:|---:| | output throughput | 11.8926923 | 12.0612512 | | 吞吐变化 | - | +1.4173% | | p99 TPOT | 44.9788 ms | 44.6850 ms | | 输出 token 总数 | 1786 | 1776 | | 文本不一致 | 0/10 | 4/10 | | failed | 0 | 0 | 局部性能信号确实存在,p99 TPOT 也有所下降;但输出 token 数和 4 个请求的文本发生变化,因此不能把这次测试描述成“等价加速”。 这里最关键的不是 1.4173%,而是 standalone 与完整服务的判断发生了分歧: ~~~text T=4096 full-graph:逐位一致 真实混合 shape:部分不一致 完整生成:4/10 文本漂移 ~~~ --- ## 6. 一个真正穿过全部门槛的对照案例 另一条 BF16 M=1 GEMM 调整提供了正向对照。它只改变 rows per block,不改变每一行的累加顺序。 三种真实权重形状中,它相对参考实现的最大差异均为 0,重复运行也保持一致。进入完整服务后: ~~~text 4K-8K: 10/10 文本 byte-identical 8K-16K: 10/10 文本 byte-identical 16K-32K: 10/10 文本 byte-identical 输出长度:三档全部一致 failed: 0 ~~~ 三档合计 30 个请求都通过,局部加速也稳定传递到端点。 这个对照说明,严格正确并不意味着不能提速;真正的要求是优化不能只在一个“方便测试的 shape”上正确。 --- ## 7. 一套更可靠的 BF16 验证顺序 ### 第一层:算术边界 - finite 检查; - max abs、mean abs、relative L2; - BF16 mismatch count; - 重复运行确定性。 ### 第二层:shape 边界 至少覆盖: - T=1 Decode; - 主要 Prefill chunk; - 非整齐尾块; - block 和 tile 的边界值; - 实际请求中出现过的 token 数。 ### 第三层:图边界 分别比较: - eager 参考; - 单独编译的 full graph; - 原始整模型图中的实现; - CUDAGraph capture 与 replay。 同一段数学在不同图边界中,可能被编译成不同归约树。不能拿 eager 的结果替代整模型图,也不能拿独立 full graph 替代原始 AOT 图。 ### 第四层:生成边界 完整服务至少记录: - 每个请求的输出 token 数; - 生成文本; - failed 数量; - TPOT 和 TTFT; - 重复运行是否保持同一方向。 --- ## 8. 这对海光 DCU 开发者意味着什么 这组结果不应被理解成“海光 DCU 的 BF16 有特殊问题”。它真正说明的是:在 gfx936 目标环境里,kernel、DTK 编译链、Inductor/AOT 图边界和 CUDAGraph 共同决定了最终浮点执行顺序。 对海光 DCU 上游算子移植和本地优化,建议额外坚持三点: 1. 在真实 gfx936 设备上覆盖生产 shape,而不是只凭其他 GPU 的结果推断; 2. 同时保留 standalone 与原始整模型图的数值对照; 3. 把生成文本和输出长度作为 DCU 推理链路的最终验收层。 海光 DCU 论坛里讨论“算子是否正确”时,也应明确它通过的是哪一级:单 kernel、CUDAGraph,还是完整 vLLM 服务。 ## 9. 结论 海光 DCU 上的 BF16 优化需要区分三件事: 1. 数学公式是否相同; 2. 浮点归约顺序是否相同; 3. 自回归生成行为是否相同。 本地数据表明,一个 T=4096 下 1.92159x、Q/K/gate 全部逐位一致的融合实现,仍可能在其他 token 形状上出现差异,并最终造成 4/10 文本漂移。 因此,算子正确性的最终定义不应停留在单个张量: > 从真实 shape 到原始计算图,再到完整生成,整条链路都通过,才算真正正确。 --- 文中数据来自同一套本地 DCU 实验记录。不同模型、编译器版本、图边界和 kernel 实现可能得到不同结果,本文结论不应外推为所有 BF16 算子的统一误差规律。
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.2
0 帖子
楼主热帖
暂无数据