GE 图引擎 IR 恢复流程分析
GE 图引擎 IR 恢复流程分析
一、问题背景:为什么需要 IR 恢复?
GE 图引擎面临一个独特的版本兼容性挑战:直构图时的算子 IR 定义与运行环境的 IR 定义可能不一致。
1.1 具体场景
- 用户在 CANN 8.0.RC1 环境下构图,模型中使用了
Add算子的 3 个输入 - 模型部署到 CANN 8.0.RC2 环境,
Add算子新增了第 4 个可选输入bias - 如果不恢复 IR,运行环境会认为算子定义不完整,导致执行失败
1.2 现有方案的不足
- 硬编码兼容:在算子实现中写大量版本判断逻辑,维护成本高
- 模型重编译:要求用户在新环境下重新构图,破坏了模型的可移植性
- 忽略差异:直接忽略新增的输入/属性,可能导致功能缺失
1.3 GE 的解决方案
设计一套自动化的 IR 恢复机制,在模型加载时动态适配版本差异,既保证兼容性,又无需用户干预。
二、设计哲学:前向兼容优先,后向兼容兜底
GE 的 IR 恢复遵循一个清晰的设计哲学:**”新环境能跑旧模型,旧环境尽量跑新模型”**。
这个哲学体现在兼容性策略的三层设计:
| 策略 | 场景 | 处理方式 | 设计动机 |
|---|---|---|---|
| kForward | 直构图 IR > 运行环境 IR | 删除多余的可选元素 | 旧环境无法支持新特性,但可选特性删掉不影响核心功能 |
| kBackward | 直构图 IR < 运行环境 IR | 添加缺失的元素 | 新环境有新特性,添加后能完整运行 |
| kNone | 版本一致 | 无需处理 | 最理想情况 |
2.1 关键权衡:为什么前向兼容要”删除”而不是”忽略”?
如果只是忽略多余的可选输入,算子的 IR 元数据会不一致——元数据说有 4 个输入,但实际只连了 3 个。这会导致后续的 shape 推导、内存分配等流程出错。删除是让元数据与实际状态保持一致的正确做法。
三、核心数据结构
3.1 IR 定义缓存结构
1 | |
3.2 兼容性策略枚举
1 | |
3.3 设计亮点
IrDefinition作为缓存单元,同类型算子共享一份,避免重复创建临时算子strategy字段记录推导结果,后续处理流程据此分支is_required_attr数组区分必选/可选属性,前向兼容时只删可选
四、核心流程
4.1 整体流程架构
flowchart TD
Start([模型加载/图预处理]) --> Entry{调用入口}
Entry -->|图级别| RecoverGraph[RecoverIrDefinitions<br/>遍历图中所有节点]
Entry -->|算子级别| RecoverOp[RecoverOpDescIrDefinition<br/>单个算子恢复]
RecoverGraph --> LoopNodes[循环处理每个节点]
LoopNodes --> GetOpType[获取算子类型]
GetOpType --> CheckCache{检查缓存<br/>op_type_to_ir_def}
CheckCache -->|已缓存| UseCache[使用缓存的 IrDefinition]
CheckCache -->|未缓存| InitIR[InitIrDefinitionsIfNeed<br/>初始化 IR 定义]
InitIR --> CreateTempOp[OperatorFactory::CreateOperator<br/>创建临时算子]
CreateTempOp --> ExtractIR[提取运行环境的 IR 定义]
ExtractIR --> CacheIR[缓存到 op_type_to_ir_def]
CacheIR --> UseCache
UseCache --> SkipCheck{特殊算子检查<br/>NETOUTPUT/Data}
SkipCheck -->|是| NextNode[处理下一个节点]
SkipCheck -->|否| DeriveStrategy[DeriveCompatibilityStrategy<br/>推导兼容性策略]
DeriveStrategy --> StrategyResult{策略结果}
StrategyResult -->|kForward| ProcessForward[ProcessForwardCompatInputs<br/>删除多余可选输入]
StrategyResult -->|kBackward| ProcessBackward[AppendIrDefs<br/>添加缺失元素]
StrategyResult -->|kNone| NoProcess[无需处理]
StrategyResult -->|kFailed| ReturnFailed[返回失败]
ProcessForward --> RecoverAttrs[RecoverIrAttrNames<br/>处理属性差异]
ProcessBackward --> RecoverAttrs
NoProcess --> RecoverAttrs
RecoverAttrs --> RecoverIO[RecoverIrInputAndOutput<br/>处理输入输出差异]
RecoverIO --> RecoverDefaults[RecoverIrAttrDefaultValue<br/>恢复属性默认值]
RecoverDefaults --> ShareSymbols[ShareDtypeSymbolsFrom<br/>共享数据类型符号]
ShareSymbols --> CheckSubGraph{检查子图属性}
CheckSubGraph -->|有子图| RecoverSubGraph[递归恢复子图]
CheckSubGraph -->|无子图| NextNode
RecoverSubGraph --> NextNode
NextNode -->|还有节点| GetOpType
NextNode -->|全部完成| End([恢复完成])
ReturnFailed --> End
style Start fill:#e1f5e1
style End fill:#ffe1e1
style StrategyResult fill:#fff4e1
style ProcessForward fill:#e1f0ff
style ProcessBackward fill:#f0e1ff
style ReturnFailed fill:#ffe1e1
4.2 兼容性策略推导流程
flowchart TD
Start([开始推导策略]) --> Compare[比较节点 IR 与运行环境 IR]
Compare --> CalcAttrDiff[计算属性差异<br/>attr_diff = node_attrs - runtime_attrs]
Compare --> CalcInputDiff[计算输入差异<br/>input_diff = node_inputs - runtime_inputs]
CalcAttrDiff --> CheckConsistency{检查方向一致性}
CalcInputDiff --> CheckConsistency
CheckConsistency -->|属性差异为正<br/>输入差异为正| Forward[kForward<br/>前向兼容]
CheckConsistency -->|属性差异为负<br/>输入差异为负| Backward[kBackward<br/>后向兼容]
CheckConsistency -->|属性差异为零<br/>输入差异为零| None[kNone<br/>无差异]
CheckConsistency -->|方向不一致<br/>如属性差异为正但输入差异为负| Failed[kFailed<br/>推导失败]
Forward --> ExplainForward[解释:<br/>直构图版本高于运行环境版本<br/>需要删除多余的可选元素]
Backward --> ExplainBackward[解释:<br/>直构图版本低于运行环境版本<br/>需要添加缺失的元素]
None --> ExplainNone[解释:<br/>版本一致,无需处理]
Failed --> ExplainFailed[解释:<br/>属性与输入方向矛盾<br/>属于不兼容变更]
ExplainForward --> Return([返回策略])
ExplainBackward --> Return
ExplainNone --> Return
ExplainFailed --> Return
style Start fill:#e1f5e1
style Return fill:#e1f5e1
style Forward fill:#e1f0ff
style Backward fill:#f0e1ff
style None fill:#f5f5f5
style Failed fill:#ffe1e1
4.3 前向兼容处理流程(删除多余元素)
sequenceDiagram
participant Main as RecoverIrUtils
participant OpDesc as OpDesc<br/>节点算子描述
participant IRMeta as IRMeta<br/>IR 元数据
Main->>OpDesc: GetIrInputs()
OpDesc-->>Main: ir_inputs_in_node
Main->>Main: 遍历多余输入<br/>[runtime_size, node_size)
loop 每个多余输入
Main->>OpDesc: MutableInputDesc(input_name)
OpDesc-->>Main: input_desc or nullptr
alt 未连边的可选输入
Main->>IRMeta: RemoveIrInput(input_name)
Note right of IRMeta: 删除多余可选输入<br/>保持元数据一致性
Main->>Main: GELOGD 记录删除日志
else 必选输入或已连边
Main->>Main: GELOGE 错误日志
Main-->>Caller: 返回 FAILED
end
end
Main->>OpDesc: GetIrAttrNames()
OpDesc-->>Main: attr_names_in_node
Main->>Main: 遍历多余属性<br/>[runtime_size, node_size)
loop 每个多余属性
Main->>OpDesc: HasAttr(attr_name)
OpDesc-->>Main: true/false
alt 可选属性且未配置
Main->>IRMeta: RemoveIrAttrName(attr_name)
Note right of IRMeta: 删除多余可选属性
else 必选属性或已配置
Main->>Main: GELOGE 错误日志
Main-->>Caller: 返回 FAILED
end
end
Main-->>Caller: GRAPH_SUCCESS
4.4 后向兼容处理流程(添加缺失元素)
sequenceDiagram
participant Main as RecoverIrUtils
participant OpDesc as OpDesc<br/>节点算子描述
participant RuntimeIR as IrDefinition<br/>运行环境 IR
Main->>OpDesc: GetIrInputs()
OpDesc-->>Main: ir_inputs_in_node
Main->>RuntimeIR: inputs
RuntimeIR-->>Main: runtime_ir_inputs
Main->>Main: 遀历缺失输入<br/>[node_size, runtime_size)
loop 每个缺失输入
Main->>OpDesc: AppendIrInput(name, type)
Note right of OpDesc: 添加缺失的输入定义<br/>保持顺序一致性
end
Main->>OpDesc: GetIrOutputs()
OpDesc-->>Main: ir_outputs_in_node
Main->>RuntimeIR: outputs
RuntimeIR-->>Main: runtime_ir_outputs
Main->>Main: 遀历缺失输出<br/>[node_size, runtime_size)
loop 每个缺失输出
Main->>OpDesc: AppendIrOutput(name, type)
Note right of OpDesc: 添加缺失的输出定义
end
Main->>OpDesc: GetIrAttrNames()
OpDesc-->>Main: attr_names_in_node
Main->>RuntimeIR: attr_names
RuntimeIR-->>Main: runtime_attr_names
Main->>Main: 遀历缺失属性<br/>[node_size, runtime_size)
loop 每个缺失属性
Main->>OpDesc: AppendIrAttrName(name)
Note right of OpDesc: 添加缺失的属性名
end
Main-->>Caller: GRAPH_SUCCESS
五、关键设计决策
5.1 为什么用临时算子获取 IR 定义?
决策:通过 OperatorFactory::CreateOperator 创建临时算子实例,从中提取 IR 定义。
替代方案:
- 直接读取算子注册表:更直接,但注册表可能不包含完整的 IR 元数据
- 硬编码 IR 定义:维护成本高,每次算子更新都要改代码
权衡分析:
- 临时算子方案能获取完整的 IR 元数据(包括默认值、类型信息)
- 代价是创建算子实例有开销,但通过缓存机制(
op_type_to_ir_def) amortize 了成本 - 对于没有注册 IR 的算子(如 FrameworkOp),优雅降级为跳过恢复
代码依据:ir_definitions_recover.cc:252-275
5.2 为什么前向兼容要删除而不是忽略?
决策:前向兼容时,删除多余的可选输入/属性,而不是保留但忽略。
替代方案:
- 保留但不处理:元数据不一致,后续流程(shape 推导、内存分配)会出错
- **标记为”已忽略”**:增加复杂度,需要额外的状态管理
权衡分析:
- 删除保证了元数据与实际状态的一致性,这是 GE 架构的基础假设
- 只删除可选且未连边/未配置的元素,不影响核心功能
- 必选元素或已连边元素存在差异时,判定为不兼容并报错,避免运行时崩溃
代码依据:ir_definitions_recover.cc:172-203
5.3 为什么输出总是向后兼容?
决策:输出处理不区分前向/后向,总是添加缺失的输出(如果运行环境有更多输出)。
设计动机:
- 输出是算子的”产品”,新增输出不影响已有输出的语义
- 输出不需要”连边”检查——输出是算子自己产生的,不是外部提供的
- 前向兼容场景下,节点输出数不可能大于运行环境输出数(否则推导失败)
代码依据:ir_definitions_recover.cc:384-391
5.4 为什么属性和输入方向必须一致?
决策:如果属性差异和输入差异方向不一致(如属性多了但输入少了),判定为 kFailed。
设计动机:
- 方向不一致意味着算子发生了不兼容的结构性变更
- 例如:新增了必选属性,但删除了必选输入——这种变更无法通过简单的增删元素来适配
- 强制一致性检查,避免运行时出现更严重的错误
代码依据:ir_definitions_recover.cc:299-328
六、模块间协作关系
6.1 协作模式分析
- OperatorFactory:负责创建临时算子,提供运行环境的 IR 定义
- OpDesc/IRMeta:被修改的主体,RecoverIrUtils 作为友元类直接操作其内部状态
- 缓存机制:
op_type_to_ir_def在图级别恢复时共享,避免重复创建临时算子 - 递归处理:子图属性(如
kUbOriginGraphAttrKey)触发递归恢复,保证嵌套图的兼容性
七、业界对比与设计洞察
7.1 与其他框架的兼容性方案对比
| 框架 | 兼容性方案 | 设计哲学 | 优缺点 |
|---|---|---|---|
| TensorFlow | 版本号检查 + Op 版本注册表 | 显式版本管理 | 优点:精确控制;缺点:维护成本高 |
| PyTorch | JIT 编译时重新推导 | 动态适配 | 优点:灵活;缺点:性能开销 |
| ONNX Runtime | Opset 版本 + 兼容性层 | 标准化版本 | 优点:跨框架;缺点:依赖标准演进 |
| GE | IR 恢复 + 兼容性策略推导 | 自动化适配 | 优点:无需用户干预;缺点:推导逻辑复杂 |
7.2 GE 的独特之处
- 不依赖显式的版本号,而是通过比较 IR 定义差异自动推导策略
- 前向兼容设计(删除多余可选元素)是其他框架少见的做法
7.3 如果重新设计,可能的改进方向
引入版本号语义:
- 当前方案完全依赖 IR 定义比较,无法区分”新增可选输入”和”新增必选输入”的版本差异
- 如果算子注册时携带版本号,可以更精确地判断兼容性
支持部分前向兼容:
- 当前方案对必选元素差异直接报错,可以考虑提供”降级运行”选项
- 例如:新增的必选输入有默认值时,允许前向兼容运行
增强可观测性:
- 当前只有日志记录,可以增加恢复统计(多少算子前向兼容、多少后向兼容)
- 帮助用户了解模型的版本适配情况
八、亮点与问题
8.1 亮点
- 自动化程度高:用户无需关心版本差异,系统自动适配
- 缓存机制高效:同类型算子共享 IR 定义,避免重复开销
- 设计哲学清晰:前向兼容优先,后向兼容兜底,策略推导有明确规则
- 元数据一致性保证:删除多余元素而非忽略,避免后续流程出错
8.2 问题
- 推导逻辑复杂:属性和输入方向一致性检查增加了理解难度
- 缺少版本号语义:无法区分”可选新增”和”必选新增”的版本差异
- 错误处理不够友好:兼容性失败时只报错,不提供降级选项
- 可观测性不足:缺少恢复统计,用户难以了解适配情况
九、总结与启发
9.1 核心启发
- 元数据一致性是架构的基础假设:删除多余元素而非忽略,体现了对一致性的重视
- 自动化适配需要明确的规则:兼容性策略的三层设计(前向/后向/无差异)提供了清晰的决策框架
- 缓存是 amortize 开销的关键:临时算子创建有成本,但通过缓存让成本可接受
9.2 适用场景
- 需要支持模型跨版本部署的框架
- 算子定义可能随版本演进的系统
- 希望减少用户版本管理负担的场景
十、调用入口汇总
| 调用位置 | 文件路径 | 调用场景 |
|---|---|---|
| 图预处理 | compiler/graph/preprocess/graph_prepare.cc:2390 |
图编译前的预处理阶段 |
| RT2 模型转换 | runtime/v2/lowering/model_converter.cc:552 |
RT2 运行时模型转换 |
| Hybrid 模型构建 | runtime/v1/hybrid/model/hybrid_model_builder.cc:1067 |
Hybrid 执行器模型构建 |
| 图管理器 | compiler/graph/manager/graph_manager.cc:4190 |
图管理器统一入口 |
| 算子 Tiling | base/common/op_tiling/op_tiling_rt2.cc:844 |
RT2 算子 Tiling 阶段 |
| 图重写 | compiler/graph/fusion/graph_rewriter.cc:255 |
图融合重写后恢复 IR |
| GE 工具函数 | compiler/graph/common/ge_utils.cc:73 |
GE 通用工具函数 |
| FE 图优化 | compiler/engines/nn_engine/optimizer/graph_optimizer/fe_graph_optimizer.cc:1857 |
FE 图优化器 |
十一、动态输入的处理机制
11.1 IR 输入类型定义
GE 的 IR 输入分为三种类型,每种类型在恢复时有不同的处理规则:
1 | |
11.2 动态输入的典型案例:Concat 算子
1 | |
IR 定义解读:
concat_dim:必选输入,位置固定在第 0 位x:动态输入,可以有多个(如x0,x1,x2…),数量由属性N决定N:属性,记录动态输入的实际数量
11.3 动态输入的处理规则
规则一:顺序兼容性检查(强制)
1 | |
含义:动态输入的数量可以不同,但前缀部分必须一致。
规则二:动态输入的数量检查
1 | |
11.4 Concat 算子的具体恢复场景
场景一:前向兼容(节点有更多动态输入)
1 | |
处理方式:
- 检查前缀:
concat_dim一致,x0/x1/x2类型一致(都是 Dynamic)✅ - 推导策略:
kForward(节点输入数 > 运行环境输入数) - 关键判断:检查
x3是否已连边- 如果
x3未连边 → 删除该输入,恢复成功 - 如果
x3已连边 → 报错失败(旧环境无法处理这个输入)
- 如果
场景二:后向兼容(节点有更少动态输入)
1 | |
处理方式:
- 检查前缀:
concat_dim一致,x0/x1类型一致 ✅ - 推导策略:
kBackward(节点输入数 < 运行环境输入数) - 添加缺失的
x2输入定义,恢复成功
场景三:必选输入差异(失败)
1 | |
处理方式:
- 检查前缀:第 0 位输入不一致(
concat_dim_v2vsconcat_dim) - 直接失败,无法恢复
11.5 什么样的 IR 可以被恢复?
可以恢复的情况
| 场景 | 条件 | 处理方式 |
|---|---|---|
| 动态输入数量差异 | 前缀一致,多余输入未连边 | 删除多余输入(前向)或添加缺失输入(后向) |
| 可选输入差异 | 多余可选输入未连边 | 删除多余可选输入 |
| 可选属性差异 | 多余可选属性未配置 | 删除多余可选属性 |
| 输出数量差异 | 节点输出 ≤ 运行环境输出 | 添加缺失输出 |
不能恢复的情况
| 场景 | 原因 | 代码依据 |
|---|---|---|
| 必选输入差异 | 必选输入必须存在且连边 | ir_definitions_recover.cc:182-187 |
| 已连边的多余输入 | 实际使用了新特性,旧环境不支持 | ir_definitions_recover.cc:190-194 |
| 已配置的多余属性 | 实际使用了新特性 | ir_definitions_recover.cc:226-229 |
| 前缀顺序不一致 | 破坏了 IR 的基本结构 | ir_definitions_recover.cc:147-152 |
| 属性与输入方向矛盾 | 不兼容的结构性变更 | ir_definitions_recover.cc:312-320 |
11.6 动态输入的特殊性
关键洞察:动态输入的数量差异不触发兼容性策略推导。
1 | |
实际行为:
- 动态输入的数量差异在
CheckIrSpec检查时就被允许了(不判定为不匹配) - 只有当必选输入的数量或顺序发生变化时,才会触发兼容性策略推导
11.7 Concat 算子的完整恢复流程示例
sequenceDiagram
participant Node as 节点 Concat<br/>N=4
participant Runtime as 运行环境 IR<br/>N=3
participant Recover as RecoverIrUtils
Node->>Recover: GetIrInputs()
Note right of Node: [concat_dim, x0, x1, x2, x3]
Runtime->>Recover: GetIrInputs()
Note right of Runtime: [concat_dim, x0, x1, x2]
Recover->>Recover: ValidateIrOrderCompatibility
Note right of Recover: 检查前缀 [concat_dim, x0, x1, x2]<br/>类型都是 Required/Dynamic ✅
Recover->>Recover: DeriveCompatibilityStrategy
Note right of Recover: input_diff = 1,且为正<br/>策略 = kForward
Recover->>Node: MutableInputDesc("x3")
Node-->>Recover: nullptr(未连边)
alt x3 未连边
Recover->>Node: RemoveIrInput("x3")
Note right of Node: 删除多余的动态输入
Recover-->>Caller: GRAPH_SUCCESS
else x3 已连边
Recover->>Recover: GELOGE 错误
Recover-->>Caller: GRAPH_FAILED
end
11.8 动态输入处理的核心原则
总结:
- 数量可变,顺序不变:动态输入的数量可以不同,但前缀部分必须完全一致
- 前向兼容需检查连边:多余的动态输入如果已连边,则无法恢复(实际使用了新特性)
- 后向兼容直接添加:缺失的动态输入直接添加定义即可
- 不触发策略推导:动态输入的数量差异在规格检查时就被允许,不参与兼容性策略推导
Concat 算子的恢复条件:
concat_dim必选输入必须一致- 动态输入
x的数量可以不同,但已有的x0/x1...类型必须一致 - 多余的动态输入如果未连边,可以删除;如果已连边,则失败
分析日期:2026-05-05
分析工具:repo-analyzer skill
代码版本:GE trunk_ai/ge