GE 图引擎动态图拆分模块分析

GE 图引擎动态图拆分模块分析

一、问题背景:为什么需要动态图拆分?

GE 图引擎面临一个根本性的架构挑战:如何在一张图中同时处理静态 shape 算子和动态 shape 算子?

1.1 具体场景

典型问题场景

  • 用户模型中包含动态 shape 算子(如 ReshapeBroadcast),其输入/输出 shape 在编译时未知(dim=-1)
  • 同时模型中也有大量静态 shape 算子(如 Conv2DMatMul),其 shape 在编译时已确定
  • 如果不拆分,整个图会被标记为”动态图”,导致:
    • 静态算子无法享受静态图优化(如内存复用、算子融合)
    • 动态算子无法享受动态图特性(如 shape 推导、流式执行)
    • 整体性能严重下降

1.2 现有方案的不足

方案 问题
全静态图 动态算子无法执行,shape 推导失败
全动态图 静态算子性能损失,无法做深度优化
用户手动拆分 用户负担重,破坏模型可移植性
算子级别动态调度 调度开销大,无法做跨算子优化

1.3 GE 的解决方案

设计一套自动化的动态图拆分机制,将一张混合图拆分成多个子图:

  • 静态子图:包含静态 shape 算子,享受静态图优化
  • 动态子图:包含动态 shape 算子,享受动态图特性
  • 自动连接:通过 PartitionedCall 算子自动连接子图,保证数据流正确

核心价值:用户无需关心拆分细节,系统自动适配,既保证兼容性,又最大化性能。


二、设计哲学:动静分离,最优调度

GE 的动态图拆分遵循一个清晰的设计哲学:**”静态算子走静态路,动态算子走动态路,自动连接不丢数据”**。

这个哲学体现在三个核心原则:

2.1 原则一:最小化动态子图

动机:动态子图的调度开销大,应尽量减少。

实现

  • 只将真正需要动态 shape 的算子放入动态子图
  • 静态算子尽量合并到静态子图,即使与动态算子相邻
  • 小静态子图(<阈值)会被合并到动态子图,避免碎片化

代码依据dynamic_shape_partition.cc:774-791ChangeSmallClusterType

2.2 原则二:最大化静态子图优化

动机:静态子图可以做深度优化(内存复用、算子融合),应尽量放大。

实现

  • 静态算子优先合并到静态子图
  • 静态子图可以跨多个动态算子(通过 PartitionedCall 连接)
  • 静态子图内部可以做完整的图优化

代码依据dynamic_shape_partition.cc:912-963MergeClustersNormal

2.3 原则三:保证数据流正确性

动机:拆分后数据流不能断,必须保证输入输出正确。

实现

  • 每个 Cluster 对应一个 PartitionedCall 算子
  • PartitionedCall 的输入/输出锚点与原 Cluster 的输入/输出锚点一致
  • 子图内部通过 DataNetOutput 算子连接

代码依据base_cluster.cc:BuildFrameCombinePartitionFrame


三、核心数据结构

3.1 Cluster:拆分的基本单元

1
2
3
4
5
6
7
8
9
10
11
class BaseCluster : public std::enable_shared_from_this<BaseCluster> {
int32_t type_index_; // Cluster 类型(DATA/KNOWN_SHAPE/UNKNOWN_SHAPE/NETOUTPUT)
size_t id_; // Cluster ID(拓扑排序位置)
size_t min_; // Cluster 内节点的最小拓扑序
size_t max_; // Cluster 内节点的最大拓扑序
std::vector<BaseCluster *> in_clusters_; // 输入 Cluster
std::vector<BaseCluster *> out_clusters_; // 输出 Cluster
std::vector<NodePtr> nodes_; // Cluster 包含的节点
ComputeGraphPtr subgraph_; // 对应的子图
NodePtr partition_node_; // 对应的 PartitionedCall 算子
};

设计亮点

  • min_max_ 记录拓扑范围,用于判断是否可以合并(避免环)
  • in_clusters_out_clusters_ 记录 Cluster 间的连接关系
  • subgraph_partition_node_ 是拆分后的产物

3.2 DynamicShapeCluster:动态图拆分的特化

1
2
3
4
5
6
class DynamicShapeCluster : public BaseCluster {
bool IsKnownShape() const; // 是否为静态 Cluster
bool IsUnknownShape() const; // 是否为动态 Cluster
Status SetUnknownAttr(); // 设置动态属性(内存不连续等)
void Merge(std::shared_ptr<BaseCluster> other) override; // 合并时更新类型
};

关键行为

  • 合并时,如果被合并的 Cluster 是动态类型,当前 Cluster 也会变为动态类型
  • 动态 Cluster 的子图会被标记为”内存不连续分配”(ATTR_NAME_MEMORY_DISCONTIGUOUS_ALLOCATION

3.3 DynamicShapePartitioner:拆分的主控制器

1
2
3
4
5
6
7
class DynamicShapePartitioner : public BasePartitioner {
std::unordered_set<NodePtr> known_shape_nodes_; // 静态节点集合
std::unordered_set<NodePtr> unknown_shape_nodes_; // 动态节点集合
std::unordered_set<NodePtr> unknown_shape_no_tiling_nodes_; // 支持 No Tiling 的动态节点
int64_t static_model_ops_lower_limit_; // 静态子图最小算子数阈值
bool merge_known_first_; // 是否优先合并静态 Cluster
};

核心职责

  • 标记动态节点(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(一组节点)作为拆分的基本单元,而不是单个节点。

替代方案

  1. 节点级别拆分:每个节点一个子图,调度开销大
  2. 算子类型拆分:按算子类型拆分,无法处理混合场景
  3. 用户手动拆分:用户负担重,破坏可移植性

权衡分析

  • Cluster 可以最大化子图大小,减少调度开销
  • Cluster 内部可以做跨算子优化(如算子融合、内存复用)
  • Cluster 的合并逻辑可以动态调整,适应不同场景
  • 代价是 Cluster 的合并逻辑复杂,需要处理环检测、拓扑排序等

代码依据base_cluster.h:76-191

5.2 为什么动态 Cluster 合并要”合并路径”?

决策:合并动态 Cluster 时,不是简单合并两个 Cluster,而是合并路径上的所有 Cluster。

示例

1
ClusterA (动态) -> ClusterB (静态) -> ClusterC (静态) -> ClusterD (动态)

合并 ClusterA 和 ClusterD 时,会合并 ClusterB 和 ClusterC。

设计动机

  • 动态 Cluster 之间的静态 Cluster 如果不合并,会导致数据流断裂
  • 静态 Cluster 在动态路径上无法独立执行(需要动态 shape 输入)
  • 合并路径保证了数据流的连续性

代码依据dynamic_shape_partition.cc:687-721MergeClustersUnknownShape

5.3 为什么静态 Cluster 合并要”尝试合并”?

决策:合并静态 Cluster 时,使用 TryMerge,检查是否会产生环。

设计动机

  • 静态 Cluster 合并可能产生环(如 A->B->C,合并 A 和 C 会产生环)
  • 环会导致拓扑排序失败,无法构建子图
  • TryMerge 通过检查 min_max_ 来判断是否会产生环

环检测逻辑

1
2
3
4
5
6
7
8
bool TryMerge(std::shared_ptr<BaseCluster> other) {
// 如果 other 的拓扑范围在当前 Cluster 内,会产生环
if ((other->MinId() >= min_) && (other->MaxId() <= max_)) {
return false; // 会产生环,不合并
}
Merge(other);
return true;
}

代码依据base_cluster.cc:TryMerge

5.4 为什么小静态 Cluster 要变为动态 Cluster?

决策:静态 Cluster 的节点数小于阈值(默认 4)时,强制变为动态 Cluster。

设计动机

  • 小静态子图会占用流资源(每个子图需要一个流)
  • 小静态子图会导致动态子图碎片化(动态子图被分割成多段)
  • 合并到动态子图可以保证动态子图的连续性

权衡分析

  • 优点:减少流资源占用,保证动态子图连续性
  • 缺点:静态算子失去静态优化机会
  • 阈值可配置(OPTION_STATIC_MODEL_OPS_LOWER_LIMIT),用户可以调整

代码依据dynamic_shape_partition.cc:774-791ChangeSmallClusterType

5.5 为什么支持 No Tiling 机制?

决策:动态 shape 算子如果支持 No Tiling,可以不走 Tiling 流程,直接执行。

设计动机

  • Tiling 流程需要Host 计算,开销大
  • 某些动态算子(如 MemcpyAsync)不需要 Tiling,可以直接执行
  • No Tiling 可以减少 Host 开销,提升性能

No Tiling 的条件

  1. 算子引擎支持 Tiling Inline(在 Device 上执行 Tiling)
  2. 算子引擎支持 Export Shape(执行后更新 shape)
  3. 输入节点的引擎也支持 Export Shape
  4. Shape Range 有效(max shape >= 0)

代码依据dynamic_shape_partition.cc:1018-1099IsNodeSupportNoTiling


六、模块间协作关系

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 如果重新设计,可能的改进方向

  1. 引入更智能的合并策略

    • 当前合并策略基于拓扑排序和类型,可以引入性能预估模型
    • 根据算子执行时间、内存占用等预估子图性能,优化合并策略
  2. 支持跨子图优化

    • 当前子图内部可以做优化,但跨子图无法优化
    • 可以引入跨子图算子融合,如将 PartitionedCall 前后的算子融合
  3. 增强 No Tiling 支持

    • 当前 No Tiling 只支持部分算子,可以扩展到更多算子
    • 可以引入自动 No Tiling 判断,根据算子特性自动决定是否走 No Tiling
  4. 支持动态子图缓存

    • 动态子图的执行开销大,可以引入子图缓存机制
    • 根据 shape range 缓存不同 shape 的子图执行结果

八、亮点与问题

8.1 亮点

  1. 自动化程度高:用户无需关心拆分细节,系统自动适配
  2. Cluster 抽象精妙:用 Cluster 作为拆分单元,最大化子图大小
  3. 路径合并保证连续性:动态 Cluster 合并时合并路径,避免数据流断裂
  4. No Tiling 机制创新:支持动态算子不走 Tiling,减少 Host 开销
  5. 二次拆分优化:通过 PartitionerPass 进行二次优化,提升性能

8.2 问题

  1. 合并逻辑复杂:多种合并策略(前向、后向、控制流),理解难度大
  2. 环检测开销大TryMerge 需要检查拓扑范围,大规模图时开销大
  3. 阈值配置不灵活:静态子图阈值固定,无法根据场景动态调整
  4. No Tiling 支持有限:只支持部分算子,无法覆盖所有动态算子
  5. 缺少性能预估:合并策略基于拓扑,缺少性能预估模型

九、总结与启发

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,可以:

  1. 在 Device 上执行 Tiling(Tiling Inline)
  2. 执行后更新 shape(Export Shape)
  3. 减少 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 Tiling
  • ATTR_NAME_TENSOR_NO_TILING_MEM_TYPE:Tensor 是否走 No Tiling 内存类型
  • ATTR_NAME_OP_MAX_SHAPE:算子的最大 shape(用于 shape range)
  • ATTR_NAME_TENSOR_MAX_SHAPE:Tensor 的最大 shape

执行流程

  1. 编译时标记 No Tiling 属性
  2. 执行时,如果算子支持 No Tiling:
    • 不走 Host Tiling 流程
    • 在 Device 上执行 Tiling Inline
    • 执行后更新 shape(Export Shape)

代码依据dynamic_shape_partition.cc:1119-1165MarkOpNoTiling


十二、控制流 Cluster 的特殊处理

12.1 控制流算子的特殊性

控制流算子CaseWhileIfPartitionedCall 等,包含子图。

特殊性

  • 控制流算子的子图可能包含动态节点
  • 控制流算子本身可能是静态的(子图全静态)
  • 控制流算子的合并需要特殊处理

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-201MergeClustersControlFlow


分析日期:2026-05-07
分析工具:repo-analyzer skill
代码版本:GE trunk_ai/ge


GE 图引擎动态图拆分模块分析
https://orion-wyc.github.io/2026/05/07/2026-05-07-GE图引擎动态图拆分模块深度架构分析/
作者
Yuchen
发布于
2026年5月7日
许可协议