CANN graph-autofusion 自动融合精度一致性指南:二进制不一致的根源、保证边界与实操应对
【免费下载链接】graph-autofusionGraph-autofusion 是一个面向昇腾(Ascend)芯片的轻量级、解耦式组件集合,旨在通过自动融合技术加速模型执行。 目前已开源 SuperKernel 组件和 Autofuse 组件,未来将持续开放更多自动融合相关模块。项目地址: https://gitcode.com/cann/graph-autofusion
导读
本文基于 CANN graph-autofusion 开源仓库的《关于自动融合模块精度一致性的说明》,系统阐述自动融合(Autofuse)与 Eager 手写算子之间二进制精度差异的根本来源、自动融合自身提供的精度保证,以及希望在继续使用自动融合的前提下与 Eager 严格对齐时的实操处理路径。读者阅读本文后,将理解"为何融合代码与手写 kernel 在末位上必然存在差异""自动融合为何能做到精度不差于 Eager",并掌握 PyTorch Inductor 与 TensorFlow(GE 后端)两个场景下关闭/规避特定融合以对齐 Eager 的具体开关与注意事项。
一、结论先行:不保证二进制一致,但保证精度不差于 Eager
自动融合模块无法保证与 Eager 模式下手写算子的输出在二进制(bitwise)上完全一致;但通过保守的精度策略,可以保证精度不差于 Eager 模式。这两句话分别界定了"能承诺什么"与"不能承诺什么",也是本文全部内容展开的两条主线:
| 承诺项 | 是否保证 |
|---|---|
| 与 Eager 手写算子二进制完全一致 | ❌ 不保证(计算类算子);纯搬运类算子理论上可一致 |
| 精度不差于 Eager(累积舍入误差上界 ≤ Eager) | ✅ 保证(基于保守的精度策略) |
| 自动融合自身的输出确定性(可复现性) | ✅ 保证(编译期输入与运行期输入不变时,多次运行输出二进制一致) |
二、精度差异的根本来源:从"源码不同"到"数值不同"
2.1 代码生成路径不同——一切差异的起点
- Eager 模式:每个算子由人工预先编写,编译为固定的二进制 kernel;
- 自动融合:通过 codegen 技术在运行时(JIT)生成算子代码。即使不做任何融合,同一语义的算子,自动融合生成的代码与手写 kernel 也不是同一份源码。
源码不同,是一切数值差异的起点。在 graph-autofusion 仓库中,这条 JIT 代码生成路径的核心实现位于 autofuse/codegen 目录(如 codegen_kernel.cpp、codegen_tiling.cpp),自动融合在编译期动态产出融合 kernel 的代码与 Tiling 策略,这与人工固化在算子库中的二进制实现本质上是两条独立的生产链。
2.2 源码差异在四个层面演变为末位差异
同一算子的两份不同源码,至少会在以下四个层面引入可观察的末位差异:
1. 升精度策略不同为保持 fp16 下的数值稳定性,算子内部常把中间计算提升到 fp32。是否升、在哪一步升、何时降回,手写实现与 codegen 实现的选择不一定一致。
2. 指令选择不同同一语义可以用不同指令实现。例如Tensor * Scalar:
- 先 broadcast scalar,再走标准乘法指令;
- 直接使用
muls类的标量乘指令。
两者数学上等价,但在硬件上走的是不同的乘法单元,舍入行为不完全一致。
3. 切块策略与 Reduction 累加顺序不同浮点加法不满足结合律:(a+b)+c与a+(b+c)的结果在最低位通常不同。reduce、matmul、layernorm等计算的归约顺序由切块策略和并行度决定,而切块策略本身在自动融合与手写算子之间从硬件资源约束上不可能一致。
展开来说,在相同大小切块下,一个融合 kernel 内部要承担多个算子的中间计算,单步 UB(Unified Buffer)占用会高于单算子 kernel。而手写单算子为了降低搬运开销,往往会把切块尽量做大(用满 UB)——这种切块尺寸在融合场景下可能放不下。
4. 算法实现不同同一数学函数可以用不同算法逼近。例如rsqrt、div常采用多轮迭代逼近实现,迭代轮次、初值选取不同,末位误差就不同。
2.3 小结:这是原理层面限制,而非工程 bug
源码不同 → 在升精度、指令、累加序、算法等任一维度产生差异 → 最终输出在二进制位上不一致。这是原理层面的限制,不是可通过工程对齐消除的 bug。
三、可以保证二进制一致的场景:纯搬运类算子
精度差异来源于计算。对纯搬运类算子(不做任何数学运算,只重排数据):
broadcastconcat/splittranspose/reshape/slice
这类算子理论上可以做到二进制一致。
但这类场景的融合收益通常很小。自动融合的收益主要来自:
- 多个计算算子融合(消除中间结果的读写);
- 多个计算 + 单个搬运的融合(计算与访存 overlap)。
纯搬运融合在真实业务中占比低,因此"能保证一致的场景"与"值得融合的场景"交集很窄。值得注意的是,从仓库的精度提升实现看,improve_precision.cpp 中内置的默认黑名单(kBlackList1)恰好包含Data、Load、Scalar、Store、Output、Broadcast、Transpose、Concat、Gather、Slice等节点类型——这些纯搬运/数据类节点不参与计算精度提升,也从实现层面印证了"搬运类算子不做数学运算、不引入数值差异"的设计判断。
四、自动融合提供的三项保证
虽然与手写算子之间不保证二进制一致,自动融合在精度与可复现性两个方向上提供以下保证:
4.1 融合区域内部强制 fp32 累积
自动融合在生成的融合 kernel 内部,默认将所有中间计算统一提升到 fp32,只在融合块的输入/输出边界保留原 dtype。以损失部分计算性能的代价,换取更高的中间精度。
仓库中的实现证据非常直观:ImprovePrecisionForAscGraph(improve_precision.cpp)作为 PreProcess::Run 的第一步被调用,其核心动作包括:
- 在
Load/Gather之后插入升精度Cast(fp16/bf16 → fp32),见ProcessLoadGatherNodes; - 将计算类节点的输出 dtype 从 fp16/bf16 统一改写为 fp32(
FromDtypeToOtherDtype); - 在
Store落盘前插入降回原 dtype 的Cast(ProcessStoreNodes),保证融合块边界 dtype 不变。
对应的 ST 测试 test_improve_precision.cpp 验证了典型场景:一条load → abs → add → mul → sub → store的 fp16 链路经过ImprovePrecisionForAscGraph处理后,abs0、add0、mul0、sub0的输出 dtype 全部变为DT_FLOAT(fp32),且 Load 之后至少插入一个升精度 Cast。
同时,该 pass 提供黑名单机制:--autofuse_enhance_precision_blacklist可配置跳过硬编码黑名单之外的指定 AscIR 算子类型(取值为算子类型字符串,多个用英文逗号分隔,或配置为all;默认值为空)。需要注意:Sum、Mean、Prod不支持低精度类型,即使加入黑名单也仍会提升精度;CheckNodeDtype(improve_precision.cpp)会校验加入黑名单的节点在低精度下仍被 InferDtype 支持,否则直接报错拒绝配置。更多环境变量细节参见 环境变量参考。
4.2 精度不差于 Eager 模式
Eager 模式下,每个算子独立执行,算子与算子的中间结果必须按 tensor 原 dtype(如 fp16/bf16)落盘,再由下一个算子读入。这意味着:
- Eager 模式:在每个算子边界都发生一次 fp16/bf16 截断;
- 自动融合:把一串算子合并为一个 kernel,只在融合块边界截断,块内全程 fp32。
因此,自动融合的累积舍入误差上界 ≤ Eager 模式下对应算子链的累积误差。即,精度不低于 Eager 模式。这一点与 4.1 的源码实现完全吻合:升精度 Cast 插在Load之后、降精度 Cast 插在Store之前,正是"块内 fp32、边界原 dtype"的工程落地。
4.3 自身的确定性(可复现性)
本节讨论的是自动融合自身的输出确定性,不涉及与 Eager 模式的对比。
自动融合的确定性可以分两层来看:
- 编译确定性:相同的编译期输入 → 相同的 kernel 二进制。编译期输入包括模型结构、shape、dtype、自动融合的版本与编译选项。
- 执行确定性:相同的 kernel 二进制 + 相同的运行期输入 → 相同的输出。运行期输入包括张量数值、芯片型号等。
两层叠加,即可得到端到端的保证:编译期输入与运行期输入都不变时,多次运行的输出在二进制上完全一致。
也就是说,前文列举的差异来源(升精度策略、指令选择、切块/累加顺序、算法实现),在自动融合自身的两次编译+运行之间是完全一致的——它们只在"自动融合 vs 手写算子"之间产生分歧。自动融合本身不引入额外的不确定性。仓库中的配套测试计划(自动融合精度测试报告 中的任务 A)正是针对这一论点设计:同一进程内连续执行 1000 次输出 MD5 集合大小应为 1,清空 JIT 缓存冷启动后 kernel hash 与输出 MD5 均应保持不变。
五、PyTorch Inductor 的相关说明:业界共识
PyTorch Inductor 社区不保证精度的二进制一致,其逻辑与自动融合一致。以下是文档摘录的三个权威来源:
5.1 PyTorch 文档《Numerical accuracy》
涉及浮点结合律与 bit-exact,原文:
"PyTorch is not guaranteed to produce bitwise identical results for floating point computations that are mathematically identical."
"floating point addition and multiplication are not associative, so the order of the operations affects the results."
5.2 Edward Yang《Ways to use torch.compile》(2024)
作者为 PyTorch 核心开发者,文章在 "Improve training efficiency on a small-medium scale" 一节中说明torch.compile与 eager 的数值关系:
"Unfortunately, the compiler does not guarantee exact bitwise equivalence with eager code; we reserve the right to do things like select different matrix multiply algorithms with different numerics or eliminate unnecessary downcast/upcasts when fusing half precision compute together."
5.3 PyTorch 文档《torch.compile Troubleshooting》
"Accuracy Debugging" 一节中关于下游编译器数值表现的说明:
"the reason we need this is downstream compilers will codegen code whether it's Triton code or the C++ backend, the numerics from those downstream compilers can be different in subtle ways yet have dramatic impact on your training stability."
这组引用说明一个行业级事实:"编译器不保证与 eager 二进制一致"是 PyTorch 官方及核心开发者公开声明的设计立场,与 graph-autofusion 的精度一致性结论同源同逻辑。
六、对照表:Eager vs 自动融合
| 维度 | Eager 手写算子 | 自动融合 |
|---|---|---|
| 源代码 | 人工固定 | JIT 生成 |
| 二进制一致性 | 基准 | 不保证(计算类)/ 可一致(纯搬运类) |
| 中间计算精度 | 受限于算子间 dtype(常为 fp16) | 融合块内部 fp32 |
| 累积误差上界 | 基准 | ≤ Eager 基准 |
| 性能 | 基准 | 显著优于基准 |
七、使用建议:希望继续使用自动融合时的处理路径
若用户关注的是数值正确性(模型精度、收敛性、下游业务指标),自动融合在精度上无劣势、在性能上有显著收益,可直接启用,无需额外处理。
若用户有与 Eager 严格对齐的诉求,又希望继续使用自动融合的性能收益,是十分有挑战的,可参考以下策略组合使用。
7.1 PyTorch-Inductor 场景:状态和使用建议
与 Eager 模式严格对齐涉及 Inductor 融合前的图处理与融合阶段两部分。Inductor 对多数相关流程未提供原生关闭开关,需通过 patch 或修改源码实现。
融合前处理
Inductor 在自动融合前会执行以下可能引入数值差异的流程:
- Inplace 算子函数化:例如
relu_→relu,函数化后的 Kernel 实现与 Eager 可能存在差异。 - Decompose:将复杂算子分解为基础算子以扩大融合范围,与单算子实现相比算法有差异。
- FX 图 Pass:主要为 pattern 替换类融合 Pass,涉及 Kernel 替换。
- 前反向切图:基于自动重计算的切图策略,重计算的融合 Kernel 与 Eager 模式可能存在差异。
建议措施:
- Inplace 算子函数化为融合前提、非功能项,无法关闭。实践中建议仅实现 Inplace 版本,通过自动函数化减少 Kernel 差异带来的数值差异。
- Decompose:Inductor 原生不提供关闭选项,需通过 patch 或修改源码实现。
- FX 图 Pass:Inductor 不提供全部关闭的选项,且每个版本生效的 Pass 可能不同,需根据执行时实际情况依次关闭。
- 前反向切图:Inductor 使用最大流最小割算法切图,会引入重计算,可通过配置
custom_partitioner_fn替换为无重计算的切图策略。
自动融合
Inductor 融合阶段因重新生成执行 Kernel 引入数值差异,涉及的融合类型如下:
| 融合类型 | 影响二进制精度 | 数值差异来源 | 用户可配置 |
|---|---|---|---|
| Reduction 类 | 是 | 累加顺序不同 | 需 patch 或修改源码 |
| Pointwise 类 | 是 | 类型提升规则、指令映射、迭代算法差异 | 需 patch 或修改源码 |
| MM/FA 模板类 | 是 | 算法实现差异 | 需 patch 或修改源码 |
使用须知:
- 如追求与 Eager 严格对齐,需关闭上述所有融合类型。Inductor 未提供相应配置项,需通过 patch 或修改源码实现。
- 建议显式开启
TORCHINDUCTOR_EMULATE_PRECISION_CASTS,避免 Lowering 过程中的 Cast 消除优化引入数值误差。PyTorch 场景的其他调测环境变量(如TORCH_COMPILE_DEBUG、TORCHINDUCTOR_FORCE_DISABLE_CACHES)可参见 环境变量参考。
7.2 TensorFlow 场景:状态和使用建议
通用优化
TensorFlow 场景默认使用 GE 作为图编译器后端。GE 自带的融合 pass 与硬件无关优化 pass(例如常量折叠、等价公式变换)都会导致 kernel 源代码变化,使二进制精度一致无法保障。
建议措施:
- 将图编译优化等级设为 O0(仅保留功能类优化)。
- O0 等级下仍会执行功能相关与静态 shape 相关优化。可打开 DUMP 图开关观察 GE 中实际生效的 pass,对照前文 2.2 节判断对精度的影响;若有影响,可通过手工调整脚本规避相应优化(难度较高)。
自动融合
自动融合目前支持以下类别算子的融合,默认策略与可控性如下:
| 融合类型 | 默认状态 | 影响二进制精度 | 用户可配置 |
|---|---|---|---|
| elemwise(含 broadcast) | 开启 | 是 | 否 |
| reduce | 关闭 | 是 | 是 |
| concat | 关闭 | 否 | 是 |
| slice | 关闭 | 否 | 是 |
| gather | 关闭 | 否 | 是 |
| transpose | 关闭 | 否 | 是 |
默认关闭的融合可通过环境变量显式开启,以 concat 为例:
export AUTOFUSE_FLAGS="--enable_autofuse=true;--autofuse_enable_pass=concat"多个扩展融合可用英文逗号组合,例如同时开启 reduce 与 concat:
export AUTOFUSE_FLAGS="--enable_autofuse=true;--autofuse_enable_pass=reduce,concat"仓库中的 TensorFlow 融合样例 af_tf_eleandreduce/README.md 完整演示了这一流程:Reduce 融合默认不使能,需要先设置--autofuse_enable_pass=reduce再运行脚本;融合生效时,可在 Profiling 的op_summary_*.csv中观察到autofuse_reduce_前缀的融合 Kernel(其中包含 Abs 与 ReduceSum 计算),且不再出现对应的独立Abs、ReduceSumKernel。
使用须知:
- elemwise 融合暂不支持关闭。该类融合既是二进制精度差异的主要来源,也是融合收益的主要来源,关闭后整体收益将显著降低。如确有关闭需求,可提交 issue 反馈。从配置实现看,auto_fuse_config.h 等源码中
--autofuse_enable_pass目前仅支持扩展融合类型,基础 elemwise 融合由--enable_autofuse整体控制,两者在开关粒度上不对称,印证了"基础融合不可单独关闭"的约束。 - 默认关闭的融合属于实验特性,开启后可能出现功能异常或性能劣化,建议在目标网络上实测后再决定是否保留。
八、附:配套测试计划与回归体系
仓库为上述四类核心论点提供了可执行的验证框架,详见 自动融合精度测试报告,其任务拆解与本文论点的对应关系为:
- 任务 S(共享基础设施):提供
bit_equal、ulp_diff、error_stats、fp64_reference等数值比较工具,覆盖 fp16 / bf16 / fp32 及 inf/nan 场景,是其余任务的公共底座; - 任务 A:验证 §4.3 自动融合自身的二进制确定性(1000 次运行 hash 唯一);
- 任务 C1/C2/C3:分别验证 §2.2 中的指令选择差异、Reduction 累加顺序差异、rsqrt 等迭代算法差异;
- 任务 D:验证 §3 纯搬运类算子的二进制一致性(transpose / reshape / slice / concat / split / broadcast 组合子图);
- 任务 B/E:验证 §4.2"累积误差不差于 Eager"的定量结论,以及端到端训练指标对齐(loss / grad norm / PPL 曲线)。
结合源码层的 test_improve_precision.cpp ST 用例,读者可以完整构建"文档结论 → 源码实现 → 自动化测试"三层证据链,既理解"为什么",也掌握"怎么验证"。
总结
自动融合与 Eager 手写算子的二进制不一致是原理层面的必然——代码生成路径不同、升精度策略不同、指令选择不同、累加顺序与算法实现不同,任何一环都足以造成末位差异;但自动融合通过块内强制 fp32 累积、只在融合块边界截断的保守精度策略,保证精度不差于 Eager,同时保持自身编译与执行的双重确定性。对于确有严格对齐诉求的场景,PyTorch Inductor 侧需通过 patch 或修改源码关闭 Reduction / Pointwise / MM/FA 三类融合并显式开启TORCHINDUCTOR_EMULATE_PRECISION_CASTS,TensorFlow 侧则需将 GE 图编译优化等级设为 O0、按需通过AUTOFUSE_FLAGS控制扩展融合(elemwise 基础融合不可关闭)。在绝大多数关注模型精度、收敛性与下游业务指标的场景下,直接启用自动融合即可获得无精度劣势的显著性能收益。
【免费下载链接】graph-autofusionGraph-autofusion 是一个面向昇腾(Ascend)芯片的轻量级、解耦式组件集合,旨在通过自动融合技术加速模型执行。 目前已开源 SuperKernel 组件和 Autofuse 组件,未来将持续开放更多自动融合相关模块。项目地址: https://gitcode.com/cann/graph-autofusion
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考