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

移动端推理引擎 — 核心抽象深度对比

调研时间:2026-06-22 基于 ncnn / MNN / ONNX Runtime / TNN / Paddle-Lite / MACE 六个项目的源码阅读 目的:理解不同推理引擎在”数据容器 / 算子抽象 / 推理调度 / 后端组织”四个核心维度上的设计取舍


一、全景速览

六个引擎对”模型怎么在设备上跑”这个问题给出了不同的架构回答。每一个的差异本质上都是在回答同样的四个子问题:数据怎么存?算子怎么抽象?推理怎么调度?多后端怎么管?

引擎 数据容器 算子抽象 推理调度 后端组织
ncnn Mat(elempack 打包) Layer(单类继承) Net/Extractor(按需递归) 编译时分层注册表
MNN Tensor(NC4HW4 + 虚拟 Region) Op-Execution 分离 Interpreter/Session/Pipeline Geometry Compiler
ONNX Runtime OrtValue→Tensor(浅层指针) OpKernel + KernelRegistry InferenceSession(图分区) Execution Provider 插件
TNN Blob(BlobDesc + BlobHandle) BaseLayer + LayerAcc 双层 TNN→Instance→DefaultNetwork AbstractDevice 设备抽象
Paddle-Lite TensorLite(Place 三元组) OpLite + KernelLite 分离 Predictor→MIR→RuntimeProgram NNAdapter HAL 驱动
MACE Tensor(Buffer + Runtime) Operation + Delegator 委托 MaceEngine→Flow→BaseNet Flow 分流 + Hexagon 整图下推

二、数据容器:同一个问题的六种回答

所有引擎的数据容器都要解决三个问题:怎么描述形状?怎么管内存?怎么让 SIMD 跑得快?

2.1 ncnn Mat — elempack 打包

ncnn 的 Mat 是六个引擎中最独特的设计。核心创新是 elempack 字段:

// ncnn src/mat.h
class Mat {
    void* data;
    size_t elemsize;   // 单个存储元素的字节数 = sizeof(标量) × elempack
    int elempack;      // 打包数:1/4/8/16
    int dims, w, h, d, c;
    size_t cstep;      // channel 步长
    Allocator* allocator;
};

elempack=4 意味着 4 个逻辑 channel 被打包进一个存储元素。当 elemsize=16(4×float32)时,Mat 的每个”像素位置”存储了 4 个 channel 的值——恰好填满一个 128-bit NEON/SSE 寄存器。运行时根据 CPU 能力自动选择最优 elempack:AVX-512 用 16,AVX 用 8,NEON/SSE 用 4。

这个设计的妙处是打包逻辑和 SIMD 指令集直接绑定,不需要像 NC4HW4 那样用固定的”4”。Mat 甚至为 SIMD 类型提供了 fill() 重载:

void fill(float32x4_t _v);   // ARM NEON
void fill(const __m256& _v);  // x86 AVX

2.2 MNN Tensor — NC4HW4 + 虚拟 Region

MNN 基于 Halide 的 halide_buffer_t 构建 Tensor,核心创新有两点:

NC4HW4 内存布局:将 channel 维度按 4 对齐打包,物理存储为 [N, ceil(C/4), H, W, 4]。4 个 channel 的同一空间位置数据连续存放,SIMD 友好。与 ncnn 的 elempack 区别在于:NC4HW4 是固定按 4 打包的布局约定,贯穿整个推理栈;ncnn 的 elempack 是动态的,随 CPU 能力变化。

虚拟 Tensor 与 Region:MNN 允许 Tensor 标记为 MEMORY_VIRTUAL,不分配实际内存。Reshape/Transpose/Slice/Concat 等操作只生成 Region 映射描述(源 tensor + 偏移 + 步长),零拷贝。最终由统一的 Raster Op 批量执行实际数据搬运。

struct Region {
    struct View { int32_t offset; int32_t stride[3]; };
    View src, dst;
    int32_t size[3];
    Tensor* origin;  // 源 tensor
};

2.3 ONNX Runtime OrtValue/Tensor — 浅层指针

ORT 的 Tensor 是最”薄”的——它不拥有内存,只持有裸指针 + 形状 + 设备位置信息:

class Tensor final {
    void* p_data_;          // 裸数据指针
    TensorShape shape_;
    MLDataType dtype_;
    OrtMemoryInfo alloc_info_;  // CPU/CUDA/DML 位置
};

内存的生命周期完全由 Executor/Workspace 管理。OrtValue 是 type-erased 容器,可包装 Tensor/SparseTensor/TensorSeq 等不同数据类型。TensorShape 对 ≤5 维用内联存储优化(避免堆分配)。

设备信息通过 OrtMemoryInfo 描述,包含 OrtDevice{DeviceType, MemoryType, DeviceId} 三元组——这让 ORT 可以精确描述”CUDA 设备 0 的 pinned memory”这样的位置。

2.4 TNN Blob — BlobDesc + BlobHandle

TNN 将 Blob 拆分为描述(BlobDesc)和句柄(BlobHandle)两部分:

struct BlobDesc {
    DeviceType device_type;   // ARM/OpenCL/Metal/CUDA
    DataType data_type;       // float32/fp16/int8
    DataFormat data_format;   // NCHW/NC4HW4/NC8HW8/NHC4W4
    DimsVector dims;
};
struct BlobHandle {
    void* base;
    uint64_t bytes_offset;
};

Blob 使用 pimpl 模式(内部持有 BlobImpl),自身不拥有内存——内存生命周期由 BlobManager 管理。BlobManager 通过 blob_memory_pool_map_ 实现内存池化。

值得注意的是 DataFormat 的丰富性:NC4HW4(ARM 主用)、NC8HW8(ARM fp16)、NHC4W4(OpenCL image2d)。不同设备使用不同的最优布局,BlobConverter 负责在用户接口的 Mat(始终 NCHW)和内部 Blob 之间转换。

2.5 Paddle-Lite TensorLite — Place 三元组

Paddle-Lite 的 Tensor 最大特点是内嵌了 TargetType + PrecisionType 信息:

class TensorLite {
    TargetType target_;        // kARM/kOpenCL/kXPU/kNNAdapter
    PrecisionType precision_;  // kFloat/kInt8/kFP16
    DDimLite dims_;
    std::shared_ptr<Buffer> buffer_;  // 共享内存,支持多 Tensor 视图
    LoD lod_;                  // Level-of-Detail,PaddlePaddle 特有
};

Buffer 采用 lazy allocation(只在空间不足时才重新分配)。TargetMalloc/TargetFree 全局分发函数按 TargetType 路由到对应后端的分配器。LoD 字段是 PaddlePaddle 生态特有的——支持变长序列的多层索引。

2.6 MACE Tensor — Buffer + Runtime

MACE 通过 Runtime 指针多态管理不同设备的内存:

class Tensor {
    std::vector<index_t> shape_;
    Runtime *runtime_;                  // 负责分配/映射的运行时
    std::shared_ptr<Buffer> buffer_;    // 底层内存(buf_ + host_)
    DataFormat data_format_;            // NHWC/NCHW
    BufferContentType content_type_;    // OpenCL image 布局类型
};

Buffer 同时持有 buf_(设备端原生内存)和 host_(CPU 映射地址)。通过 runtime_->MapBuffer()/UnMapBuffer() 在两者之间切换,MappingGuard 提供 RAII 保护。

MACE 对 OpenCL image2d 的支持最精细——BufferContentType 定义了 8 种不同的 tensor→image2d 映射方式(CONV2D_FILTERIN_OUT_CHANNELWINOGRAD_FILTER 等),每种 Op 选择最优的映射以利用 GPU 纹理缓存的 2D 局部性。

2.7 数据容器横向对比

维度 ncnn MNN ORT TNN Paddle-Lite MACE
内存所有权 Mat 拥有(refcount) InsideDescribe 描述 不拥有(Executor 管) BlobManager 管 Buffer shared_ptr Buffer shared_ptr
SIMD 优化 elempack 动态打包 NC4HW4 固定 无内置布局优化 NC4HW4/NC8HW8 无特殊布局 Image2D 纹理
零拷贝 channel_range 视图 虚拟 Region BlobHandle offset Buffer offset
设备信息 类型隐含 InsideDescribe.backend OrtMemoryInfo BlobDesc.device_type TargetType Runtime 指针
布局灵活性 由 elempack 决定 三种 DimensionType 通用 strides 6+ 种 DataFormat DataLayoutType 8 种 ContentType

核心取舍:ncnn 的 elempack 最灵活(自动适配 SIMD 宽度),但增加了 convert_layout 的复杂性。MNN 的 NC4HW4 简单统一但固定死了打包数为 4。ORT 最”薄”——把所有复杂性推给了 EP 层。MACE 的 image2d 映射最精细,但增加了大量格式转换代码。


三、算子抽象:设计哲学的分水岭

算子层是六个引擎之间设计差异最大的地方。核心问题是:平台无关的算子语义和平台特定的计算实现之间,怎么划界?

3.1 ncnn Layer — 最简单的单类继承

ncnn 用一个 Layer 基类搞定一切:

class Layer {
    virtual int load_param(const ParamDict& pd);
    virtual int load_model(const ModelBin& mb);
    virtual int create_pipeline(const Option& opt);
    virtual int forward(const Mat& bottom, Mat& top, const Option& opt) const;
    // Vulkan 版本
    virtual int forward(const VkMat& bottom, VkMat& top, VkCompute& cmd, const Option& opt) const;
};

平台特化通过继承实现:ConvolutionConvolution_arm → (内部再按 NEON/fp16/int8 分支)。Vulkan 版本通过 Convolution_vulkan 独立继承。

Layer Factory 是编译时生成的静态注册表。运行时通过 CPU 能力检测选择最优实现链:先查 AVX-512 注册表 → AVX → NEON → 通用。Layer_final 内部组合一个 CPU 层和一个可选的 Vulkan 层。

这种设计的优点是极度简单——通读一个 Convolution_arm.cpp 就能完全理解 ARM 卷积的实现。缺点是 shape 推断和计算实现耦合在同一个类中,新增后端需要写一个完整的子类。

3.2 MNN Op-Execution 分离 + Geometry Compiler

MNN 将算子拆成三层:

Op(Schema 层):用 FlatBuffers 声明式定义,纯数据。OpType_Convolution 只描述”是什么操作、参数是什么”。

Execution(Runtime 层)onResize()(根据形状分配临时内存)+ onExecute()(实际计算)。两阶段分离让内存分配可以在所有 op 的 resize 完成后统一优化。

Geometry Compiler(降级层):最具创新性的设计。非计算密集的 Op(Reshape/Transpose/Slice/Concat)被”降级”为 Region 描述(内存视图操作),不生成实际计算 kernel。所有 Region 最终合并到统一的 Raster Op 中执行。

传统方式:|Op| × |Backend| 个 kernel 实现
MNN 方式:少量核心 Op 的 kernel + 大量 Op 只写 GeometryComputer(纯 shape 逻辑)

每个 Backend 只需实现 Raster + Conv/MatMul/BinaryOp 等少量核心 Op,大幅降低了新增后端的门槛。

3.3 ONNX Runtime OpKernel + KernelRegistry

ORT 用标准的 Compute(OpKernelContext*) 接口:

class OpKernel {
    virtual Status Compute(OpKernelContext* context) const = 0;
    virtual Status PrePack(const Tensor& tensor, ...);  // 预打包权重
};

注册通过宏 + KernelRegistry 完成,维度是 {domain, op_type, opset_version, type_constraints, provider}。ONNX opset 版本化是 ORT 独有的——同一个 Add 算子在 opset 7-12、13、14+ 可能有不同的语义和 kernel 实现。

REG_ELEMENTWISE_VERSIONED_TYPED_KERNEL(Add, 7, 12, float, Add)  // opset 7-12
REG_ELEMENTWISE_TYPED_KERNEL(Add, 14, float, Add)               // opset 14+

这种设计的代价是注册机制相对复杂,但好处是完美对齐 ONNX 标准的版本演进。

3.4 TNN BaseLayer + LayerAcc 双层

TNN 是 ncnn 的”架构翻新版”,最大改进是把 ncnn 的单类 Layer 拆成了两层:

BaseLayer:平台无关,只做 shape 推断(InferOutputShape())。所有设备共享同一份 shape 逻辑。

AbstractLayerAcc:平台特定的计算实现。Init()/Reshape()/Forward() 三个接口。

桥接逻辑在 BaseLayer::Init() 中:

layer_acc_ = device->CreateLayerAcc(type_);  // Device 创建平台特定的 acc
layer_acc_->Init(context, param, resource, inputs, outputs);

以 Convolution 为例:ConvLayer(BaseLayer)只有 InferOutputShape();ArmConvLayerAcc 内部再通过工厂选择 1x1/3x3 Winograd/depthwise 等具体实现;OpenCLConvLayerAcc 独立选择 Depthwise/1x1/Winograd/Common。

这种双层设计的好处:新增后端只需写 LayerAcc,shape 逻辑零重复。坏处:多一层间接调用,调试时需要追踪两个注册表。

3.5 Paddle-Lite OpLite + KernelLite — 三维派发

Paddle-Lite 的 Kernel 注册使用 (Target, Precision, Layout) 三维坐标:

template <TargetType Target, PrecisionType Precision, DataLayoutType DataLayout>
class KernelLite : public KernelBase {
    TargetType target() const override { return Target; }
    PrecisionType precision() const override { return Precision; }
    DataLayoutType layout() const override { return DataLayout; }
};

同一个 mul 算子可以有 (kARM, kFloat, kNCHW)(kARM, kFP16, kNCHW)(kARM, kInt8, kNCHW) 三个 kernel。MIR 的 static_kernel_pick_pass 根据得分选择最优组合。当相邻节点的 Place 不匹配时,自动插入三类 cast 算子:type_target_cast_pass(跨设备拷贝)、type_precision_cast_pass(精度转换)、type_layout_cast_pass(布局变换)。

这种设计在多后端覆盖场景下很强大——Paddle-Lite 支持 18+ 种 TargetType,三维坐标系统自然支持了庞大的组合空间。

3.6 MACE Operation + Delegator — 运行时委托

MACE 的 Op 层面是常规的 Run() 接口,创新在于 Delegator 中间层:

Operation → Delegator → 具体内核

Delegator 用 5 维 key 查找:{delegator_name, data_type, runtime, impl_type, tag}。tag 允许运行时根据参数选择最优内核:

Conv2dOp::Run()
  ├── 1x1 filter → tag K1x1
  ├── 3x3 s1 大通道 → tag K3x3Winograd
  ├── 3x3 s1 小通道 → tag K3x3S1
  ├── 3x3 s2 → tag K3x3S2
  └── 默认 → 通用 NEON

Delegator 在首次 Run() 时惰性创建并缓存。这种设计比 TNN 的 ArmConvLayerAccFactory 更通用——不仅卷积,BiasAdd/Activation 等也通过 Delegator 分发。

注意:OpenCL 路径走 Delegator 模式,而是直接持有 opencl::image::Conv2dKernelopencl::buffer::Conv2dKernel 指针——两种 GPU 内存类型需要完全不同的 CL kernel 代码。

3.7 算子抽象横向对比

维度 ncnn MNN ORT TNN Paddle-Lite MACE
Shape/Compute 分离 不分离 Op-Execution 不分离 BaseLayer-LayerAcc Op-Kernel Operation-Delegator
后端绑定 继承子类 Backend::onCreate EP::GetKernelRegistry Device::CreateLayerAcc 三维注册 5维 Delegator
新增后端代价 写完整子类 只写核心 Op 实现 EP 写 LayerAcc 写 Kernel 写 Flow+Runtime
版本管理 ONNX opset
运行时选择 编译时注册表 Backend Factory KernelRegistry Device Factory MIR pass Delegator 惰性选择

四、推理调度:从按需递归到图分区

4.1 ncnn — 按需递归(Lazy Evaluation)

ncnn 的调度是六个引擎中最独特的:不按拓扑序顺序执行,而是从输出 blob 反向递归计算依赖链

// 用户调用
ex.extract("output", output_mat);
// 内部触发
forward_layer(output_blob.producer, blob_mats, opt);
// forward_layer 递归确保所有输入已计算
for (每个输入 blob) {
    if (blob 未计算) forward_layer(blob.producer, ...);
}
// 然后执行当前层
do_forward_layer(layer, blob_mats, opt);

Light mode 下中间 blob 被消费后立即释放,最大限度节省内存。这种按需计算模式在只需要部分输出的场景下非常高效——未被依赖的层完全不会执行。

4.2 MNN — Session/Pipeline 三阶段

MNN 的调度分三个层次:

Interpreter → 加载模型(FlatBuffers 反序列化)
  → Session → 绑定输入输出 + 管理多条 Pipeline
    → Pipeline → 形状计算 + Geometry 变换 + 创建 Execution + 内存分配

Pipeline::encode() 的四步流水线:(1) SizeCompute 推导输出 shape → (2) GeometryTransform 将非核心 Op 降级为 Region → (3) Backend::onCreate 创建 Execution → (4) Execution::onResize 分配临时内存。

一个 Interpreter 可创建多个 Session(共享模型权重,独立拥有中间 tensor),适合多路并行推理。

4.3 ONNX Runtime — InferenceSession 图分区

ORT 的调度核心是 Initialize() 阶段的图分区

Load(model) → TransformGraph(优化) → PartitionGraph(分区) → CreateKernels → CreateExecutionPlan

分区逻辑:EP 按优先级排列,高优先级 EP 先通过 GetCapability() 认领能处理的节点集合,剩余节点由低优先级 EP 处理。CPU EP 作为最终兜底。

当一个 tensor 的 producer 和 consumer 在不同 EP 上时,ORT 自动插入 MemcpyToHost/MemcpyFromHost 节点处理数据传输。SequentialExecutionPlan 还做内存复用——生命周期不重叠的 tensor 共享同一块 buffer。

4.4 TNN — Instance → DefaultNetwork 顺序执行

TNN 的调度相对简单直接:

TNN::Init() → TNNImpl 解析模型
Instance::Init() → DefaultNetwork::Init()
  → Device 选择 → 图优化(conv+bn 融合等)→ BlobManager 初始化
  → InitLayers(创建 BaseLayer + LayerAcc)→ AllocateBlobMemory → ReshapeLayers
DefaultNetwork::Forward()
  → for (auto layer : layers_) layer->Forward();  // 顺序执行

有一个亮点:ConstFolder(常量折叠预处理)。Instance::Init 会先创建一个 ConstFolder 网络,预计算所有常量子图,将结果注入到主网络中——运行时这些子图不再执行。通过 DataFlag::CHANGE_NEVER 标记哪些 blob 是常量。

4.5 Paddle-Lite — MIR 优化管线

Paddle-Lite 有六个引擎中最重的优化管线:

模型加载 → Program(解析 op/var)
  → Optimizer::Run()
    → SSAGraph 构建
    → [pass₁, pass₂, ..., passₙ]  // 50+ 个 pass
    → RuntimeProgram 生成

SSAGraph 是 Arg 节点(数据)和 Stmt 节点(算子)交替相连的二部图。Pass 分三种粒度:kProgramWise(改图拓扑)、kStmtWise(改语句不改拓扑)、kDebug(只观察)。

关键 pass 类别包括:kernel 选择(static_kernel_pick_pass)、类型 cast 自动插入(3 个维度各一个 pass)、算子融合(conv+bn、fc fuse 等 50+ 个)、子图切分(subgraph_pass,将部分图切给 NPU)、内存优化(复用中间 buffer)。

最终的 RuntimeProgram 是一个扁平化的 Instruction 序列,每条绑定了一个 Op 和其选定的 Kernel,按拓扑序逐条 Launch。

4.6 MACE — Flow 分流执行

MACE 的调度有四个层次:

MaceEngine(公共 API, PIMPL)
  → SerialEngine(串行执行多个 Flow)
    → BaseFlow(单个子网络的执行流)
      → BaseNet(op 拓扑序执行)

Flow 是 MACE 特有的概念:不只是”按顺序跑 Op”,而是管理整个子图的生命周期——模型加载、内存规划与分配、输入输出的格式转换(NHWC ↔ NCHW 转置),以及设备上下文的 BeforeRun/AfterRun。不同后端有不同的 Flow 子类:CpuRefFlowCpuBf16FlowOpenclRefFlowHexagonRefFlowQnnRefFlow

FlowRegistry 按 {RuntimeType, FlowSubType} 注册,允许一个模型中同时使用 CPU 和 GPU 子图。

MemoryManager 使用 4 种租用方式:RENT_PRIVATE(权重独占)、RENT_SHARE(Op 输出间共享)、RENT_SCRATCH(临时缓冲区)、RENT_SLICE(从模型数据零拷贝切片)。

4.7 调度策略横向对比

维度 ncnn MNN ORT TNN Paddle-Lite MACE
执行策略 按需递归 Pipeline 顺序 拓扑序顺序 顺序 指令序列 Flow 分流
图优化 轻量(无独立 pass) Geometry 降级 独立优化器链 NetOptimizer 50+ MIR pass 无独立 pass
内存复用 Light mode 立即释放 Pipeline 统一规划 SequentialPlan BlobManager memory_optimize_pass MemoryPool
多会话 Extractor 多实例 多 Session 共享权重 多 Session 多 Instance Clone 共享权重 多 Flow
常量折叠 GeometryComputer 图优化 pass ConstFolder MIR pass

五、后端组织:多平台适配的工程哲学

5.1 ncnn — 编译时分层注册表

ncnn 通过 CMake 在编译时生成多层静态注册表:

static const layer_registry_entry layer_registry[] = { ... };        // 通用
static const layer_registry_entry layer_registry_arch[] = { ... };   // ARM/x86 基础
static const layer_registry_entry layer_registry_avx[] = { ... };    // AVX 特化
static const layer_registry_entry layer_registry_avx512[] = { ... }; // AVX-512 特化
static const layer_registry_entry layer_registry_vulkan[] = { ... }; // Vulkan 特化

运行时先查最高能力的注册表,逐级回退。Layer_final 内部组合 CPU 层 + Vulkan 层:

Layer* create_layer(int index) {
    Layer_final* lf = new Layer_final;
    lf->layer_cpu = create_layer_cpu(index);      // 分层 CPU 选择
    lf->layer_vulkan = create_layer_vulkan(index); // 可能为 null
    return lf;
}

Vulkan 后端是 ncnn 的独特优势:通过 Vulkan Compute Shader 实现 GPU 加速,跨 Android/Windows/Linux/macOS 全平台。每个算子有多个 .comp shader 变体(按 packing/算法/数据类型组合),编译为 SPIR-V 嵌入二进制。VkCompute 录制命令、Pipeline 封装 shader dispatch,最终 submit_and_wait() 提交执行。

5.2 MNN — Geometry Compiler 降维

MNN 的后端组织核心创新是 Geometry Compiler——一种降维策略:

传统:每个 Op × 每个 Backend → 一个 kernel 实现
MNN:核心 Op(Conv/MatMul/BinaryOp)× Backend → kernel
     其他 Op → GeometryComputer(纯 shape 逻辑)→ Region → Raster Op

每个 Backend 必须实现的最少 Op 集合非常小:Raster(内存搬运)+ Conv2D + MatMul + 少量计算密集 Op。这大幅降低了新增后端的工程量。

Backend 接口的核心方法:onCreate() 创建 Execution、onAcquire() 分配内存、onCopyBuffer() 做设备间拷贝和布局转换。Runtime 是 Backend 的工厂,按 MNNForwardType(CPU/Metal/OpenCL/Vulkan/CUDA)查找注册的 BackendCreator。

三层 Factory 链:Runtime/BackendCreator → Backend → Execution Creator。

5.3 ONNX Runtime — Execution Provider 插件

ORT 的 EP 是六个引擎中最标准的插件架构:

class IExecutionProvider {
    // 核心:报告此 EP 能执行哪些子图
    virtual vector<ComputeCapability> GetCapability(const GraphViewer&, const IKernelLookup&);
    // 提供 kernel 注册表
    virtual shared_ptr<KernelRegistry> GetKernelRegistry() const;
    // 编译融合子图
    virtual Status Compile(vector<FusedNodeAndGraph>&, vector<NodeComputeInfo>&);
    // 设备间数据传输
    virtual unique_ptr<IDataTransfer> GetDataTransfer() const;
};

EP 按优先级排列:TensorRT EP > CUDA EP > CPU EP。每个 EP 通过 GetCapability() 认领节点,未被认领的落到下一级。从 ORT 1.23 起,新 EP 推荐作为动态库插件实现(libonnxruntime_providers_<name>.so),无需修改 ORT 核心代码。

这种设计的扩展性最强——TensorRT/OpenVINO/QNN/CoreML 等都作为独立 EP 插件存在。代价是抽象层最厚,单步调试时 call stack 较深。

5.4 TNN — AbstractDevice 设备抽象

TNN 用 AbstractDevice 统一所有后端:

class AbstractDevice {
    virtual BlobMemorySizeInfo Calculate(BlobDesc&) = 0;       // 计算内存大小
    virtual Status Allocate(void**, BlobMemorySizeInfo&) = 0;  // 分配内存
    virtual AbstractLayerAcc* CreateLayerAcc(LayerType) = 0;   // 创建算子加速器
    virtual Context* CreateContext(int device_id) = 0;         // 创建设备上下文
};

全局 map<DeviceType, AbstractDevice> 通过静态初始化注册。双重分发模式:先按 DeviceType 找 Device,再按 LayerType 找 LayerAcc Creator。

已有后端:ARM(NC4HW4)、OpenCL(NHC4W4 image2d)、Metal(NC4HW4)、CUDA(NCHW)、x86(NC4HW4)——每种设备有自己的最优数据布局。

5.5 Paddle-Lite — NNAdapter HAL 驱动

NNAdapter 是 Paddle-Lite 最有特色的设计——一个统一的 NPU 硬件抽象层,通过 8 个函数指针定义完整的硬件驱动接口:

typedef struct Device {
    const char* name;
    int (*open_device)(void** device);
    void (*close_device)(void* device);
    int (*create_context)(void* device, const char* properties, ...);
    void (*destroy_context)(void* context);
    int (*validate_program)(void* context, const core::Model* model, bool* supported_ops);
    int (*create_program)(void* context, core::Model* model, core::Cache* cache, void** program);
    void (*destroy_program)(void* program);
    int (*execute_program)(void* program, uint32_t input_count, core::Argument* inputs, ...);
} Device;

每个硬件后端编译为独立的 .so,运行时 dlopen 加载。已支持 13+ 种硬件:华为昇腾/麒麟 NPU、瑞芯微 NPU、联发科 APU、Intel OpenVINO、NVIDIA TensorRT、高通 QNN、寒武纪 MLU 等。

NNAdapter 有自己的 IR(Operand/Operation/Model),独立于 MIR。子图切分流程:MIR subgraph_pass 标记可 offload 的 op → 切分子图 → 转换为 NNAdapter Model → validate_program 确认硬件支持 → create_program 编译 → execute_program 执行。不支持的 op 自动 fallback 到 CPU。

5.6 MACE — Flow 分流 + Hexagon 整图下推

MACE 的后端组织有两种截然不同的模式:

CPU/OpenCL — 逐 Op 执行:通过 Flow + BaseNet 按拓扑序逐个 Op 执行。CPU 用 Delegator 分发到 ref/NEON 实现;OpenCL 区分 image2d 和 buffer 两条路径。

Hexagon DSP — 整图下推:完全不同的执行模型——整个网络定义和权重一次性发送到 DSP

class HexagonControlWrapper {
    virtual bool SetupGraph(const NetDef& net_def, ...);      // 整图传给 DSP
    virtual bool ExecuteGraphNew(input_tensors, output_tensors); // 整图执行
};

通过 FastRPC 与 DSP 通信,ION 共享内存实现 CPU ↔ DSP 零拷贝。HTA(Hexagon Tensor Accelerator)路径甚至支持 OpenCL+HTA 混合执行。

MACE 还有独特的电源/性能管理:CPU 大小核绑定(AFFINITY_BIG_ONLY/AFFINITY_POWER_SAVE)、GPU GPUPerfHint/GPUPriorityHint 控制、DSP SetPower() 直接控制电压/频率角点。内存对齐也按硬件适配:Hexagon 128B、ARM 64B、x86 32B。

5.7 后端组织横向对比

维度 ncnn MNN ORT TNN Paddle-Lite MACE
抽象模式 编译时注册表 Backend + Geometry EP 插件 Device NNAdapter HAL Flow + Runtime
新增后端 写 Layer 子类 写核心 Op 实现 EP 接口 写 LayerAcc 8 个函数指针 写 Flow+Runtime
NPU 适配 CoreML/NNAPI EP 认领 NNAPI NNAdapter 13+ 芯片 Hexagon/QNN
GPU 路径 Vulkan 跨平台 Metal/OpenCL 按 EP OpenCL/Metal OpenCL OpenCL image2d
动态加载 EP .so 插件 NNAdapter .so

六、设计模式总结

6.1 六个核心设计模式

1. elempack / NC4HW4 / image2d — SIMD 友好内存布局

所有引擎都认同”连续内存中放相邻 channel 的数据”这个优化方向,但实现方式不同:ncnn 动态 elempack(与 ISA 绑定)、MNN/TNN 固定 NC4HW4(与 NEON 128-bit 绑定)、MACE image2d(与 GPU 纹理缓存绑定)、ORT/Paddle-Lite 不做内置布局优化(交给 EP/Kernel 自行处理)。

2. Op-Kernel 分离 — 语义与实现解耦

MNN(Op-Execution)、TNN(BaseLayer-LayerAcc)、Paddle-Lite(OpLite-KernelLite)、MACE(Operation-Delegator)都做了这个分离。ncnn 和 ORT 没有显式分离——ncnn 靠继承,ORT 的 OpKernel 同时包含语义和实现。

分离的好处:shape 逻辑只写一次,新增后端只需写计算实现。代价:多一层间接,调试稍复杂。

3. 注册表模式 — 编译时 vs 运行时

ncnn 用编译时代码生成(CMake → .h.in → 静态数组)。MNN/TNN/MACE 用运行时静态初始化(全局变量构造函数注册到 map)。ORT 用宏 + 运行时注册。Paddle-Lite 用宏 + KernelFactory。

编译时注册的好处是零运行时开销和更好的 dead code elimination;运行时注册更灵活,支持条件注册。

4. 图优化管线 — 轻量 vs 重量

从轻到重:ncnn(无独立优化)→ TNN(NetOptimizer 融合)→ MNN(Geometry 降级)→ ORT(图变换 + 分区)→ Paddle-Lite(50+ MIR pass)。

优化管线越重,模型加载越慢但推理越快——适合部署场景。ncnn 的极简路线适合快速原型和小模型。

5. NPU 适配策略 — 子图 vs 整图 vs HAL

ORT:EP 认领子图节点,剩余 CPU 兜底。Paddle-Lite:NNAdapter HAL 统一接口,8 个函数指针覆盖 13+ 芯片。MACE:Hexagon 整图下推,CPU/GPU 子图逐 Op 执行。TNN:NNAPI 接入。ncnn:无 NPU 支持。

6. Vulkan vs OpenCL vs Metal — GPU 路径选择

ncnn 是唯一选择 Vulkan 作为 GPU 后端的框架——获得了最广的跨平台覆盖(Android/Windows/Linux/macOS),但需要写 GLSL compute shader。MNN/TNN/MACE/Paddle-Lite 选择 OpenCL(移动端主流)+ Metal(iOS)的组合。ORT 通过 EP 支持 CUDA/DML/CoreML 等各种 GPU API。

6.2 架构演进关系

ncnn (2017, 极简先驱)
  ├── 启发 → TNN (2020, ncnn 的"架构翻新版")
  │           增加: BaseLayer/LayerAcc 双层, Device 抽象, pimpl
  │           保留: Blob 概念, 顺序执行
  └── 对比 → MNN (2019, 全栈路线)
              增加: Geometry Compiler, 端侧训练, LLM 推理
              不同: 基于 Halide buffer, FlatBuffers schema

ONNX Runtime (2019, 标准化路线)
  → 独立演进, Execution Provider 插件架构
  → 最广生态: TensorRT/OpenVINO/QNN/CoreML 全接入

Paddle-Lite (2019, 异构覆盖路线)
  → MIR + NNAdapter, 国产 NPU 覆盖最广
  → 借鉴 Android NNAPI 的 HAL 设计

MACE (2018, DSP 深度优化路线)
  → Delegator 模式, Hexagon 整图下推
  → image2d 纹理缓存优化
  → 项目维护活跃度下降

七、犀牛鸟参与建议

从源码阅读的深度和贡献空间角度:

ncnn 最适合深度参与:代码量最小(~500 文件),零依赖,所有核心逻辑完全暴露。Vulkan Compute Shader 是独一份的 GPU 路径,社区活跃(22K star)。推荐切入点:ARM NEON 算子优化、Vulkan shader 新增/优化、新算子支持。

具体阅读路线

  1. Mat → Layer → Net/Extractor:理解数据流和调度(3-5 天)
  2. Convolution_arm.cpp:深入理解 NEON 优化(Winograd/im2col/GEMM 策略选择)
  3. Vulkan 管线gpu.h → command.h → pipeline.h → convolution_vulkan.h → convolution_*.comp
  4. 对比 TNN 的双层设计:理解”为什么 ncnn 不做 shape/compute 分离”的取舍
  5. 对比 MNN 的 Geometry Compiler:理解”为什么 ncnn 每个 Op 都手写”的取舍