犀牛鸟 2026 研究笔记 estelledc.github.io

Ch21: 图优化——编译器思维在推理引擎中的应用

Part 4: 核心抽象深度对比 前置章节:Ch20: 后端组织——如何管理多个硬件加速器 后续章节:Ch22: 横向总结——六个引擎的设计取舍全景图


核心问题

当你用 PyTorch 训练完一个模型并导出为 ONNX 或其他格式时,导出的计算图通常不是最优的。训练框架在导出时倾向于保持语义清晰——每个 Python 函数调用对应一个独立的算子节点,中间变量保留完整的计算过程。但这种”语义清晰”在推理时就变成了”冗余低效”。

举一个最常见的例子:训练框架导出的图中,一个 Conv2d 后面紧跟着一个 BatchNorm。在训练时它们必须分开(BN 层需要更新 running mean/var),但在推理时 BatchNorm 的参数已经冻结——它的计算可以数学上等价地合并到前面的 Conv2d 中,把两次访存和两次计算变成一次。这就是图优化中最经典的 Conv+BN 融合。

图优化本质上是编译器优化在推理引擎中的映射——就像 GCC 对 C 代码做常量折叠、死代码消除、循环展开一样,推理引擎对计算图做算子融合、常量折叠、布局转换。六个框架在图优化上的投入程度差异悬殊——从 ncnn 的”几乎不做”到 Paddle-Lite 的”50+ 轮 pass 优化”。


装修施工队的类比

想象你请了装修队来改造房子,装修设计图就是”计算图”,图优化就是施工前的”方案优化”:

ncnn 像一个经验丰富的老师傅——他不看设计图,到了现场凭经验直接动手。遇到”先刷底漆再刷面漆”他知道可以用二合一漆,但这种优化全靠他个人经验,没有系统化的流程。ncnn 的图优化极简——只在模型转换工具(ncnnoptimize)中做少量融合,推理时不做图级优化。

MNN 像一个带有 3D 建模能力的设计师——他不直接优化原始设计图,而是在另一个维度上做文章。他的 Geometry Compiler 把复杂操作分解成基本操作的组合,然后在基本操作层面做优化(比如两次连续的 Reshape 合并成一次)。这不是传统的”图优化”,而是一种”降维优化”。

ORT 像一个专业的建筑设计事务所——有一套标准化的优化流程。GraphTransformer 框架允许不同团队(不同 EP)注册自己的优化规则,事务所按阶段(Level 1 基础优化 → Level 2 扩展优化 → Level 3 后端特定优化)依次执行。新的优化规则可以随时插入而不影响现有流程。

TNN 像一个注重细节的工长——有 NetOptimizer 做常规优化(算子融合),还有 ConstFolder 做常量折叠(把编译时就能算出来的结果提前算好,施工时直接用成品)。不算特别系统但覆盖了主要的优化场景。

Paddle-Lite 像一个全自动化的智能装修系统——50+ 轮 MIR pass 覆盖了从类型转换、算子融合、量化调整到内存规划的所有方面。每一轮 pass 都有明确的职责和顺序依赖。系统性最强,但也最复杂——pass 之间的交互效应难以完全预测。

MACE 像一个极简主义者——他认为好的设计不需要后期优化,所以几乎不做独立的图优化。MACE 的”优化”主要发生在模型转换阶段(外部工具 mace_converter),推理时直接执行转换后的图。


ncnn:极简的模型转换时优化

ncnn 的图优化理念是:尽可能少做,但做对。它把优化工作主要放在离线的模型转换工具(ncnnoptimize)中,而不是推理引擎内部。

ncnnoptimize:离线优化工具

# ncnn 的图优化发生在模型转换阶段
# 用户手动运行 ncnnoptimize 工具
./ncnnoptimize input.param input.bin output.param output.bin 1
# 最后的 1 表示开启 fp16 存储(减小模型体积)

ncnnoptimize 做的优化包括:

// ncnn/tools/ncnnoptimize.cpp(简化版关键优化列表)
void NetOptimize::optimize() {
    // 1. Conv + BN 融合
    fuse_convolution_batchnorm();
    // 把 BatchNorm 的参数合并到 Convolution 的权重中
    // weight_new = weight * (gamma / sqrt(var + eps))
    // bias_new = (bias - mean) * gamma / sqrt(var + eps) + beta

    // 2. Conv + ReLU 融合
    fuse_convolution_activation();
    // Convolution 自带 activation_type 参数
    // 设置 activation_type = 1 (ReLU) 就等于融合了后面的 ReLU

    // 3. 消除冗余操作
    eliminate_noop();           // 删除空操作
    eliminate_flatten_after_global_pooling();
    eliminate_reshape_after_global_pooling();
    eliminate_dropout();        // 推理时 Dropout 是恒等操作

    // 4. 常量折叠(简单版)
    // 如果某个算子的所有输入都是常量,提前计算结果
}

运行时无图优化

ncnn 的推理引擎内部不做任何图级优化——加载模型后直接按图结构执行。这是一个有意的设计决策:ncnn 的定位是极致轻量,图优化的代码会增加二进制体积和初始化耗时,而离线优化工具已经覆盖了主要的优化场景。

// ncnn 加载模型后直接执行,不做运行时优化
int Net::load_param(const char* protopath) {
    // 读取算子参数
    // 不做图变换、不做算子融合
    // 直接按原始顺序存储 Layer
}

Conv 内部的 activation 融合

ncnn 的一种”隐式优化”是在 Convolution 算子内部支持 activation 融合——通过 activation_type 参数,Convolution 可以在计算完成后立即执行 ReLU/ReLU6/Sigmoid 等激活函数,避免单独的激活层带来的额外访存:

// ncnn/src/layer/arm/convolution_arm.cpp(简化版)
int Convolution_arm::forward(...) const {
    // ... 卷积计算 ...

    // 内置激活融合
    if (activation_type == 1) {
        // ReLU:在卷积结果上直接执行,无需额外的内存访问
        for (int i = 0; i < output_size; i++) {
            output[i] = std::max(output[i], 0.f);
        }
    } else if (activation_type == 2) {
        // ReLU6
        for (int i = 0; i < output_size; i++) {
            output[i] = std::min(std::max(output[i], 0.f), 6.f);
        }
    }
    // ... 更多激活类型 ...
}

设计取舍

ncnn 的图优化策略是”够用就好”——离线工具做 Conv+BN 融合和 ReLU 融合这两个收益最大的优化,运行时不做任何图变换。好处是引擎极简,加载即执行。坏处是错过了一些运行时才能决定的优化(比如根据实际输入形状选择最优的融合策略)。对于 ncnn 的核心场景(小模型、CPU 推理),这种”够用就好”的策略是合适的。


MNN:Geometry Compiler 的降维优化

MNN 的图优化不走传统的”在原始图上做 pass 变换”路线,而是通过 Geometry Compiler 把复杂算子降级为基本算子组合,然后在基本算子层面做优化。

传统图优化 vs Geometry Compiler

传统图优化(ORT/Paddle-Lite):
  原始图 -> [Pass1: Conv+BN融合] -> [Pass2: Conv+ReLU融合] -> ... -> 优化后的图
  每个 Pass 需要精确匹配模式:
    "如果看到 Conv -> BN -> ReLU 的连续模式,替换为 FusedConvBNReLU"

MNN Geometry Compiler:
  原始图中的复杂算子 -> 降级为基本算子组合 -> 在基本算子层面自然融合
  不需要为每种组合写专门的匹配模式

降级过程中的零拷贝优化

Geometry Compiler 降级时会大量使用 MEMORY_VIRTUAL 张量(Ch17 讲过),这些虚拟张量不占用实际内存,只是通过 Region 描述数据映射关系:

// 例:Permute (Transpose) 的 Geometry 实现
class GeometryPermute : public GeometryComputer {
    bool onCompute(const Op* op,
                   const std::vector<Tensor*>& inputs,
                   const std::vector<Tensor*>& outputs,
                   Context& context,
                   CommandBuffer& buffer) const override {
        auto input = inputs[0];
        auto output = outputs[0];
        auto perm = op->main_as_Permute()->dims();

        // 输出标记为 MEMORY_VIRTUAL
        auto des = TensorUtils::getDescribe(output);
        des->memoryType = InsideDescribe::MEMORY_VIRTUAL;

        // 创建 Region 描述映射关系
        InsideDescribe::Region region;
        region.origin = input;
        region.offset = 0;

        // 根据 perm 参数设置 stride 映射
        // 这样 Permute 就变成了"零拷贝的视图变换"
        for (int i = 0; i < perm->size(); i++) {
            region.size[i] = input->length(perm->Get(i));
            region.src_stride[i] = input->stride(perm->Get(i));
            region.dst_stride[i] = output->stride(i);
        }

        des->regions.push_back(region);
        return true;
    }
};

Region 合并优化

当多个连续的 Reshape/Permute/Slice 操作被降级后,它们的 Region 描述可以合并——多次逻辑变换被压缩成一次物理数据搬运:

原始图:Tensor -> Reshape -> Permute -> Reshape -> MatMul

降级后:
  Reshape1: MEMORY_VIRTUAL, Region{src_stride=[...], dst_stride=[...]}
  Permute:  MEMORY_VIRTUAL, Region{src_stride=[...], dst_stride=[...]}
  Reshape2: MEMORY_VIRTUAL, Region{src_stride=[...], dst_stride=[...]}

Region 合并后:
  三个 Region 合并成一个 Region
  从原始 Tensor 直接到 MatMul 输入的映射
  中间三个操作的内存开销 = 0

这种优化对 Transformer 模型尤其有效——Transformer 中的 Multi-Head Attention 包含大量的 Reshape 和 Permute 操作(把 [B, L, D] 变换为 [B, H, L, D/H] 再转置),这些操作在 Geometry Compiler 下几乎零开销。

设计取舍

MNN 的 Geometry Compiler 不是传统意义上的图优化——它不直接修改原始图结构,而是在执行时通过降级和 Region 合并来消除冗余。好处是不需要为每种算子组合写专门的融合规则(ncnn 的 Conv+BN 融合需要专门的代码),降级机制自然地处理了各种组合情况。坏处是降级后的基本算子组合比手写的融合 kernel 慢——如果你有一个高性能的 FusedConvBNReLU kernel,直接用它比把 Conv、BN、ReLU 拆开再分别执行要快。MNN 的策略是:对性能关键的组合提供专用 Execution,其余通过 Geometry Compiler 兜底。


ONNX Runtime:GraphTransformer 体系化优化

ORT 的图优化是六个框架中最”体系化”的——通过 GraphTransformer 框架提供了三个层级的优化,每个层级包含多个可插拔的优化规则。

三个优化层级

// onnxruntime/core/optimizer/graph_transformer_level.h
enum class TransformerLevel {
    Level1,  // 基础优化:常量折叠、死代码消除、基本融合
    Level2,  // 扩展优化:更激进的融合、布局优化
    Level3,  // 后端特定优化:EP 自定义的优化
};

用户可以控制优化级别:

Ort::SessionOptions options;
// 设置优化级别
options.SetGraphOptimizationLevel(
    GraphOptimizationLevel::ORT_ENABLE_EXTENDED  // Level 1 + 2
);

// 或者只启用基础优化
options.SetGraphOptimizationLevel(
    GraphOptimizationLevel::ORT_ENABLE_BASIC     // Level 1 only
);

GraphTransformer 接口

// onnxruntime/core/optimizer/graph_transformer.h(简化版)
class GraphTransformer {
public:
    virtual ~GraphTransformer() = default;

    // 对整个图执行优化
    virtual common::Status Apply(Graph& graph,
                                  bool& modified) const = 0;

    // 优化器名称(用于日志和调试)
    const std::string& Name() const { return name_; }

protected:
    std::string name_;
};

// 基于规则的优化器(匹配模式 + 替换)
class RuleBasedGraphTransformer : public GraphTransformer {
    // 注册优化规则
    void Register(std::unique_ptr<RewriteRule>&& rule);

    // 遍历图中每个节点,尝试应用所有规则
    Status Apply(Graph& graph, bool& modified) const override;
};

关键优化 Pass

ORT 内置了几十个优化 Pass,这里列出最重要的几个:

// Level 1 优化
class ConstantFolding : public GraphTransformer {
    // 常量折叠:如果一个算子的所有输入都是常量,
    // 提前计算结果并替换为常量节点
    // 例:Shape(X) 在形状已知时替换为常量 [1,3,224,224]
};

class CommonSubexpressionElimination : public GraphTransformer {
    // 公共子表达式消除:如果两个算子有相同的输入和参数,
    // 只保留一个,另一个引用同一个输出
};

class ConvBNFusion : public RewriteRule {
    // Conv + BN 融合:数学等价合并
    bool SatisfyCondition(const Graph& graph,
                           const Node& node) const override {
        // 检查:node 是 Conv,唯一下游是 BN,BN 参数是常量
        return node.OpType() == "Conv" &&
               OnlyConsumer(graph, node) &&
               graph.GetNode(node.OutputDef(0)->Name())->OpType()
                   == "BatchNormalization";
    }

    Status Apply(Graph& graph, Node& conv_node,
                  bool& modified) const override {
        // 数学合并:把 BN 参数融合到 Conv 权重中
        // 删除 BN 节点
        modified = true;
        return Status::OK();
    }
};

// Level 2 优化
class MatMulAddFusion : public RewriteRule {
    // MatMul + Add -> Gemm 融合
};

class AttentionFusion : public GraphTransformer {
    // Q*K^T/sqrt(d)*V 模式识别 -> 融合为 MultiHeadAttention 算子
    // 这是 Transformer 模型优化的关键
};

// Level 3 优化(EP 特定)
class CUDAConvActivationFusion : public GraphTransformer {
    // CUDA EP 特有:Conv + ReLU 融合为 cuDNN 的融合 kernel
};

优化结果可视化

ORT 支持把优化后的图保存下来,方便调试和分析:

Ort::SessionOptions options;
// 保存优化后的模型
options.SetOptimizedModelFilePath("optimized_model.onnx");
// 创建 session 时会自动保存优化后的图

设计取舍

ORT 的 GraphTransformer 体系是最标准化的图优化框架——三级层次、可插拔规则、支持保存优化结果。好处是可扩展性极强——第三方 EP 可以注册自己的优化规则,研究人员可以方便地实验新的优化策略。坏处是 pattern matching 的组合爆炸——每增加一种新的算子组合模式就需要写一条新的 RewriteRule,而且规则之间的交互(一条规则的输出可能影响另一条规则的匹配条件)很难完全验证。


TNN:NetOptimizer + ConstFolder

TNN 的图优化介于 ncnn 的极简和 ORT 的体系化之间——有明确的优化器接口但优化覆盖面不如 ORT 广。

NetOptimizer:算子融合

// TNN/source/tnn/optimizer/net_optimizer.h(简化版)
class NetOptimizer {
public:
    virtual std::string Strategy() = 0;

    virtual Status Optimize(NetStructure* structure,
                              NetResource* resource);
};

// 具体的优化器实现
class NetOptimizerFuseConvBN : public NetOptimizer {
    std::string Strategy() override {
        return "fuse_conv_batchnorm";
    }

    Status Optimize(NetStructure* structure,
                      NetResource* resource) override {
        // 遍历图,查找 Conv -> BN 模式
        for (int i = 0; i < structure->layers.size() - 1; i++) {
            auto& curr = structure->layers[i];
            auto& next = structure->layers[i + 1];

            if (curr->type == LAYER_CONVOLUTION &&
                next->type == LAYER_BATCH_NORM) {
                // 合并 BN 参数到 Conv 权重
                FuseConvBN(curr, next, resource);
                // 删除 BN 层
                structure->layers.erase(
                    structure->layers.begin() + i + 1);
            }
        }
        return TNN_OK;
    }
};

ConstFolder:常量折叠

TNN 的 ConstFolder 是一个独立于 NetOptimizer 的优化组件——它在模型加载时检测所有输入都是常量的算子,提前执行这些算子并用结果替换原始节点:

// TNN/source/tnn/optimizer/const_folder.h(简化版)
class ConstFolder {
public:
    Status Fold(NetStructure* structure,
                 NetResource* resource) {
        bool changed = true;
        while (changed) {
            changed = false;
            for (auto& layer : structure->layers) {
                if (AllInputsAreConst(layer, resource)) {
                    // 执行这个算子,得到常量结果
                    auto result = Execute(layer, resource);
                    // 把结果存为新的常量 Blob
                    resource->constant_map[layer->outputs[0]] = result;
                    // 从图中删除这个算子
                    RemoveLayer(structure, layer);
                    changed = true;
                    break;
                }
            }
        }
    }
};

常量折叠是一个迭代过程——折叠一个算子后可能让另一个算子的所有输入都变成常量,所以需要反复执行直到没有新的折叠机会。

优化执行顺序

TNN 的优化在网络初始化(DefaultNetwork::Init)时执行,顺序是:ConstFolder -> NetOptimizer(多个)-> BlobManager 内存分配 -> LayerAcc 创建。

设计取舍

TNN 的图优化”中规中矩”——覆盖了最重要的优化(Conv+BN 融合、常量折叠),但没有 ORT 那样系统化的多级 Pass 框架。优化器的数量和种类比 ncnn 多,但比 Paddle-Lite 少。对于 TNN 的定位(ncnn 的架构翻新版)来说,这种中等复杂度的优化方案是合适的。


Paddle-Lite:50+ MIR Pass 的全覆盖优化

Paddle-Lite 的图优化是六个框架中最”重量级”的——50+ 轮 MIR(Model Intermediate Representation)Pass 覆盖了从类型转换到算子融合到内存规划的所有方面。

MIR 的 SSA 图表示

Paddle-Lite 的优化不是在原始模型格式上做的,而是先把模型转换为 MIR 的 SSA(Static Single Assignment)图表示——这借鉴了编译器中间表示的设计:

// paddle-lite/lite/core/optimizer/mir/ssa_graph.h(简化版)
class SSAGraph {
public:
    // 节点列表(算子节点 + 数据节点)
    std::list<std::unique_ptr<Node>> nodes_;

    // 添加算子节点
    Node* AddInstruction(const cpp::OpDesc& desc);
    // 添加数据节点(变量)
    Node* AddArgument(const std::string& name);
};

Pass 分类

50+ 轮 Pass 可以分为几大类:

void Optimizer::Run(Program* program,
                     const std::vector<Place>& valid_places) {
    // ======= 第一类:类型推导与转换 =======
    RunPass("type_target_cast_pass");    // 设备间转换(CPU<->GPU)
    RunPass("type_precision_cast_pass"); // 精度转换(fp32<->fp16<->int8)
    RunPass("type_layout_cast_pass");    // 布局转换(NCHW<->NHWC)

    // ======= 第二类:算子融合 =======
    RunPass("lite_conv_bn_fuse_pass");
    RunPass("lite_conv_activation_fuse_pass");
    RunPass("lite_fc_fuse_pass");
    RunPass("lite_shuffle_channel_fuse_pass");
    RunPass("lite_transpose_softmax_transpose_fuse_pass");
    RunPass("lite_interpolate_fuse_pass");
    // ... 20+ 种融合模式

    // ======= 第三类:Kernel 选择 =======
    RunPass("static_kernel_pick_pass");
    // 根据三维 Place (Target, Precision, Layout)
    // 为每个 Op 选择最佳 kernel

    // ======= 第四类:子图优化 =======
    RunPass("nnadapter_subgraph_pass");
    // 把支持 NNAdapter 的连续算子合并为子图

    // ======= 第五类:内存优化 =======
    RunPass("memory_optimize_pass");
    // 分析张量生命周期,复用内存

    // ======= 第六类:量化相关 =======
    RunPass("lite_quant_dequant_fuse_pass");
    RunPass("weight_quantization_preprocess_pass");
}

算子融合 Pass 示例

// paddle-lite/lite/core/optimizer/mir/fusion/conv_bn_fuse_pass.cc
class ConvBNFusePass : public ProgramPass {
public:
    void Apply(const std::unique_ptr<SSAGraph>& graph) override {
        // 模式匹配:找 Conv -> BN 的连续模式
        for (auto& node : graph->nodes_) {
            if (!node->IsStmt()) continue;
            auto& stmt = node->AsStmt();

            if (stmt.op_type() == "conv2d") {
                // 检查输出是否只被一个 BN 消费
                auto out = stmt.op_info()->Output("Output").front();
                auto consumers = FindConsumers(graph, out);

                if (consumers.size() == 1 &&
                    consumers[0]->AsStmt().op_type() == "batch_norm") {
                    // 执行融合
                    FuseConvBN(&stmt, &consumers[0]->AsStmt());
                    // 删除 BN 节点
                    GraphSafeRemoveNode(graph, consumers[0]);
                }
            }
        }
    }
};

Pass 顺序的重要性

Paddle-Lite 的 Pass 顺序是精心设计的——某些 Pass 必须在其他 Pass 之前执行:

1. 类型转换 Pass(最先)
   -> 确定每个 Tensor 的 Target/Precision/Layout

2. 算子融合 Pass(中间)
   -> 必须在类型确定后才能判断融合是否安全
   -> 融合后可能改变类型需求

3. Kernel 选择 Pass(在融合后)
   -> 需要知道最终的算子图才能选 kernel

4. 内存优化 Pass(最后)
   -> 需要知道最终的执行顺序才能分析生命周期

设计取舍

Paddle-Lite 的 50+ Pass 是最全面的图优化方案——几乎覆盖了所有已知的优化技术。好处是优化效果最好(理论上)——每种优化都有专门的 Pass 负责。坏处是:Pass 数量太多导致优化耗时长(大模型可能几秒到十几秒);Pass 之间的交互效应复杂(一个 Pass 的修改可能破坏另一个 Pass 的假设);调试困难(50+ 轮变换后出了问题很难定位是哪个 Pass 导致的)。


MACE:无独立图优化

MACE 是六个框架中唯一没有独立的运行时图优化模块的框架。

模型转换时优化

MACE 的优化主要发生在模型转换阶段——外部工具 mace_converter 把 TensorFlow/ONNX/Caffe 模型转换为 MACE 格式时做优化:

# mace/python/tools/converter_tool/transformer.py(简化版)
class Transformer:
    def run(self):
        # 转换时的优化
        self.fold_batchnorm()        # BN 折叠
        self.fold_conv_and_bn()      # Conv+BN 融合
        self.fuse_activation()       # 激活融合
        self.transpose_filters()     # 权重转置(适配 GPU 布局)
        self.add_opencl_informations()  # 添加 OpenCL 执行信息
        self.transform_buffer_to_image()  # 转为 GPU image2d 格式

运行时没有图变换

MACE 的推理引擎在加载模型后直接构建执行 Flow,不做任何图级变换。Flow 中的 Operation 按照模型定义的顺序执行——因为所有能做的优化已经在转换阶段完成了:

// mace/core/flow/common_fp32_flow.cc(简化版)
MaceStatus CommonFp32Flow::Run(TensorMap* input_tensors,
                                TensorMap* output_tensors) {
    // 直接按顺序执行每个 Operation
    // 没有运行时图优化、没有动态融合
    for (auto& op : net_->operators()) {
        MACE_RETURN_IF_ERROR(op->Run());
    }
    return MaceStatus::MACE_SUCCESS;
}

为什么 MACE 不做运行时优化

MACE 的设计哲学是”模型转换时一次性优化到位”。这有几个原因:MACE 的主要加速后端是 OpenCL GPU,GPU kernel 的启动开销使得小算子融合的收益不如 CPU 上明显;MACE 的 mace_converter 是 Python 工具,修改和扩展比 C++ 更方便;MACE 的目标场景是固定模型部署(不像 ORT 那样需要支持任意 ONNX 模型),所以可以在转换时做针对性优化。

设计取舍

MACE 把所有优化放在离线转换阶段,运行时零优化开销。好处是推理引擎极简——加载即执行,没有优化相关的代码和内存开销。坏处是丧失了运行时的优化灵活性——无法根据实际输入数据或运行环境做自适应优化。这种设计适合嵌入式部署场景(模型固定、硬件固定),但不适合需要动态适配的场景。


六框架图优化对比

维度 ncnn MNN ORT TNN Paddle-Lite MACE
优化时机 离线工具 运行时(Geometry) 运行时(Session创建) 运行时(Init) 运行时(Build) 离线转换
优化框架 无(手写函数) Geometry Compiler GraphTransformer NetOptimizer MIR Pass 无(Python脚本)
核心机制 直接修改权重 算子降级+Region合并 Pattern Match+替换 Pattern Match SSA图+50+ Pass 转换脚本
Conv+BN融合 有(离线) 有(降级兜底) 有(RewriteRule) 有(NetOptimizer) 有(Fuse Pass) 有(转换时)
常量折叠 简单版(离线) 通过降级隐式实现 有(ConstantFolding) 有(ConstFolder) 有(专用Pass) 有(转换时)
Attention融合 通过降级部分实现 有(专用规则) 部分 有(专用Pass)
优化级别控制 三级可选
可扩展性 中(写GeometryComputer) 高(注册GraphTransformer) 中(写NetOptimizer) 中高(写MIR Pass) 低(改Python脚本)
优化开销 零(运行时) 低(降级轻量) 中(视级别) 高(50+ Pass) 零(运行时)
Pass数量 ~10(离线) N/A(机制不同) ~30+(三级合计) ~10 50+ ~10(离线)

图优化技术的分类学

前面逐框架分析了具体实现,这里从技术角度归类,帮你建立图优化的知识框架。

第一类:代数等价变换

这类优化不改变计算的数学语义,只是用等价但更高效的方式实现同样的计算。所有六个框架都在某种程度上支持这类优化:

Conv+BN 融合是最典型的例子。BatchNorm 的计算公式是 y = gamma * (x - mean) / sqrt(var + eps) + beta,当 x 是 Conv 的输出时,可以把 gamma、mean、var、eps、beta 合并到 Conv 的 weight 和 bias 中。数学上完全等价,但少了一个算子的访存和计算开销。

第二类:结构变换

这类优化改变图的结构但保持语义等价——比如消除冗余节点(Dropout、Identity)、合并连续的同类操作(两个 Reshape 合并为一个)。MNN 的 Region 合并属于这一类的高级形态。

第三类:布局优化

为目标硬件选择最优的数据布局——比如 CPU 上 NCHW 对 SIMD 友好,GPU 上 NHWC 对内存合并访问友好。Paddle-Lite 的 type_layout_cast_pass 专门处理这类优化。

第四类:后端特定优化

利用特定硬件的能力做优化——比如 ORT 的 CUDA EP 可以把 Conv+BN+ReLU 融合为 cuDNN 的一个融合 kernel 调用,比分别执行三个 kernel 快得多。这类优化只在 Level 3(后端特定)执行。

编译器优化的对应关系

编译器                    推理引擎
------                    --------
常量折叠                  常量折叠(所有框架都支持)
死代码消除                删除 Dropout/Identity 等无用节点
公共子表达式消除          ORT 的 CSE Pass
函数内联                  子图展开(把子图中的算子内联到主图)
循环展开/向量化           算子内部的 SIMD/NEON 优化(不在图优化层面)
寄存器分配                内存复用(Paddle-Lite 的 memory_optimize_pass)
指令调度                  算子调度(Ch19 讲过)

选择建议

图优化能力是评估推理引擎时容易被忽视的维度——它不直接体现在 API 中,但对推理性能有显著影响。

如果你的模型是标准 CNN(ResNet、MobileNet 等):所有框架的图优化都够用——Conv+BN 融合和 ReLU 融合是收益最大的优化,六个框架都支持。

如果你的模型是 Transformer 架构:ORT 的 AttentionFusion 和 MNN 的 Geometry Compiler 有明显优势。ORT 能把 Q/K/V 计算和 Attention 模式识别并融合为一个高效算子;MNN 能把 Transformer 中大量的 Reshape/Permute 操作零开销化。

如果你需要自定义优化规则:ORT 的 GraphTransformer 框架是最佳选择——注册一个新的 RewriteRule 就能插入自定义优化逻辑。Paddle-Lite 的 MIR Pass 也支持扩展,但复杂度更高。

如果你追求部署极简:ncnn 和 MACE 的”离线优化 + 运行时零开销”模式最适合——模型转换时一次优化到位,部署时不需要任何优化相关的代码和依赖。


本章小结

图优化是推理引擎中”看不见的编译器”——用户不直接调用它,但它对性能的影响可能超过算子内部的微优化。六个框架的图优化策略反映了各自的设计哲学:ncnn 和 MACE 选择在离线工具中做最少的必要优化,追求运行时极简;MNN 用 Geometry Compiler 开创了降维优化的新路线;ORT 建立了最完整的多级 Pass 体系;TNN 取中间路线覆盖核心场景;Paddle-Lite 用 50+ Pass 追求极致全面。

一个反直觉的观察:图优化的 Pass 数量和最终推理性能并不成正比。Paddle-Lite 有 50+ Pass,但在某些模型上并不比只有几个离线优化的 ncnn 快——因为图优化只是性能优化的一个环节,算子内部的 kernel 实现质量、内存管理效率、调度开销等因素同样重要。图优化做得再好,如果底层 kernel 不够高效,整体性能也提不上去。

下一章我们把 Ch17-Ch21 的所有维度汇总,画出六个引擎的设计取舍全景图——帮你在实际项目中做出选择。


导读目录:README.md 上一章:Ch20: 后端组织——如何管理多个硬件加速器 下一章:Ch22: 横向总结——六个引擎的设计取舍全景图