已合并
fix: ensure deterministic codegen and reliable PGO fallback #1662
fix: ensure deterministic codegen and reliable PGO fallback #1662
已合并
zhang_shengjie创建于 21 天前
zhang_shengjie成员
21 天前

描述

一、主要解决的问题

1.1 跨进程 codegen 产物不确定

相同静态图在独立进程中可能生成不同的源码和缓存 hash,导致 Inductor PGO 无法稳定复用 codegen 结果。根因包括:

  • VF 并行节点和边界锚点排序依赖节点 ID、插入顺序。
  • VF 拓扑排序只考虑数据边,未纳入控制依赖。
  • 临时 Buffer 分配顺序受裸指针地址影响。

1.2 PGO skip 场景未可靠回退

  • fallback 程序返回 0,调用方将 no-op 误判为调优成功。
  • PGO 搜索结果文件缺失时直接抛出 OSError,中断正常回退流程。

1.3 共享低精度 Cast 被删除导致计算分支舍入语义变化

问题发生在一个 FP32 中间结果显式降精度到 BF16/FP16 后,同时被两个分支消费的场景:

对应用例:P0-NET-01

该问题由 Inductor baseline/PGO 对比验证中的 RMSNorm 组合用例 P0-NET-01 暴露。真实 PyTorch 构图如下:

def net01(x, residual, weight):
    add = x + residual
    add_fp32 = add.to(torch.float32)
    inv_rms = torch.rsqrt(torch.mean(torch.pow(add_fp32, 2), dim=-1, keepdim=True) + 1e-6)
    out = weight * (add_fp32 * inv_rms).to(torch.bfloat16)
    return add, inv_rms, out

输入规格:

输入 dtype shape
x BF16 32×2048
residual BF16 32×2048
weight BF16 2048

三个输出具有不同的精度合同:

输出 dtype/shape 数值来源 rtol/atol
add BF16,32×2048 x + residual 的 BF16 舍入结果 0/0,要求精确一致
inv_rms FP32,32×1 基于 add 升 FP32 后的平方、尾轴 Mean 和 Rsqrt 1e-5/1e-5
out BF16,32×2048 add_fp32 × inv_rms 降为 BF16 后再乘 weight 1e-2/1.25e-1

关键数据依赖为:

x BF16 ───────┐
              ├─ Add BF16 ───────────────→ output0: add BF16
residual BF16 ┘      │
                     ↓ Cast FP32
                add_fp32
                     ├─ Pow 2 → Mean axis=-1 → Add 1e-6 → Rsqrt
                     │                                      └→ output1: inv_rms FP32
                     └─ Mul inv_rms → Cast BF16 → Mul weight ─→ output2: out BF16

add 既是直接输出,又是 RMSNorm 计算输入,因此它的 BF16 舍入是可观察语义。eager 计算要求后续 RMSNorm 使用:

FP32(BF16(x + residual))

ImprovePrecision 删除共享低精度 Cast 后,Store 分支会重新补 BF16 Cast,但计算分支直接使用升精度后的 Add 结果,等价于:

FP32(x) + FP32(residual)

两者在 BF16 舍入边界附近并不等价,会进一步影响 inv_rmsout。修复必须保留 add 对应的共享 BF16 舍入边界,并只在计算分支将这个已舍入值升回 FP32。

原始语义:
FP32 计算结果
    ↓ Cast BF16 或 FP16,产生一次明确舍入
    ├─→ Store,形成低精度输出
    └─→ Cast FP32 → 后续计算,形成另一个输出

ImprovePrecision 会删除所有浮点类型间 Cast,再根据下游节点补 Cast。对于上述共享节点,旧流程会把降精度 Cast 只补到 Store 分支,计算分支则直接使用未舍入的 FP32 值:

错误改写:
FP32 计算结果
    ├─→ 新 Cast BF16 或 FP16 → Store
    └─→ 后续 FP32 计算
         ↑ 未经过原图要求的低精度舍入,计算结果可能变化

这会破坏多输出图的数值语义:低精度输出仍然正确,但后续计算输出不再基于同一个已舍入中间值。该问题不是 PGO 特有问题;PGO 或泛化测试可能暴露它,但根因位于通用 ImprovePrecision 图预处理。

修复后,满足以下全部条件的 Cast 被识别为共享舍入边界并保留:

  • 输入为 FP32,输出为 BF16 或 FP16。
  • Cast 输出至少连接一个 Store。
  • Cast 输出同时连接至少一个非 Store 计算分支。

后续预处理会在计算分支补回 BF16/FP16 到 FP32 的升精度 Cast,因此计算仍使用已舍入值:

修复后:
FP32 计算结果
    ↓ 保留 Cast BF16 或 FP16
    ├─→ Store
    └─→ 新 Cast FP32 → 后续 FP32 计算
         ↑ 保留原始低精度舍入语义

单 Store、单计算分支、低精度升 FP32 以及其他数据类型的 Cast 删除行为保持不变。

二、问题解决前后对比

场景 解决前 解决后 效果
VF 并行节点排序 使用节点 ID,同级节点顺序受插入顺序影响 使用节点名作为稳定主键,入口校验节点名唯一 跨进程节点顺序一致
VF 依赖建模 仅统计数据边 数据边、控制边共同参与拓扑排序,并对相同后继去重 保留执行顺序约束
VF 边界锚点 沿节点和 peer 遍历顺序收集 按节点名、anchor index 排序 输入输出 tensor 编号稳定
临时 Buffer 分配 按指针地址或不完整的生命期键排序 按生命期、allocation_order、group_id 排序 Buffer ID 和源码稳定
PGO skip 返回码 返回 0,被误判为成功 返回 1 正确回退原始解
搜索文件缺失 抛出 OSError 返回失败三元组 编译流程不中断
共享 FP32→BF16/FP16 Cast 删除后仅在 Store 分支补回降精度 Cast,计算分支使用未舍入 FP32 值 保留共享降精度 Cast,并在计算分支补升精度 Cast 保持多输出图的舍入和精度语义
optimize_ut 编译 缺少 AscGraphBuilder 声明 显式包含 asc_graph_builder.h 阻塞编译恢复
NBNC 告警 MergeTensorByGroupId 为 51 删除一次性类型别名,等价表达后为 50 满足 codecheck 门限
flowchart LR
    subgraph Before[解决前]
        B1[VF 顺序受 ID 和插入顺序影响] --> B2[边界顺序不稳定]
        B2 -->|影响| B3[Buffer ID 受指针地址影响]
        B3 -->|导致| B4[源码和缓存 hash 波动]
        B5[PGO skip 返回 0] --> B6[误判调优成功]
        B7[测试缺少直接 include] --> B8[optimize_ut 编译失败]
    end
    subgraph After[解决后]
        A1[节点名和控制依赖稳定拓扑] --> A2[稳定边界锚点]
        A2 -->|保证| A3[稳定生命期分配]
        A3 -->|实现| A4[源码和缓存 hash 一致]
        A5[PGO skip 返回 1] --> A6[回退原始解]
        A7[显式 include] --> A8[optimize_ut 编译通过]
    end
    B4 -.修复.-> A4
    B6 -.修复.-> A6
    B8 -.修复.-> A8

三、修改方案

3.1 VF 排序稳定化

  • Partition 入口校验节点名唯一,避免稳定排序键产生歧义。
  • 并行节点优先按节点名排序;拓扑依赖从 GetOutDataNodes 改为 GetOutAllNodes。
  • VF 输入输出边界按节点名和 anchor index 排序。

3.2 Buffer 分配稳定化

  • TensorInfo、TensorGroup 增加 allocation_order,并按图遍历顺序赋值。
  • TensorGroupLifeLess 以 merged_life_start 为主键,以 allocation_order、group_id 为稳定次键。
  • AllocTmpBuff 将指针 map 内容收集到 vector 后稳定排序,再分配 Buffer。

3.3 PGO 回退可靠化

  • PGO fallback 生成代码返回值从 0 改为 1,使 pgo_program_exec 进入失败回退路径。
  • pgo_get_top_result 捕获 OSError,结果文件缺失时返回 (None, None, None)。

3.4 保留共享低精度舍入边界

  • ShouldDeleteCastNode 增加消费者信息,只对 FP32→BF16/FP16 且同时连接 Store 和计算分支的 Cast 禁止删除。
  • 保留降精度 Cast,使低精度输出和计算分支共享同一个已舍入中间值。
  • ProcessOtherComputeNodes 在低精度 Cast 到计算节点之间补回升精度 Cast,使后续计算继续以 FP32 执行,但输入值与原图一致。
  • 修复不依赖 enable_autofuse_pgo,对经过 ImprovePrecisionForAscGraph 的通用 Autofuse 图生效。

3.5 Review 阻塞项修复

  • test_vf_partition.cpp 显式包含 asc_graph_builder.h,消除传递 include 依赖。
  • MergeTensorByGroupId 直接使用 int64_t map key,并用 const auto 保存 group_id;不改变分组逻辑,NBNC 从 51 降至 50。

变更类型

关联的 Issue

无。

如何测试

一、本地验证

验证项 命令或范围 结果
optimize UT 构建 cmake --build build --target optimize_ut -j 8 通过,生成最新 optimize_ut
定向回归 Buffer 分配 1 项、VF 分区 4 项 5/5 通过
Python UT test_compile_adapter.py 41 项通过
Codegen UT PgoTilingKeyCountOverflowShouldFallbackTfAndPgoRunner 1 项通过
ImprovePrecision ST Fp32ToBf16CastSharedByStoreAndCompute_Preserved 覆盖共享 BF16 舍入边界保留和下游 FP32 计算
格式与补丁 git diff --check、git clang-format --diff 通过
提交门禁 pre-commit clang-format、codespell、OAT 等全部通过
NBNC MergeTensorByGroupId 50

定向回归覆盖:

  • 临时 Buffer 按生命期稳定分配。
  • VF 控制依赖保序。
  • 重复节点名在改图前被拒绝。
  • VF 输出和 tensor 顺序不受并行节点插入顺序影响。
  • FP32→BF16 共享 Cast 同时连接 Store 和计算分支时保留舍入边界。

二、远端 compile

  • 流水线 #1004745,HEAD 1cac68b542f8:success。
  • static-check、compile、llt 均完成;X86、ARM、Ubuntu 24 构建及 API_Check、PreSmoke 通过。
  • ci-pipeline-passed label 已回写,结果对应最新提交。

三、系统与上板验证

  • Ascend 950 环境下,SR001、SR007、SR012 多进程重复生成的 cache hash 均唯一。
  • NC018 静态 shape 场景中,baseline Top1 与 PGO Top3 的 prepare、profile 均通过,cache_reused=True。
  • P0-NET-01 修复后 baseline Top1、PGO Top3 均通过,cache_reused=true;add 精确一致,inv_rms 按 1e-5/1e-5 校验,out 按 1e-2/1.25e-1 校验。
  • 验证时成套加载同一次构建的 Graph-autofusion 动态库,避免新旧组件混用。

核对清单

其他信息

兼容性与影响

  • 不修改公开 API/ABI、Python/C++ 绑定、构建打包或 runtime 接口。
  • 图优化只固定同级节点和边界锚点顺序,并补全既有控制依赖,不引入新 pass。
  • Buffer 变更只消除非确定性遍历顺序;PGO 返回码调整是既有失败语义修正。
  • ImprovePrecision 仅收窄共享 FP32→BF16/FP16 Cast 的删除条件;该精度修复为通用预处理行为,不以 PGO 开关为条件。
  • 未新增高频日志、额外 runtime 调用或显著复杂度,性能影响可忽略。

提交记录

Commit 描述 修改文件数
b71a34a0 ensure deterministic codegen across processes 7
58571b25 preserve control dependencies in VF ordering 2
269b71d9 fallback when TF PGO search is skipped 5
1cac68b5 address review build and codecheck issues 2
136deb13 preserve shared low precision cast semantics 2

修改文件清单

文件路径 说明
autofuse/v35/optimize/partition/vector_func_partitioner.cpp VF 拓扑与边界锚点稳定排序,纳入控制依赖
autofuse/v35/optimize/partition/vector_func_partitioner.h 声明节点名唯一性校验
autofuse/optimize/buffer_allocate/buf_que_allocator.cpp 记录临时 Buffer allocation_order
autofuse/optimize/buffer_allocate/mem_reuse_manager.cpp 稳定分配顺序并修复 NBNC 告警
autofuse/optimize/buffer_allocate/mem_reuse_manager.h 声明单组临时 Buffer 分配函数
autofuse/optimize/buffer_allocate/tensor_mem_defs.h 增加 allocation_order
autofuse/codegen/codegen_tiling_pgo_runtime.cpp 修正 PGO fallback 返回码
autofuse/compiler/python/compile_adapter.py 处理 PGO 搜索文件缺失
autofuse/tests/v35/ut/optimize/test_vf_partition.cpp VF 稳定性、控制依赖和重复名 UT;补直接 include
autofuse/tests/ut/optimize/test_buf_que_allocator.cpp Buffer 稳定分配 UT
autofuse/tests/ut/python/test_compile_adapter.py 搜索文件缺失 UT
autofuse/tests/ut/codegen/test_codegen_tiling.cpp 适配 PGO fallback 返回码
autofuse/tests/st/backend_e2e/pgo_add_abs_inductor_test/pgo_add_abs_inductor_backend_generate.cpp 适配 fallback 生成代码
autofuse/optimize/pre_process/improve_precision.cpp 保留 Store 与计算分支共享的低精度舍入边界
autofuse/tests/st/optimize/test_improve_precision.cpp 覆盖共享 BF16 Cast 的图改写语义
likedislike
Pull Request已成功合入, 合并人@CANN-robot
(感谢 zhang_shengjie 的贡献)
Zzhang_shengjie成员
21 天前 创建了 pull request,commit 3729bb90
atomgit-bot
atomgit-bot
21 天前 评论:

变更摘要

本次 PR 旨在修复跨进程 codegen 产物不确定的问题。根因在于 VF(Vector Function)分区中的并行节点/边界锚点排序依赖插入顺序或节点 ID,以及临时 Buffer 分配间接受 std::map 裸指针键遍历顺序影响。修改通过引入基于节点名和 anchor index 的稳定排序策略,并为临时 Buffer 记录确定的 allocation_order 字段,使 TensorGroup 排序以生命周期、allocation order 和 group ID 为稳定键,从而确保相同静态图在不同进程中生成一致的源码和缓存 hash。

主要改动

  • VF 分区节点排序稳定化: 在 vector_func_partitioner.cpp 中,将 BuildDependencyAwareRanksTopologicalSortingForVfGraph 中的回退比较从仅比较 GetId() 改为 (GetName(), GetId()) 对,确保同名节点优先按节点名确定顺序。
  • VF 边界锚点排序稳定化: 新增 SortBoundaryAnchors 函数,对输入边界 (load_to_peer_in_anchors) 和输出边界 (store_to_peed_in_anchors) 按节点名和 anchor index 排序,并在 BuildSubgraph 中调用,消除锚点插入顺序对子图构建的影响。
  • 临时 Buffer 分配顺序稳定化: 在 TensorInfoTensorGroup 中新增 allocation_order 字段;在 InitNodeTmpBuffInfo 中按遍历顺序递增赋值;在 MemReuseManager 中新增 TensorGroupLifeLess 比较器(按 merged_life_startallocation_ordergroup_id 排序),并将其应用于 AllocForTQueAllocForCalc 以及重构后的 AllocTmpBuff / AllocTmpBuffGroup,消除 map 裸指针键遍历顺序对 Buffer ID 分配的影响。
  • 新增确定性行为单元测试: test_vf_partition.cpp 新增测试验证不同并行节点插入顺序下 VF 输出消费者顺序和图节点顺序一致;test_buf_que_allocator.cpp 新增测试验证临时 Buffer 在地址顺序与生命周期顺序相反时仍按生命周期稳定分配 ID。
likedislike
atomgit-bot
atomgit-bot
21 天前 评论:

代码审查

✅ 未发现问题

likedislike
CANN-robotCANN-robot成员
21 天前 添加了label:cann-cla/yes
CANN-robot
CANN-robot成员
21 天前 评论:

Thanks for your pull-request.
The full list of commands accepted by me can be found at here
You can get sig-info at here


PR Approval Progress

Congratulations! All modules have met the lgtm and approve requirements.

Module Approval Details

module lgtm status approve status
repo-cann/graph-autofusion 张德鹏, xchu42, xuyafei (3/2) 张德鹏 (1/1)

💡 Tip:

  • Committer can comment /approve or /lgtm
  • Commenting /approve implies both code review (lgtm) and intent to merge (approve)

CLA Signature Pass

zhang_shengjie, thanks for your pull request. All authors of the commits have signed the CLA. 👍

likedislike
此处折叠了80条消息 查看更多
张德鹏成员
16 天前 评论:

/approve

likedislike
CANN-robotCANN-robot成员
16 天前 添加了label:approved
CANN-robotCANN-robot成员
16 天前 合入了pull request
Zzhang_shengjie成员
15 天前 修改了pull request 的描述
Zzhang_shengjie成员
15 天前 修改了pull request 的描述