Ch19: 推理调度——从按需递归到图分区
Part 4: 核心抽象深度对比 前置章节:Ch18: 算子抽象——设计哲学的分水岭 后续章节:Ch20: 后端组织——如何管理多个硬件加速器
核心问题
你已经有了数据容器(Ch17)和算子(Ch18),现在的问题是:按什么顺序执行这些算子? 这个问题听起来简单——按照计算图从前到后顺序执行不就行了?但实际上远没有这么简单。
“按什么顺序执行”只是表面问题,更深层的问题包括:需要先把整个图都构建好再执行,还是可以”要什么算什么”?什么时候推导输出形状、什么时候分配内存、什么时候执行计算——这三件事是同时发生还是分阶段进行?如果模型要跑在多个硬件上(一部分在 CPU、一部分在 GPU),怎么决定哪些算子跑在哪个硬件上?中间结果的内存什么时候可以释放?
六个框架对这些问题给出了六种截然不同的调度策略,从最简单的”按需递归”到最复杂的”图分区 + 执行计划生成”。
做菜顺序的类比
想象你在厨房里要做一桌包含 6 道菜的晚餐,每道菜之间有依赖关系(比如”沙拉酱”是”凯撒沙拉”的前置步骤)。六个框架处理这个问题的方式就像六种不同的下厨风格:
ncnn 像一个接到点单才做菜的大厨——客人说”我要凯撒沙拉”,他才从沙拉开始反向推导”那我先要做沙拉酱”,”做沙拉酱需要蛋黄”,一路递归回去找到最源头的食材开始做。做完一道菜就立刻扔掉用过的碗碟(Light mode 即释内存),绝不多占灶台空间。
MNN 像一个有条不紊的行政总厨——他把做菜过程严格分成四步走:第一步算好每道菜需要多少食材多大盘子(SizeCompute);第二步把复杂菜品分解成基本操作(Geometry 降级);第三步把所有锅碗瓢盆准备到位(onCreate 分配内存);第四步让厨师们按清单上菜(onResize + onExecute)。四步之间不能跳步。
ORT 像一个美食广场的运营经理——他先拿着菜单去问每个摊位”你能做哪些菜”(EP GetCapability),然后把菜单分区(图分区),每个摊位认领一部分菜品。接着他生成一份”上菜计划”(SequentialExecutionPlan),标注好每道菜需要的食材什么时候送到、用完的餐具什么时候回收(生命周期内存复用)。
TNN 像一个中央厨房的流水线——所有菜按固定顺序一道一道做,从前到后一个 for 循环走完。简单直接,没有花哨的调度策略。
Paddle-Lite 像一个自动化餐厅——菜单先经过一个”智能优化系统”(MIR,50+ 轮 pass 优化),系统会自动调整菜品顺序、合并步骤、插入转接菜品(类型转换算子),最后输出一个扁平的”操作指令序列”(RuntimeProgram 的 Instruction 序列),后厨照着一条一条执行就行。
MACE 像一个有分店的连锁餐厅总部——总部经理(MaceEngine)先根据厨房类型(CPU/GPU/DSP)把菜品分流到不同的厨房(Flow 分流),每个厨房内部按拓扑序执行。如果是 Hexagon 厨房,则把整桌菜打包发过去让那边一次搞定(整图下推)。
ncnn:Net/Extractor 按需递归
ncnn 的调度策略是六个框架中最”极简”的——它甚至不预先构建执行计划。用户通过 Extractor 请求某个输出,引擎从该输出反向递归,只计算真正需要的算子。
Net 与 Extractor
// 用户视角的 API
ncnn::Net net;
net.load_param("model.param");
net.load_model("model.bin");
ncnn::Extractor ex = net.create_extractor();
ex.input("input", input_mat);
ncnn::Mat output;
ex.extract("output", output);
// 此时引擎才开始计算——从 "output" 反向递归
按需递归的核心逻辑
// ncnn/src/net.cpp(简化版)
int Extractor::extract(const char* blob_name, Mat& feat) {
int blob_index = net->find_blob_index_by_name(blob_name);
// 如果这个 blob 已经计算过了,直接返回
if (blob_mats[blob_index].dims != 0) {
feat = blob_mats[blob_index];
return 0;
}
// 找到产生这个 blob 的 Layer
int layer_index = net->blobs[blob_index].producer;
Layer* layer = net->layers[layer_index];
// 递归:先确保这个 Layer 的所有输入都已经计算好
for (int i = 0; i < layer->bottoms.size(); i++) {
int bottom_blob_index = layer->bottoms[i];
// 递归调用 extract,计算上游依赖
extract_helper(bottom_blob_index);
}
// 所有输入就绪,执行这个 Layer
layer->forward(bottom_blobs, top_blobs, opt);
// 保存结果
blob_mats[blob_index] = top_blobs[0];
feat = blob_mats[blob_index];
return 0;
}
这种按需递归有一个重要的优势——如果模型有多个输出但你只需要其中一个,那些不在依赖链上的算子完全不会执行。对于多任务模型(同一个 backbone 加多个 head),如果你只需要检测结果而不需要分割结果,ncnn 会自动跳过分割 head 的所有计算。
Light mode:即时释放
ncnn 的 Light mode(轻量模式)进一步利用了按需递归的特性——当一个中间结果被它的所有下游消费者用完后,立即释放内存:
// Light mode 下,消费完的中间结果立即释放
if (opt.lightmode) {
// 检查这个 blob 的所有消费者是否都已经执行完毕
bool all_consumers_done = true;
for (int consumer : net->blobs[bottom_blob_index].consumers) {
if (!layer_done[consumer]) {
all_consumers_done = false;
break;
}
}
if (all_consumers_done) {
blob_mats[bottom_blob_index] = Mat(); // 释放
}
}
Light mode 让 ncnn 的内存峰值接近理论最优——在任何时刻,内存中只保留”当前正在计算的算子的输入和输出”以及”还有下游消费者未消费的中间结果”。
设计取舍
按需递归的好处是概念简单、实现直接、天然支持部分图执行和最优内存回收。坏处是:没有预先的全局执行计划,无法做跨算子的调度优化(比如算子融合、内存预分配);递归调用带来函数调用栈开销;不支持真正的异步执行(GPU 命令队列需要提前规划)。对于 ncnn “极简 CPU 推理”的定位来说,这些坏处都可以接受。
MNN:四步流水线
MNN 的调度策略是六个框架中最”工程化”的——它把推理过程严格分为四个阶段,每个阶段有明确的职责和边界。
Interpreter → Session → Pipeline
// 用户视角的 API
auto interpreter = MNN::Interpreter::createFromFile("model.mnn");
MNN::ScheduleConfig config;
config.type = MNN_FORWARD_CPU;
MNN::Session* session = interpreter->createSession(config);
// createSession 时发生了四步初始化
// 后续 runSession 时执行计算
interpreter->runSession(session);
四步详解
第一步:SizeCompute(形状推导)
遍历计算图中的每个算子,根据输入形状推导输出形状。这一步和实际计算无关——只是确定每个中间张量的形状,为后续的内存分配做准备。
// MNN/source/core/Pipeline.cpp(简化版)
ErrorCode Pipeline::allocMemory(bool firstMalloc) {
// Step 1: 推导所有张量的形状
for (auto& unit : mUnits) {
auto& cmd = unit.cmd;
// 调用每个 Op 的 shape inference
SizeComputer::computeOutputSize(cmd.op, cmd.inputs, cmd.outputs);
}
// ...
}
第二步:Geometry(降级)
对于没有直接 Execution 实现的算子,GeometryComputer 将其降级为基本算子组合(Ch18 讲过)。这一步会产生新的 MEMORY_VIRTUAL 类型的中间张量(Ch17 讲过),它们不分配实际内存。
// Step 2: Geometry 降级
for (auto& unit : mUnits) {
if (!unit.hasExecution) {
// 用 GeometryComputer 降级
GeometryComputer::compute(unit.cmd.op,
unit.cmd.inputs,
unit.cmd.outputs,
mContext,
unit.commandBuffer);
}
}
第三步:onCreate(分配内存)
根据前两步确定的形状信息,一次性为所有中间张量分配内存。这一步使用了内存复用策略——生命周期不重叠的张量可以共享同一块内存。
// Step 3: 分配内存并创建 Execution
for (auto& unit : mUnits) {
// 为中间张量分配内存
for (auto tensor : unit.cmd.outputs) {
mBackend->onAcquireBuffer(tensor, Backend::DYNAMIC);
}
// 创建 Execution 对象
unit.execution.reset(
mBackend->onCreate(unit.cmd.inputs,
unit.cmd.outputs,
unit.cmd.op));
}
第四步:onResize + onExecute(准备 + 执行)
先调用每个 Execution 的 onResize(根据实际形状做准备,如选择算法、分配工作空间),然后调用 onExecute 执行实际计算。
// Step 4a: onResize
for (auto& unit : mUnits) {
unit.execution->onResize(unit.cmd.inputs, unit.cmd.outputs);
}
// Step 4b: onExecute
for (auto& unit : mUnits) {
unit.execution->onExecute(unit.cmd.inputs, unit.cmd.outputs);
}
动态形状的优化
四步流水线的一个关键优势是动态形状下的增量更新。当输入形状变化时:
- SizeCompute 重新推导所有形状
- 如果形状没变的算子,跳过后续步骤
- 形状变了的算子,重新做 Geometry、onCreate、onResize
- 所有算子重新 onExecute
如果形状完全没变(连续推理相同尺寸输入),步骤 1-3 可以全部跳过,直接执行步骤 4。这让 MNN 在”固定形状连续推理”场景下几乎没有调度开销。
设计取舍
四步流水线的好处是清晰、可控、可优化——每一步的输入输出都是确定的,可以在步骤之间插入优化逻辑(比如在 Step 3 做内存复用分析)。坏处是前三步的初始化开销较大——第一次推理需要走完四步,比 ncnn 的”直接开算”慢。但对于需要多次推理的场景(大多数实际部署场景),初始化是一次性的,后续推理的执行效率才是关键。
ONNX Runtime:图分区 + 执行计划
ORT 的调度策略是六个框架中最”编译器化”的——它在推理前先把计算图按硬件后端分区,为每个分区生成执行计划,并在计划中嵌入内存生命周期分析和复用策略。
InferenceSession 初始化
// 用户视角的 API
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "inference");
Ort::SessionOptions session_options;
// 添加硬件后端(执行提供者)
session_options.AppendExecutionProvider_CUDA(cuda_options);
// CPU EP 总是作为 fallback 存在
Ort::Session session(env, "model.onnx", session_options);
图分区:EP GetCapability 竞价
图分区是 ORT 调度的核心——每个 ExecutionProvider(EP)”竞价”自己能高效执行的算子:
// onnxruntime/core/framework/graph_partitioner.cc(简化版)
Status GraphPartitioner::Partition(Graph& graph,
const std::vector<ExecutionProvider*>& providers) {
// 按优先级遍历每个 EP(CUDA > TensorRT > CPU)
for (auto* ep : providers) {
// EP 报告自己能处理哪些子图
auto capabilities = ep->GetCapability(graph, kernel_lookup);
for (auto& cap : capabilities) {
// cap.sub_graph 描述了 EP 能处理的一组连续节点
// 把这些节点标记为属于该 EP
AssignNodes(cap.sub_graph, ep);
}
}
// 未被任何 EP 认领的节点 fallback 到 CPU EP
AssignRemainingToCPU(graph);
}
分区的结果是整个计算图被分成若干”子图”,每个子图由一个特定的 EP 负责执行。子图之间的边界可能需要数据拷贝(比如从 GPU 到 CPU)。
SequentialExecutionPlan:执行计划
分区完成后,ORT 生成一个 SequentialExecutionPlan——一个扁平的指令序列,标注了每个节点的执行顺序、内存分配和释放时机:
// onnxruntime/core/framework/sequential_execution_plan.h(简化版)
struct SequentialExecutionPlan {
// 扁平的执行序列
std::vector<SequentialExecutionPlan::NodeExecutionPlan> execution_plan;
// 内存分配计划
struct AllocationPlan {
AllocKind alloc_kind; // Allocate / Reuse / PreExisting
int reuse_buffer; // 如果是 Reuse,复用哪个 buffer
OrtMemoryInfo location; // 在哪个设备的内存上
};
std::vector<AllocationPlan> allocation_plan; // 每个张量的分配策略
// 释放计划
// 每一步执行后,哪些张量可以释放
std::vector<std::vector<int>> to_be_freed;
};
生命周期内存复用
ORT 的内存复用策略是在执行计划生成阶段完成的——分析每个中间张量的”生命周期”(从被创建到最后一次被使用),找到生命周期不重叠的张量对,让它们共享同一块内存:
张量 A: [====] (第 1-3 步使用)
张量 B: [=====] (第 4-6 步使用)
张量 C: [================] (第 1-8 步使用)
张量 D: [====] (第 5-7 步使用)
复用分析:
- A 和 B 生命周期不重叠 → 可以共享内存
- A 和 C 重叠 → 不能共享
- B 和 D 重叠 → 不能共享
分配计划:
- Buffer 0: A(第 1 步分配)→ B(第 4 步复用 A 的内存)
- Buffer 1: C(第 1 步分配)
- Buffer 2: D(第 5 步分配)
设计取舍
ORT 的图分区 + 执行计划策略是六个框架中最”重量级”的——初始化阶段做了大量分析和优化工作。好处是执行阶段非常高效——内存分配是预规划好的,执行顺序是预确定的,EP 之间的数据传输路径也是预计算好的。坏处是初始化慢(大模型的图分区和执行计划生成可能需要几百毫秒到几秒),而且执行计划是静态的——不能根据运行时的实际情况动态调整。
TNN:顺序执行的简单循环
TNN 的调度策略是六个框架中最直接的——一个 for 循环从前到后依次执行每个 Layer。
TNN → Instance → DefaultNetwork
// 用户视角
TNN tnn;
tnn.Init(model_config);
// Instance 是一次推理的上下文
auto instance = tnn.CreateInst(network_config, status);
// DefaultNetwork 是内部的网络实现
// 初始化时按拓扑序排列所有 Layer
顺序执行
// TNN/source/tnn/core/default_network.cc(简化版)
Status DefaultNetwork::ForwardAsync(
std::shared_ptr<BlobStatisticCallback> before,
std::shared_ptr<BlobStatisticCallback> after) {
// 一个简单的 for 循环
for (auto& layer : layers_) {
// 可选的前置回调(用于性能统计)
if (before) {
before->OnBlobBefore(layer->input_blobs_,
layer->output_blobs_,
layer->layer_name_);
}
// 执行 Layer(实际委托给 LayerAcc)
Status ret = layer->Forward();
if (ret != TNN_OK) {
return ret;
}
// 可选的后置回调
if (after) {
after->OnBlobAfter(layer->input_blobs_,
layer->output_blobs_,
layer->layer_name_);
}
}
return TNN_OK;
}
BlobManager 的内存管理
虽然 TNN 的调度策略很简单,但它的内存管理并不简单——BlobManager 在初始化时就做了内存复用分析(和 ORT 类似),只是这个分析是和调度分开的:
// TNN/source/tnn/core/blob_manager.cc(简化版)
Status BlobManager::Init(...) {
// 1. 为所有输入/输出 Blob 创建对象
for (auto& layer_info : net_structure->layers) {
for (auto& output_name : layer_info->outputs) {
Blob* blob = new Blob(GetBlobDesc(output_name));
blob_map_[output_name] = blob;
}
}
// 2. 分析张量生命周期
// 3. 基于生命周期做内存复用
// 4. 一次性分配大块内存,按偏移分配给各 Blob
MemoryAssignStrategy strategy(device_);
strategy.AssignAllBlobMemory(blob_map_, layers_);
return TNN_OK;
}
设计取舍
TNN 的顺序执行方案是六个框架中最”没有调度策略”的调度策略。没有按需递归(不能跳过不需要的算子),没有图分区(不能在多个后端之间分配算子),没有执行计划(执行顺序就是拓扑序)。好处是实现最简单、行为最可预测、调试最容易——出了问题你一个断点就能定位到具体的 Layer。坏处是缺乏灵活性——在多后端场景下,TNN 的”单后端顺序执行”比 ORT 的”图分区多后端并行”效率低。
Paddle-Lite:MIR 优化 + 扁平指令序列
Paddle-Lite 的调度策略最独特的地方在于它的前置优化阶段——在推理前,模型会经过 50+ 轮 MIR(Model Intermediate Representation)pass 优化,最终生成一个扁平的 Instruction 序列。
Predictor → MIR → RuntimeProgram
// 用户视角
auto predictor = paddle::lite::CreatePaddlePredictor<
paddle::lite::CxxConfig>(config);
// 创建 Predictor 时发生了以下流程:
// 1. 加载模型 → ProgramDesc
// 2. 构建 MIR SSA 图
// 3. 运行 50+ 轮优化 pass
// 4. 生成 RuntimeProgram(扁平 Instruction 序列)
MIR pass 优化链
MIR 的优化 pass 非常多,这里列出关键的几类:
// paddle-lite/lite/core/optimizer/optimizer.h(简化版)
void Optimizer::Run(Program* program,
const std::vector<Place>& valid_places) {
// 1. 类型推导 pass
RunPass("type_precision_cast_pass"); // 精度转换
RunPass("type_layout_cast_pass"); // 布局转换
RunPass("type_target_cast_pass"); // 设备转换
// 2. 算子融合 pass
RunPass("lite_conv_bn_fuse_pass"); // Conv + BN 融合
RunPass("lite_conv_activation_fuse_pass"); // Conv + ReLU 融合
RunPass("lite_fc_fuse_pass"); // FC 融合
// 3. kernel 选择 pass
RunPass("static_kernel_pick_pass"); // 根据 Place 选 kernel
// 4. 内存优化 pass
RunPass("memory_optimize_pass"); // 内存复用
// 5. 子图 pass(NPU 相关)
RunPass("nnadapter_subgraph_pass"); // NNAdapter 子图划分
// ... 共 50+ 轮 pass
}
RuntimeProgram:扁平 Instruction 序列
所有 pass 优化完成后,MIR 图被”拍平”成一个 Instruction 序列:
// paddle-lite/lite/core/program.h(简化版)
class RuntimeProgram {
public:
void Run() {
// 按顺序执行每条指令
for (auto& inst : instructions_) {
inst.Run();
}
}
private:
std::vector<Instruction> instructions_;
};
// 每条指令绑定了一个具体的 kernel
struct Instruction {
std::shared_ptr<KernelLite> kernel_;
OpLite* op_;
void Run() {
// 执行选中的 kernel
kernel_->Launch();
}
};
类型转换算子的自动插入
Paddle-Lite 调度中最有特色的是自动插入类型转换算子。当一个算子的输出和下一个算子期望的输入在 Target/Precision/Layout 三个维度的任何一个上不匹配时,MIR pass 会自动插入转换算子来桥接差异:
优化前:
[Conv2d, ARM, Float, NCHW] → [Softmax, OpenCL, Float, ImageDefault]
MIR 自动插入转换:
[Conv2d, ARM, Float, NCHW]
→ [LayoutCast, ARM, Float, NCHW→NC4HW4] // 布局转换
→ [TargetCast, ARM→OpenCL, Float, NC4HW4] // 设备转换
→ [Softmax, OpenCL, Float, ImageDefault]
设计取舍
Paddle-Lite 的 MIR 优化 + 扁平指令序列策略把大量工作前移到了”编译阶段”——50+ 轮 pass 涵盖了算子融合、类型转换、kernel 选择、内存优化等所有方面。好处是执行阶段极其简单——就是一个 for 循环遍历指令序列,没有任何运行时决策。坏处是优化阶段耗时较长(大模型可能几秒到十几秒),而且 pass 之间可能存在顺序依赖和冲突——50+ 轮 pass 的正确性和交互效应很难完全验证。
MACE:Flow 分流 + 拓扑序执行
MACE 的调度策略有一个独特的概念——Flow 分流。MaceEngine 内部有一个 Flow 对象负责根据模型的目标后端选择不同的执行策略。
MaceEngine → Flow → BaseNet
// mace/core/mace.cc(简化版)
class MaceEngine::Impl {
public:
MaceStatus Init(const NetDef* net_def,
const std::vector<std::string>& input_nodes,
const std::vector<std::string>& output_nodes,
const MaceEngineConfig& config) {
// 根据目标后端创建不同的 Flow
if (config.runtime_type() == RT_HEXAGON) {
flow_.reset(new HexagonFlow(config));
} else if (config.runtime_type() == RT_OPENCL) {
flow_.reset(new CommonFlow(config)); // GPU 和 CPU 共用
} else {
flow_.reset(new CommonFlow(config));
}
return flow_->Init(net_def, input_nodes, output_nodes);
}
MaceStatus Run(const std::map<std::string, MaceTensor>& inputs,
std::map<std::string, MaceTensor>* outputs) {
return flow_->Run(inputs, outputs);
}
private:
std::unique_ptr<BaseFlow> flow_;
};
CommonFlow:拓扑序执行
对于 CPU 和 OpenCL 后端,MACE 使用 CommonFlow,内部按拓扑序执行每个 Operation:
// mace/flows/common_flow.cc(简化版)
MaceStatus CommonFlow::Run(...) {
// 拓扑序遍历每个 Operation
for (auto& op : net_->operators()) {
// 执行 Operation(可能委托给 Delegator)
MaceStatus status = op->Run(context);
if (status != MaceStatus::MACE_SUCCESS) {
return status;
}
}
return MaceStatus::MACE_SUCCESS;
}
HexagonFlow:整图下推
Hexagon 后端使用完全不同的 Flow——把整个图作为一个黑盒下推到 Hexagon DSP:
// mace/flows/hexagon_flow.cc(简化版)
MaceStatus HexagonFlow::Init(const NetDef* net_def, ...) {
// 把整个模型设置到 Hexagon 运行时
hexagon_wrapper_->SetupGraph(*net_def);
return MaceStatus::MACE_SUCCESS;
}
MaceStatus HexagonFlow::Run(...) {
// 整图执行——不是逐算子调用
hexagon_wrapper_->ExecuteGraph(input_tensor, &output_tensor);
return MaceStatus::MACE_SUCCESS;
}
“整图下推”和”逐算子执行”是两种完全不同的调度范式。整图下推的好处是:DSP 运行时可以在其内部做全局优化(算子融合、内存规划等),这些优化对 MACE 来说是透明的。坏处是灵活性差——你不能只把部分算子放到 Hexagon 上(虽然 MACE 也支持通过 OpenCL 的”分 Op”模式来混合执行,但 Hexagon 只支持整图)。
设计取舍
MACE 的 Flow 分流策略是一种”策略模式”——根据硬件后端选择完全不同的调度策略,而不是试图用一种统一的策略适配所有后端。好处是每个后端可以有最适合自己的调度方式(CPU 逐算子、Hexagon 整图下推、OpenCL 可能是逐 Op 但要考虑命令队列)。坏处是代码路径分裂——不同后端的调度逻辑是独立维护的,难以共享优化。
横向对比
| 维度 | ncnn | MNN | ORT | TNN | Paddle-Lite | MACE |
|---|---|---|---|---|---|---|
| 调度策略 | 按需递归 | 四步流水线 | 图分区+执行计划 | 顺序for循环 | MIR优化+扁平指令 | Flow分流+拓扑序 |
| 执行方向 | 反向(从输出到输入) | 正向 | 正向 | 正向 | 正向 | 正向 |
| 部分图执行 | 天然支持 | 需配置 | 需配置 | 不支持 | 不支持 | 不支持 |
| 内存释放 | 即时(Light mode) | 步骤间复用 | 预规划复用 | BlobManager复用 | pass优化复用 | 运行时管理 |
| 动态形状 | 每次重跑forward | 增量更新(onResize) | 重新规划执行计划 | Reshape方法 | 重新编译 | 重新初始化 |
| 初始化开销 | 几乎为零 | 中等(四步) | 较高(分区+规划) | 低 | 高(50+ pass) | 中等 |
| 多后端调度 | 不支持 | Session级别 | EP图分区 | 单后端 | MIR pass | Flow分流 |
核心取舍分析
六种调度策略的分歧可以归结为一个核心权衡:在初始化和执行之间如何分配工作量。
ncnn 把几乎所有工作都推到了执行阶段——初始化只加载模型参数,不做任何分析和规划。这让第一次推理和第 N 次推理的速度几乎一样,但每次推理都要付出”现场决策”的开销。
ORT 和 Paddle-Lite 走到了另一个极端——初始化阶段做了大量的分析、优化和规划工作(图分区、50+ pass 优化、执行计划生成),执行阶段只是”照方抓药”。初始化慢但执行快,适合”初始化一次、推理千万次”的生产部署场景。
MNN 取了折中——四步流水线让初始化阶段做适量的准备(形状推导、内存分配),但把算法选择(onResize)的最终决策留到了接近执行的时刻,支持动态形状下的增量更新。
TNN 和 MACE 各有侧重——TNN 的顺序执行方案最简单但也最缺乏优化空间;MACE 的 Flow 分流策略在多后端场景下有优势但代码路径分裂。
如果你做的是手机上的实时相机应用(连续几百帧同尺寸推理),MNN 的四步流水线或 ORT 的预规划执行计划是最优选择——初始化开销被摊薄到可忽略不计。如果你做的是一次性推理任务(用户上传一张图片识别一下),ncnn 的零初始化方案反而更快——不需要等待初始化就能出结果。
本章小结
推理调度是数据容器和算子抽象之上的”指挥系统”——它决定了算子以什么顺序执行、内存什么时候分配和释放、多个硬件后端之间如何协作。六个框架的调度策略从 ncnn 的按需递归(零规划、即做即算)到 ORT 的图分区 + 执行计划(重度规划、精确执行),再到 MACE 的 Flow 分流(不同后端不同策略),覆盖了从极简到复杂的完整谱系。
调度策略和后端组织紧密关联——ncnn 不需要多后端调度因为它在编译时就锁定了一个后端,ORT 需要图分区因为它天然支持多个 EP 竞价算子。下一章我们将看到六个框架如何组织和管理多个硬件后端。
导读目录:README.md 上一章:Ch18: 算子抽象——设计哲学的分水岭 下一章:Ch20: 后端组织——如何管理多个硬件加速器