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: 横向总结——六个引擎的设计取舍全景图