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

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);
    }

动态形状的优化

四步流水线的一个关键优势是动态形状下的增量更新。当输入形状变化时:

  1. SizeCompute 重新推导所有形状
  2. 如果形状没变的算子,跳过后续步骤
  3. 形状变了的算子,重新做 Geometry、onCreate、onResize
  4. 所有算子重新 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: 后端组织——如何管理多个硬件加速器