GE 图引擎动态图拆分模块分析
GE 图引擎动态图拆分模块分析
一、问题背景:为什么需要动态图拆分?
GE 图引擎面临一个根本性的架构挑战:如何在一张图中同时处理静态 shape 算子和动态 shape 算子?
1.1 具体场景
典型问题场景:
- 用户模型中包含动态 shape 算子(如
Reshape、Broadcast),其输入/输出 shape 在编译时未知(dim=-1) - 同时模型中也有大量静态 shape 算子(如
Conv2D、MatMul),其 shape 在编译时已确定 - 如果不拆分,整个图会被标记为”动态图”,导致:
- 静态算子无法享受静态图优化(如内存复用、算子融合)
- 动态算子无法享受动态图特性(如 shape 推导、流式执行)
- 整体性能严重下降
1.2 现有方案的不足
| 方案 | 问题 |
|---|---|
| 全静态图 | 动态算子无法执行,shape 推导失败 |
| 全动态图 | 静态算子性能损失,无法做深度优化 |
| 用户手动拆分 | 用户负担重,破坏模型可移植性 |
| 算子级别动态调度 | 调度开销大,无法做跨算子优化 |
1.3 GE 的解决方案
设计一套自动化的动态图拆分机制,将一张混合图拆分成多个子图:
- 静态子图:包含静态 shape 算子,享受静态图优化
- 动态子图:包含动态 shape 算子,享受动态图特性
- 自动连接:通过
PartitionedCall算子自动连接子图,保证数据流正确
核心价值:用户无需关心拆分细节,系统自动适配,既保证兼容性,又最大化性能。
二、设计哲学:动静分离,最优调度
GE 的动态图拆分遵循一个清晰的设计哲学:**”静态算子走静态路,动态算子走动态路,自动连接不丢数据”**。
这个哲学体现在三个核心原则:
2.1 原则一:最小化动态子图
动机:动态子图的调度开销大,应尽量减少。
实现:
- 只将真正需要动态 shape 的算子放入动态子图
- 静态算子尽量合并到静态子图,即使与动态算子相邻
- 小静态子图(<阈值)会被合并到动态子图,避免碎片化
代码依据:dynamic_shape_partition.cc:774-791(ChangeSmallClusterType)
2.2 原则二:最大化静态子图优化
动机:静态子图可以做深度优化(内存复用、算子融合),应尽量放大。
实现:
- 静态算子优先合并到静态子图
- 静态子图可以跨多个动态算子(通过
PartitionedCall连接) - 静态子图内部可以做完整的图优化
代码依据:dynamic_shape_partition.cc:912-963(MergeClustersNormal)
2.3 原则三:保证数据流正确性
动机:拆分后数据流不能断,必须保证输入输出正确。
实现:
- 每个 Cluster 对应一个
PartitionedCall算子 PartitionedCall的输入/输出锚点与原 Cluster 的输入/输出锚点一致- 子图内部通过
Data和NetOutput算子连接
代码依据:base_cluster.cc:BuildFrame、CombinePartitionFrame
三、核心数据结构
3.1 Cluster:拆分的基本单元
1 | |
设计亮点:
min_和max_记录拓扑范围,用于判断是否可以合并(避免环)in_clusters_和out_clusters_记录 Cluster 间的连接关系subgraph_和partition_node_是拆分后的产物
3.2 DynamicShapeCluster:动态图拆分的特化
1 | |
关键行为:
- 合并时,如果被合并的 Cluster 是动态类型,当前 Cluster 也会变为动态类型
- 动态 Cluster 的子图会被标记为”内存不连续分配”(
ATTR_NAME_MEMORY_DISCONTIGUOUS_ALLOCATION)
3.3 DynamicShapePartitioner:拆分的主控制器
1 | |
核心职责:
- 标记动态节点(
MarkUnknownShapeNodes) - 初始化 Cluster(
InitClusters) - 合并 Cluster(
MergeClusters) - 构建子图(
BuildPartitionFrame)
四、核心流程
4.1 整体流程架构
flowchart TD
Start([图预处理入口]) --> Init[Initialize<br/>初始化阈值参数]
Init --> CheckScene{场景判断}
CheckScene -->|Host执行模式| SetUnknown[SetRootGraphUnknown<br/>整图标记为动态]
CheckScene -->|仅包含Data/NetOutput| SetUnknown
CheckScene -->|阈值=-1| SetUnknown
SetUnknown --> End([结束])
CheckScene -->|正常场景| GetIndepGraph[GetMultiBatchIndependCompileGraphs<br/>获取独立编译子图]
GetIndepGraph --> LoopGraph[遍历每个独立编译子图]
LoopGraph --> MarkNodes[MarkUnknownShapeNodes<br/>标记动态节点]
MarkNodes --> CheckNeed{是否需要拆分?}
CheckNeed -->|无动态节点| Skip[跳过拆分<br/>标记为静态图]
CheckNeed -->|单算子场景| SingleOp[单算子场景处理<br/>标记为动态图]
CheckNeed -->|需要拆分| CtrlTransfer[CtrlEdgeTransfer<br/>控制边转移]
CtrlTransfer --> PartitionImpl[PartitionImpl<br/>执行拆分]
PartitionImpl --> GenerateCluster[GenerateCluster<br/>生成 Cluster]
GenerateCluster --> InitClusters[InitClusters<br/>初始化 Cluster]
InitClusters --> MergeClusters[MergeClusters<br/>合并 Cluster]
MergeClusters --> Prune[PruneUniqueClusters<br/>去重并排序]
Prune --> RePartition[ReDynamicShapePartitioner<br/>二次拆分优化]
RePartition --> BuildFrame[BuildPartitionFrame<br/>构建子图框架]
BuildFrame --> CombineFrame[CombinePartitionFrame<br/>连接子图]
CombineFrame --> BuildSubgraph[BuildPartitionSubgraph<br/>构建完整子图]
BuildSubgraph --> Clear[ClearResource<br/>清理资源]
Clear --> NextGraph{还有子图?}
NextGraph -->|是| LoopGraph
NextGraph -->|否| End
Skip --> NextGraph
SingleOp --> NextGraph
style Start fill:#e1f5e1
style End fill:#ffe1e1
style PartitionImpl fill:#e1f0ff
style MergeClusters fill:#f0e1ff
style BuildFrame fill:#fff4e1
4.2 动态节点标记流程
flowchart TD
Start([遍历图中节点]) --> CheckAttr{检查强制标记属性}
CheckAttr -->|ATTR_IS_UNKNOWN_SHAPE=true| MarkUnknown[标记为动态节点]
CheckAttr -->|ATTR_FORCE_UNKNOWN_SHAPE=true| MarkUnknown
CheckAttr -->|无强制标记| CheckEngine{检查引擎类型}
CheckEngine -->|Host CPU引擎| MarkUnknown
CheckEngine -->|其他引擎| CheckShape{检查 Shape}
CheckShape -->|存在 Unknown Shape<br/>dim=-1/-2| CheckNoTiling{是否支持 No Tiling?}
CheckNoTiling -->|支持| MarkNoTiling[标记为 No Tiling 节点<br/>不走 Tiling 流程]
CheckNoTiling -->|不支持| MarkUnknown
CheckShape -->|静态 Shape| CheckSubgraph{检查子图}
CheckSubgraph -->|子图包含动态节点| MarkUnknown
CheckSubgraph -->|子图全静态| CheckTilingDepend{检查 Tiling Depend}
CheckTilingDepend -->|是 Tiling Depend 算子| CheckTilingSink{是否支持 Tiling Sink?}
CheckTilingSink -->|支持| MarkTilingSink[标记为 Tiling Sink<br/>走 Host Tiling]
CheckTilingSink -->|不支持| MarkUnknown
CheckTilingDepend -->|非 Tiling Depend| CheckAddrRefresh{是否支持地址刷新?}
CheckAddrRefresh -->|不支持| MarkUnknown
CheckAddrRefresh -->|支持| MarkKnown[标记为静态节点]
MarkUnknown --> CollectConst[收集相连的 Const 节点<br/>放入动态集合]
MarkNoTiling --> CollectConst
MarkKnown --> CheckSpecial{是否为特殊节点?}
CheckSpecial -->|是<br/>如 Case/While/PartitionedCall| MarkSpecial[标记 has_special_node=true]
CheckSpecial -->|否| NextNode
MarkSpecial --> NextNode
CollectConst --> NextNode
MarkTilingSink --> NextNode
NextNode{还有节点?}
NextNode -->|是| CheckAttr
NextNode -->|否| CheckMixed{检查混合场景}
CheckMixed -->|动态节点 + No Tiling 节点共存| RevertNoTiling[回退 No Tiling 标记<br/>全部放入动态集合]
CheckMixed -->|无混合| End([标记完成])
RevertNoTiling --> End
style Start fill:#e1f5e1
style End fill:#ffe1e1
style MarkUnknown fill:#ffe1e1
style MarkKnown fill:#e1f5e1
style MarkNoTiling fill:#fff4e1
style MarkTilingSink fill:#f0e1ff
4.3 Cluster 初始化流程
sequenceDiagram
participant Partitioner as DynamicShapePartitioner
participant Graph as ComputeGraph
participant Node as Node
participant Cluster as DynamicShapeCluster
Partitioner->>Graph: TopologicalSorting()
Note right of Graph: 拓扑排序保证顺序
Partitioner->>Partitioner: InitClusterType()
Note right of Partitioner: 初始化类型映射<br/>DATA/KNOWN_SHAPE/UNKNOWN_SHAPE/NETOUTPUT
Partitioner->>Graph: GetDirectNode()
Graph-->>Partitioner: nodes
loop 每个节点
Partitioner->>Node: GetType()
Node-->>Partitioner: type
alt Data节点且无输入
Partitioner->>Partitioner: type_index = kDataTypeIndex
else Const节点且无输入
Partitioner->>Partitioner: type_index = kInputNodeTypeIndex
else NetOutput节点
Partitioner->>Partitioner: type_index = kNetOutputTypeIndex
else PartitionedCall且有Stage属性
Partitioner->>Partitioner: type_index = kStageTypeIndex
else 动态节点集合中
Partitioner->>Partitioner: type_index = kUnknownShapeTypeIndex
else 其他
Partitioner->>Partitioner: type_index = kKnownShapeTypeIndex
end
Partitioner->>Cluster: new Cluster(rank++, type_index, node)
Partitioner->>Partitioner: RecordClusters(cluster)
Partitioner->>Partitioner: SetCluster(node, cluster)
Partitioner->>Node: GetInAllNodes()
Node-->>Partitioner: parent_nodes
loop 每个父节点
Partitioner->>Cluster: AddInput(GetCluster(parent))
end
alt 控制流节点
Partitioner->>Node: GetAttr(ATTR_CONTROL_FLOW_GROUP)
Node-->>Partitioner: group_index
Partitioner->>Partitioner: control_nodes_[group_index].push_back(node)
end
end
Partitioner-->>Partitioner: 初始化完成
4.4 Cluster 合并流程(核心)
flowchart TD
Start([开始合并]) --> CheckMode{合并模式}
CheckMode -->|merge_known_first=true| KnownFirst[优先合并静态 Cluster]
CheckMode -->|merge_known_first=false| UnknownFirst[优先合并动态 Cluster]
KnownFirst --> MergeControl[MergeClustersControlFlow<br/>合并控制流 Cluster]
UnknownFirst --> MergeControl
MergeControl --> CheckTopoMode{拓扑排序模式}
CheckTopoMode -->|stable_rdfs_sort| ConsistantId[MergeClustersWithConsistantId<br/>保持 ID 一致性合并]
CheckTopoMode -->|其他| Normal[MergeClustersNormal<br/>常规合并]
ConsistantId --> MergeIdConsistant[MergeIdConsistantCluster<br/>按 ID 一致性合并]
MergeIdConsistant --> MergeRef[MergeRefVariableCluster<br/>合并 Variable Cluster]
MergeRef --> MergeInput[MergeClustersInput<br/>合并输入 Cluster]
MergeInput --> End
Normal --> KnownFirstBranch{merge_known_first?}
KnownFirstBranch -->|true| TopoKnown1[TopologicalSortClusters<br/>按静态 Cluster 拓扑排序]
TopoKnown1 --> TryMergeKnown1[TryMergeClusters<br/>尝试合并静态 Cluster]
TryMergeKnown1 --> ChangeSmall1[ChangeSmallClusterType<br/>小静态 Cluster 变为动态]
ChangeSmall1 --> MergeInputData1[MergeClustersInputData<br/>合并输入数据 Cluster]
MergeInputData1 --> TopoUnknown1[TopologicalSortClusters<br/>按动态 Cluster 拓扑排序]
TopoUnknown1 --> TryMergeUnknown1[TryMergeClusters<br/>尝试合并动态 Cluster]
TryMergeUnknown1 --> End
KnownFirstBranch -->|false| TopoUnknown2[TopologicalSortClusters<br/>按动态 Cluster 拓扑排序]
TopoUnknown2 --> MergeUnknown2[MergeClustersUnknownShape<br/>合并动态 Cluster]
MergeUnknown2 --> TopoKnown2[TopologicalSortClusters<br/>按静态 Cluster 拓扑排序]
TopoKnown2 --> TryMergeKnown2[TryMergeClusters<br/>尝试合并静态 Cluster]
TryMergeKnown2 --> MergeInputData2[MergeClustersInputData<br/>合并输入数据 Cluster]
MergeInputData2 --> ChangeSmall2[ChangeSmallClusterType<br/>小静态 Cluster 变为动态]
ChangeSmall2 --> TopoUnknown2Again[TopologicalSortClusters<br/>再次按动态 Cluster 排序]
TopoUnknown2Again --> TryMergeUnknown2[TryMergeClusters<br/>再次尝试合并动态 Cluster]
TryMergeUnknown2 --> End
style Start fill:#e1f5e1
style End fill:#ffe1e1
style MergeControl fill:#f0e1ff
style Normal fill:#e1f0ff
style ConsistantId fill:#fff4e1
4.5 Cluster 合并的详细逻辑
sequenceDiagram
participant Partitioner as DynamicShapePartitioner
participant ClusterA as Cluster A<br/>(目标)
participant ClusterB as Cluster B<br/>(被合并)
participant PathClusters as Path Clusters<br/>(路径上的 Cluster)
Partitioner->>Partitioner: 遍历 ordered_cluster_
loop 每个 Cluster
Partitioner->>ClusterA: Inputs()
ClusterA-->>Partitioner: in_clusters
loop 每个输入 Cluster
alt 动态 Cluster 合并
Partitioner->>ClusterB: IsUnknownShape()
ClusterB-->>Partitioner: true
Partitioner->>ClusterA: MergeAllPathFrom(ClusterB)
Note right of ClusterA: 合并从 ClusterB 到 ClusterA<br/>路径上的所有 Cluster
ClusterA->>PathClusters: 计算路径
PathClusters-->>ClusterA: path_clusters
loop 每个路径 Cluster
ClusterA->>ClusterA: Merge(path_cluster)
Note right of ClusterA: 合并节点、更新拓扑范围<br/>更新输入输出关系
end
Partitioner->>Partitioner: expired_clusters.insert(path_clusters)
Partitioner->>Partitioner: merged_clusters.insert(ClusterA)
else 静态 Cluster 合并
Partitioner->>ClusterB: IsKnownShape()
ClusterB-->>Partitioner: true
Partitioner->>ClusterA: TryMerge(ClusterB)
Note right of ClusterA: 尝试合并,检查是否会产生环
alt 不会产生环
ClusterA->>ClusterA: Merge(ClusterB)
Partitioner->>Partitioner: SetCluster(node, ClusterA)
else 会产生环
Partitioner->>Partitioner: 不合并
end
end
end
end
Partitioner->>Partitioner: 更新 node_2_cluster_ 映射
Partitioner-->>Partitioner: 合并完成
4.6 子图构建流程
sequenceDiagram
participant Partitioner as DynamicShapePartitioner
participant Cluster as DynamicShapeCluster
participant Subgraph as ComputeGraph<br/>(子图)
participant PartitionedCall as PartitionedCall<br/>(调用算子)
participant Data as Data算子
participant NetOutput as NetOutput算子
Partitioner->>Cluster: BuildPartitionFrame()
Cluster->>Cluster: 创建子图名称
Cluster->>Subgraph: new ComputeGraph(subgraph_name)
Cluster->>Cluster: 处理输入锚点
loop 每个输入锚点
Cluster->>Data: new Data算子
Cluster->>Subgraph: AddNode(Data)
Cluster->>Cluster: inputs_index_[anchor] = index
end
Cluster->>Cluster: 处理输出锚点
loop 每个输出锚点
Cluster->>NetOutput: new NetOutput算子
Cluster->>Subgraph: AddNode(NetOutput)
Cluster->>Cluster: outputs_index_[anchor] = index
end
Cluster->>Cluster: 添加原节点到子图
loop 每个原节点
Cluster->>Subgraph: AddNode(node)
end
Cluster->>Cluster: 连接 Data 到原节点
Cluster->>Cluster: 连接原节点到 NetOutput
Cluster->>PartitionedCall: new PartitionedCall算子
Note right of PartitionedCall: 输入/输出锚点与 Cluster 一致
Cluster->>PartitionedCall: SetAttr("_subgraph", Subgraph)
Cluster->>Cluster: SetUnknownAttr()
alt 动态 Cluster
Cluster->>Subgraph: SetAttr(ATTR_MEMORY_DISCONTIGUOUS_ALLOCATION, true)
Cluster->>Subgraph: SetGraphUnknownFlag(true)
else 静态 Cluster
Cluster->>Subgraph: SetGraphUnknownFlag(false)
end
Cluster-->>Partitioner: 子图构建完成
Partitioner->>Partitioner: CombinePartitionFrame()
Note right of Partitioner: 连接 PartitionedCall 到父图<br/>替换原 Cluster 的节点
五、关键设计决策
5.1 为什么用 Cluster 作为拆分单元?
决策:用 Cluster(一组节点)作为拆分的基本单元,而不是单个节点。
替代方案:
- 节点级别拆分:每个节点一个子图,调度开销大
- 算子类型拆分:按算子类型拆分,无法处理混合场景
- 用户手动拆分:用户负担重,破坏可移植性
权衡分析:
- Cluster 可以最大化子图大小,减少调度开销
- Cluster 内部可以做跨算子优化(如算子融合、内存复用)
- Cluster 的合并逻辑可以动态调整,适应不同场景
- 代价是 Cluster 的合并逻辑复杂,需要处理环检测、拓扑排序等
代码依据:base_cluster.h:76-191
5.2 为什么动态 Cluster 合并要”合并路径”?
决策:合并动态 Cluster 时,不是简单合并两个 Cluster,而是合并路径上的所有 Cluster。
示例:
1 | |
合并 ClusterA 和 ClusterD 时,会合并 ClusterB 和 ClusterC。
设计动机:
- 动态 Cluster 之间的静态 Cluster 如果不合并,会导致数据流断裂
- 静态 Cluster 在动态路径上无法独立执行(需要动态 shape 输入)
- 合并路径保证了数据流的连续性
代码依据:dynamic_shape_partition.cc:687-721(MergeClustersUnknownShape)
5.3 为什么静态 Cluster 合并要”尝试合并”?
决策:合并静态 Cluster 时,使用 TryMerge,检查是否会产生环。
设计动机:
- 静态 Cluster 合并可能产生环(如 A->B->C,合并 A 和 C 会产生环)
- 环会导致拓扑排序失败,无法构建子图
TryMerge通过检查min_和max_来判断是否会产生环
环检测逻辑:
1 | |
代码依据:base_cluster.cc:TryMerge
5.4 为什么小静态 Cluster 要变为动态 Cluster?
决策:静态 Cluster 的节点数小于阈值(默认 4)时,强制变为动态 Cluster。
设计动机:
- 小静态子图会占用流资源(每个子图需要一个流)
- 小静态子图会导致动态子图碎片化(动态子图被分割成多段)
- 合并到动态子图可以保证动态子图的连续性
权衡分析:
- 优点:减少流资源占用,保证动态子图连续性
- 缺点:静态算子失去静态优化机会
- 阈值可配置(
OPTION_STATIC_MODEL_OPS_LOWER_LIMIT),用户可以调整
代码依据:dynamic_shape_partition.cc:774-791(ChangeSmallClusterType)
5.5 为什么支持 No Tiling 机制?
决策:动态 shape 算子如果支持 No Tiling,可以不走 Tiling 流程,直接执行。
设计动机:
- Tiling 流程需要Host 计算,开销大
- 某些动态算子(如
MemcpyAsync)不需要 Tiling,可以直接执行 - No Tiling 可以减少 Host 开销,提升性能
No Tiling 的条件:
- 算子引擎支持 Tiling Inline(在 Device 上执行 Tiling)
- 算子引擎支持 Export Shape(执行后更新 shape)
- 输入节点的引擎也支持 Export Shape
- Shape Range 有效(max shape >= 0)
代码依据:dynamic_shape_partition.cc:1018-1099(IsNodeSupportNoTiling)
六、模块间协作关系
6.1 协作模式分析
- ComputeGraph:原图,包含所有节点和连接关系
- Cluster:拆分的基本单元,一组节点
- PartitionedCall:拆分后的产物,调用子图的算子
- Subgraph:拆分后的子图,包含 Cluster 内的节点
- PartitionerPass:二次拆分优化,如
DynamicDataFlowPartitionerPass
七、业界对比与设计洞察
7.1 与其他框架的动态图处理对比
| 框架 | 动态图处理方案 | 设计哲学 | 优缺点 |
|---|---|---|---|
| TensorFlow | 动态 shape 算子走动态执行,静态算子走静态执行 | 算子级别动态调度 | 优点:简单;缺点:调度开销大 |
| PyTorch | JIT 编译时重新推导 shape | 动态推导 | 优点:灵活;缺点:编译开销大 |
| ONNX Runtime | 动态 shape 算子走动态执行路径 | 算子级别动态调度 | 优点:跨框架;缺点:无法做跨算子优化 |
| GE | 动态图拆分 + Cluster 合并 | 子图级别动态调度 | 优点:最大化静态优化;缺点:拆分逻辑复杂 |
7.2 GE 的独特之处
- Cluster 抽象:用 Cluster 作为拆分单元,而不是节点
- 路径合并:动态 Cluster 合并时合并路径,保证数据流连续性
- No Tiling 机制:支持动态算子不走 Tiling,减少 Host 开销
- 二次拆分优化:通过 PartitionerPass 进行二次优化
7.3 如果重新设计,可能的改进方向
引入更智能的合并策略:
- 当前合并策略基于拓扑排序和类型,可以引入性能预估模型
- 根据算子执行时间、内存占用等预估子图性能,优化合并策略
支持跨子图优化:
- 当前子图内部可以做优化,但跨子图无法优化
- 可以引入跨子图算子融合,如将 PartitionedCall 前后的算子融合
增强 No Tiling 支持:
- 当前 No Tiling 只支持部分算子,可以扩展到更多算子
- 可以引入自动 No Tiling 判断,根据算子特性自动决定是否走 No Tiling
支持动态子图缓存:
- 动态子图的执行开销大,可以引入子图缓存机制
- 根据 shape range 缓存不同 shape 的子图执行结果
八、亮点与问题
8.1 亮点
- 自动化程度高:用户无需关心拆分细节,系统自动适配
- Cluster 抽象精妙:用 Cluster 作为拆分单元,最大化子图大小
- 路径合并保证连续性:动态 Cluster 合并时合并路径,避免数据流断裂
- No Tiling 机制创新:支持动态算子不走 Tiling,减少 Host 开销
- 二次拆分优化:通过 PartitionerPass 进行二次优化,提升性能
8.2 问题
- 合并逻辑复杂:多种合并策略(前向、后向、控制流),理解难度大
- 环检测开销大:
TryMerge需要检查拓扑范围,大规模图时开销大 - 阈值配置不灵活:静态子图阈值固定,无法根据场景动态调整
- No Tiling 支持有限:只支持部分算子,无法覆盖所有动态算子
- 缺少性能预估:合并策略基于拓扑,缺少性能预估模型
九、总结与启发
9.1 核心启发
- Cluster 是拆分的基本单元:用 Cluster 而不是节点,最大化子图大小
- 路径合并保证连续性:动态 Cluster 合并时合并路径,避免数据流断裂
- No Tiling 减少开销:支持动态算子不走 Tiling,减少 Host 开销
- 二次优化提升性能:通过 PartitionerPass 进行二次优化,提升性能
9.2 适用场景
- 需要处理动态 shape 算子的框架
- 需要最大化静态算子优化的系统
- 需要减少动态算子调度开销的场景
十、调用入口汇总
| 调用位置 | 文件路径 | 调用场景 |
|---|---|---|
| 图预处理 | compiler/graph/preprocess/graph_prepare.cc |
图编译前的预处理阶段 |
| 图管理器 | compiler/graph/manager/graph_manager.cc |
图管理器统一入口 |
| FE 图优化 | compiler/engines/nn_engine/optimizer/graph_optimizer/fe_graph_optimizer.cc |
FE 图优化器 |
十一、No Tiling 机制详解
11.1 No Tiling 的核心思想
问题:动态 shape 算子通常需要 Tiling 流程(Host 计算 shape,Device 执行算子),开销大。
解决方案:如果算子支持 No Tiling,可以:
- 在 Device 上执行 Tiling(Tiling Inline)
- 执行后更新 shape(Export Shape)
- 减少 Host 开销,提升性能
11.2 No Tiling 的判断流程
flowchart TD
Start([检查算子是否支持 No Tiling]) --> CheckSystem{系统算子?}
CheckSystem -->|MemcpyAsync/StreamMerge/PartitionedCall| Support[支持 No Tiling]
CheckSystem -->|其他| CheckAttr{检查 ATTR_OP_NO_TILING}
CheckAttr -->|已标记| Support
CheckAttr -->|未标记| CheckEngine{检查引擎支持}
CheckEngine -->|Data/NetOutput| Skip[不需要 No Tiling]
CheckEngine -->|其他| CheckTilingInline{引擎支持 Tiling Inline?}
CheckTilingInline -->|不支持| NotSupport[不支持 No Tiling]
CheckTilingInline -->|支持| CheckExportShape{引擎支持 Export Shape?}
CheckExportShape -->|不支持| NotSupport
CheckExportShape -->|支持| CheckShapeRange{检查 Shape Range}
CheckShapeRange -->|无 Shape Range| CheckUnknownDim{检查 Unknown Dim}
CheckShapeRange -->|有 Shape Range| ParseRange[解析 Shape Range]
CheckUnknownDim -->|Unknown Dim Num=-2| NotSupport
CheckUnknownDim -->|其他| CheckRangeValid{Shape Range 有效?}
CheckRangeValid -->|无效| NotSupport
CheckRangeValid -->|有效| CheckMaxShape{max shape 是否非负?}
CheckMaxShape -->|有 -1| NotSupport
CheckMaxShape -->|全部非负| CheckInputNode{检查输入节点}
ParseRange --> CheckInputNode
CheckInputNode -->|输入节点不支持 Export Shape| NotSupport
CheckInputNode -->|输入节点支持 Export Shape| Support
Support --> MarkNoTiling[标记 ATTR_OP_NO_TILING=true<br/>标记 ATTR_TENSOR_NO_TILING_MEM_TYPE=true]
NotSupport --> MarkNormal[标记 ATTR_OP_NO_TILING=false]
Skip --> End
MarkNoTiling --> End([判断完成])
MarkNormal --> End
style Start fill:#e1f5e1
style End fill:#ffe1e1
style Support fill:#e1f5e1
style NotSupport fill:#ffe1e1
11.3 No Tiling 的实现细节
关键属性:
ATTR_NAME_OP_NO_TILING:算子是否支持 No TilingATTR_NAME_TENSOR_NO_TILING_MEM_TYPE:Tensor 是否走 No Tiling 内存类型ATTR_NAME_OP_MAX_SHAPE:算子的最大 shape(用于 shape range)ATTR_NAME_TENSOR_MAX_SHAPE:Tensor 的最大 shape
执行流程:
- 编译时标记 No Tiling 属性
- 执行时,如果算子支持 No Tiling:
- 不走 Host Tiling 流程
- 在 Device 上执行 Tiling Inline
- 执行后更新 shape(Export Shape)
代码依据:dynamic_shape_partition.cc:1119-1165(MarkOpNoTiling)
十二、控制流 Cluster 的特殊处理
12.1 控制流算子的特殊性
控制流算子:Case、While、If、PartitionedCall 等,包含子图。
特殊性:
- 控制流算子的子图可能包含动态节点
- 控制流算子本身可能是静态的(子图全静态)
- 控制流算子的合并需要特殊处理
12.2 控制流 Cluster 的合并逻辑
sequenceDiagram
participant Partitioner as DynamicShapePartitioner
participant ControlNodes as control_nodes_<br/>(控制流节点分组)
participant ClusterA as Cluster A
participant ClusterB as Cluster B
Partitioner->>ControlNodes: 遍历 control_nodes_
Note right of ControlNodes: 按 group_index 分组<br/>同一组的控制流节点
loop 每个控制流节点组
Partitioner->>ControlNodes: 获取控制流节点列表
loop 每对控制流节点
Partitioner->>ClusterA: GetCluster(node_front)
Partitioner->>ClusterB: GetCluster(node_back)
alt ClusterA == ClusterB
Partitioner->>Partitioner: 跳过(已在同一 Cluster)
else ClusterA != ClusterB
Partitioner->>ClusterB: MergeAllPathFrom(ClusterA)
Note right of ClusterB: 合并从 ClusterA 到 ClusterB<br/>路径上的所有 Cluster
loop 每个路径 Cluster
Partitioner->>Partitioner: SetCluster(node, ClusterB)
end
end
end
end
Partitioner-->>Partitioner: 控制流 Cluster 合并完成
设计动机:
- 控制流节点需要在同一 Cluster,保证控制流的正确性
- 同一
group_index的控制流节点属于同一控制流结构(如 Case 的多个分支) - 合并路径保证控制流 Cluster 的连续性
代码依据:dynamic_shape_partition.cc:180-201(MergeClustersControlFlow)
分析日期:2026-05-07
分析工具:repo-analyzer skill
代码版本:GE trunk_ai/ge