犀牛鸟 2026 · 开源研究笔记

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 的所有维度汇总,画出六个引擎的设计取舍全景图——帮你在实际项目中做出选择。


章节自测

  1. Conv+BN 融合为什么在数学上完全等价(写出 BN 参数如何合并进 Conv 的 weight 和 bias 的思路),它属于本章分类学中的哪一类优化?
  2. ncnn 把图优化放在离线的 ncnnoptimize 工具中、运行时零图优化,而 ONNX Runtime 建立了三级 GraphTransformer 体系——两种策略分别在优化什么目标?TNN 的 NetOptimizer + ConstFolder 处在这个谱系的什么位置?
  3. 为什么说 MNN 的 Geometry Compiler 是”降维优化”而不是传统的模式匹配式图优化?Region 合并在这个体系中起什么作用?
  4. 动手验证:找一个带 BN 的 ONNX 模型,用 onnx2ncnn 转换后运行 ncnnoptimize,对比优化前后 .param 文件中的层数变化——你能指出哪些层被融合或删除了吗?(提示:观察 BatchNorm、Dropout 层是否还存在)

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