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

Ch14: TNN——ncnn 的架构翻新版

Part 3: 六大框架架构精讲 前置章节:Ch13: ONNX Runtime——标准集装箱港口 后续章节:Ch15: Paddle-Lite——万能充电器


一句话定位

TNN 是腾讯优图实验室在 ncnn 之后推出的”继任者”。它保留了 ncnn 的移动端基因(轻量、高效、ARM 优化),但在架构上做了一次系统性的翻新——把 ncnn 的单层 Layer 拆成了 BaseLayer(形状推断)+ LayerAcc(硬件计算)双层结构,让多后端扩展从”在一个文件里写 #ifdef“变成了”在不同文件里写不同的 LayerAcc”。如果说 ncnn 是一栋精心建造但扩展困难的老式平房,TNN 就是在同一块地基上盖起来的、带标准化接口的新楼。

GitHub 4.5K star,腾讯优图开源,约 800 个源文件——比 ncnn 的 500 个多一些,但远少于 MNN 的 2000 个,属于一个可以在一两周内通读的规模。


类比:老平房翻新成标准化公寓

想象你有一栋老式平房。它住着很舒服、建造质量很好,但有一个问题:所有管道(水管、电线、网线)都埋在墙里,没有标准化的布线通道。如果你想加一根网线,得凿开墙壁;如果你想换一种水龙头,得把整面墙的水管都改了。

翻新方案不是拆掉重建,而是在原有结构基础上做”标准化”改造:把管道从埋墙改成走预留的管道井,每种管道有独立的通道和标准接口。加网线?往网线通道里穿一根就行,不用动墙壁。换水龙头?换一个符合标准接口的新龙头就行,不用改管道。

TNN 对 ncnn 做的就是这种翻新:

类比边界:这个类比强调的是 TNN 相对 ncnn 的架构改进。但 TNN 不仅仅是”翻新”——它也引入了一些 ncnn 完全没有的新特性,比如 BlobConverter 透明转换和 ConstFolder 预计算,这些不在”翻新”的类比范围内。


设计哲学精讲

哲学一:BaseLayer + LayerAcc 双层——算子的”设计图”与”施工队”

TNN 最核心的架构决策是把 ncnn 的单层 Layer 拆成两层:

BaseLayer (上层): 硬件无关的逻辑
  - 形状推断 (InferOutputShape)
  - 参数验证
  - 输入输出管理
  
LayerAcc (下层): 硬件特定的实现
  - 内存分配 (Reshape)
  - 实际计算 (Forward)
  - 硬件特定的优化
// TNN 的 BaseLayer(简化)
class BaseLayer {
public:
    // 形状推断:纯数学计算,不涉及任何硬件
    virtual Status InferOutputShape(bool ignore_error = false);
    
    // 生命周期管理
    virtual Status Init(Context* context, LayerParam* param,
                        LayerResource* resource,
                        const std::vector<Blob*>& inputs,
                        const std::vector<Blob*>& outputs,
                        AbstractDevice* device);
    
    // 执行推理(委托给 LayerAcc)
    virtual Status Forward();

protected:
    LayerParam* param_;
    LayerResource* resource_;
    std::vector<Blob*> input_blobs_;
    std::vector<Blob*> output_blobs_;
    
    // 持有一个 LayerAcc(硬件特定的实现)
    std::shared_ptr<LayerAcc> layer_acc_;
};
// TNN 的 LayerAcc(简化)
class LayerAcc {
public:
    // 根据输入形状分配资源(类似 MNN 的 onResize)
    virtual Status Reshape(const std::vector<Blob*>& inputs,
                            const std::vector<Blob*>& outputs);
    
    // 实际计算(类似 MNN 的 onExecute)
    virtual Status Forward(const std::vector<Blob*>& inputs,
                            const std::vector<Blob*>& outputs);

    virtual Status Init(Context* context, LayerParam* param,
                         LayerResource* resource,
                         const std::vector<Blob*>& inputs,
                         const std::vector<Blob*>& outputs);
};

以卷积算子为例,TNN 的文件组织如下:

layer/
├── base_layer.h / .cc                 # BaseLayer 基类
├── conv_layer.h / .cc                 # ConvLayer(形状推断)
│
├── arm/
│   └── arm_conv_layer_acc.h / .cc     # ARM NEON 实现
├── x86/
│   └── x86_conv_layer_acc.h / .cc     # x86 SSE/AVX 实现
├── opencl/
│   └── opencl_conv_layer_acc.h / .cc  # OpenCL 实现
└── metal/
    └── metal_conv_layer_acc.h / .mm   # Metal 实现 (iOS)

对比 ncnn:

layer/
├── convolution.h / .cpp               # 包含通用逻辑 + ARM 的 #ifdef 分支
├── arm/
│   └── convolution_arm.cpp            # ARM 特化代码(仍然是同一个类的一部分)
├── x86/
│   └── convolution_x86.cpp            # x86 特化代码
└── vulkan/
    └── convolution_vulkan.cpp         # Vulkan 特化代码

ncnn 的 Convolution 类里有大量的 #ifdef __ARM_NEON__ 来区分不同平台。TNN 的 ConvLayer 只做形状推断,不涉及任何硬件代码——所有硬件相关的实现都在对应的 LayerAcc 子类中。

这种分离的实际好处:当你需要为 TNN 加一个新的 GPU 后端时,只需要在 layer/new_gpu/ 目录下为每个算子写一个新的 LayerAcc,不需要修改任何已有的代码。而在 ncnn 中,加一个新后端意味着在每个算子的 .cpp 文件里都加上 #ifdef NEW_GPU 的分支——这是一个侵入式的修改。

哲学二:Blob = BlobDesc + BlobHandle——不拥有内存的数据描述

TNN 的 Blob 是它的核心数据结构,但它的设计和 ncnn 的 Mat、MNN 的 Tensor 都不一样。Blob 是一个纯描述符,不拥有内存。

// TNN 的 Blob(简化)
class Blob {
public:
    BlobDesc desc_;     // 描述:形状、数据类型、格式
    BlobHandle handle_; // 句柄:指向实际内存的指针
};

// BlobDesc: 元数据描述
struct BlobDesc {
    DeviceType device_type;    // CPU, OPENCL, METAL, ...
    DataType data_type;        // FLOAT, HALF, INT8, ...
    DataFormat data_format;    // NCHW, NC4HW4, NC8HW8, ...
    DimsVector dims;           // 形状 {N, C, H, W}
    std::string name;          // 名字
};

// BlobHandle: 内存句柄
struct BlobHandle {
    void* base;                // 基地址
    uint64_t bytes_offset;     // 偏移量
};

Blob 的设计哲学:数据的描述和数据的存储完全分离。BlobDesc 告诉你”这块数据是什么形状、什么类型、什么格式”,BlobHandle 告诉你”数据在内存中的哪个位置”。但 Blob 自己不管内存的分配和释放——这由 Network 的内存管理模块负责。

这和 ORT 的 Tensor “不拥有内存”的理念类似,但 TNN 的 BlobDesc 比 ORT 的 Tensor 多了两个关键信息:device_type(数据在哪种设备上)和 data_format(数据的内存布局)。这两个信息对后面要讲的 BlobConverter 至关重要。

哲学三:DataFormat 多样化——NC4HW4/NC8HW8/NHC4W4

ncnn 用 elempack 表达打包,MNN 固定用 NC4HW4,TNN 则定义了多种 DataFormat:

enum DataFormat {
    DATA_FORMAT_AUTO    = -1,
    DATA_FORMAT_NCHW    = 0,   // 标准布局
    DATA_FORMAT_NHWC    = 1,   // TensorFlow 风格
    DATA_FORMAT_NC4HW4  = 2,   // 4 通道打包(ARM NEON / OpenCL image2d)
    DATA_FORMAT_NC8HW8  = 3,   // 8 通道打包(ARM v8.2 fp16)
    DATA_FORMAT_NHC4W4  = 4,   // 特殊打包(某些 Metal Shader 偏好)
    // ...
};

NC4HW4 我们在 MNN 那章已经见过。NC8HW8 是 TNN 独有的——它一次打包 8 个通道,专门为 ARM v8.2 的 fp16 NEON 优化设计(128-bit 寄存器装 8 个 fp16)。NHC4W4 则是为某些 Metal Shader 的访问模式优化的。

相比 ncnn 的 elempack(一个整数字段表达打包数)和 MNN 的固定 NC4HW4,TNN 用枚举显式定义了每种布局。这样做的好处是语义更清晰(看到 NC8HW8 就知道是 8 通道打包,而 ncnn 要看 elempack 字段才知道),代价是不如 elempack 灵活(加新的打包格式需要改枚举定义)。

哲学四:BlobConverter 透明转换——数据格式的自动适配

TNN 引入了一个 ncnn 和 MNN 都没有的专门组件:BlobConverter。它的作用是在不同 DataFormat 之间做自动转换。

// BlobConverter(简化)
class BlobConverter {
public:
    // 从外部内存(NCHW/NHWC/BGRA)转换到 Blob 的内部格式
    virtual Status ConvertFromMat(Mat& image, MatConvertParam param,
                                   void* command_queue = nullptr);
    
    // 从 Blob 的内部格式转换到外部内存
    virtual Status ConvertToMat(Mat& image, MatConvertParam param,
                                 void* command_queue = nullptr);
    
    // 自动处理:
    // 1. 色彩空间转换 (BGRA -> RGB)
    // 2. 数据格式转换 (NCHW -> NC4HW4)
    // 3. 数据类型转换 (uint8 -> float)
    // 4. 归一化 (mean subtraction, scale)
};

为什么需要 BlobConverter?因为用户输入的数据(比如相机帧)通常是 BGRA/uint8/NHWC 格式,而 TNN 内部的计算使用 NC4HW4/float 格式。在 ncnn 中,用户需要自己做这些转换(或者用 ncnn 提供的 from_pixels 函数,但那只处理色彩空间和基本的归一化)。在 TNN 中,BlobConverter 封装了全部的转换逻辑,而且它在每个后端上都有优化的实现——OpenCL 版本的 BlobConverter 可以在 GPU 上直接做 BGRA->NC4HW4 的转换,避免了 CPU-GPU 之间的数据搬运。

// 使用 BlobConverter 的典型流程
// 输入: 来自相机的 BGRA 图片 (uint8, NHWC)
// 内部: NC4HW4 (float, 打包布局)
// 输出: 标准 NCHW (float, 给用户的)

auto input_blob = instance->GetInputBlob("input");
BlobConverter input_converter(input_blob);
MatConvertParam param;
param.scale = {1.0/255, 1.0/255, 1.0/255, 0};
param.bias  = {-0.485/0.229, -0.456/0.224, -0.406/0.225, 0};
// 自动完成: BGRA->RGB + uint8->float + 归一化 + NCHW->NC4HW4
input_converter.ConvertFromMat(camera_frame, param);

哲学五:ConstFolder 预计算——编译时消除冗余

TNN 引入了 ConstFolder(常量折叠) 机制:在模型加载时,识别出那些输入全是常量的子图,在加载阶段就执行这些子图,把结果替换为常量 Blob。

优化前:
  Const(shape) -> Reshape -> Const(weight) -> Concat -> Conv -> ...
                                                ↑
                              Const(bias) ------┘

优化后:
  [ConstFolder 在加载时执行 Reshape + Concat]
  PrecomputedWeight -> Conv -> ...
  
  省去了运行时的 Reshape 和 Concat 计算

这和 ORT 的 ConstantFolding 图优化 pass 类似,但 TNN 的实现更直接:它真的在加载时创建一个临时的 Network 执行这些常量子图,然后把结果替换回主图。

// ConstFolder(概念简化)
class ConstFolder {
    Status Fold(NetStructure* structure, NetResource* resource) {
        // 1. 找到所有输入全是常量的节点
        auto const_nodes = FindConstantSubgraph(structure);
        
        // 2. 创建临时 Network 执行这些节点
        auto temp_net = CreateTempNetwork(const_nodes);
        temp_net->Forward();
        
        // 3. 把执行结果替换回原始图
        for (auto& node : const_nodes) {
            auto result = temp_net->GetOutput(node.output);
            resource->constant_map[node.output] = result;
            structure->RemoveLayer(node.name);
        }
        
        return TNN_OK;
    }
};

哲学六:DefaultNetwork 顺序执行——简单直接的调度

和 ncnn 的 Lazy Eval 递归调度不同,TNN 的 DefaultNetwork 采用顺序执行:按拓扑排序好的层顺序,依次调用每一层的 Forward

// DefaultNetwork 的执行(简化)
class DefaultNetwork : public AbstractNetwork {
    Status Forward() {
        // 按拓扑序遍历每一层
        for (auto& layer : layers_) {
            // 1. Reshape(如果输入形状变了)
            layer->layer_acc_->Reshape(layer->input_blobs_,
                                        layer->output_blobs_);
            
            // 2. Forward(实际计算)
            layer->layer_acc_->Forward(layer->input_blobs_,
                                        layer->output_blobs_);
        }
        return TNN_OK;
    }
};

这比 ncnn 的递归调度更简单、更可预测,但也失去了 ncnn 的”只计算需要的分支”的优势。TNN 每次 Forward 都会执行整个图——即使某些输出分支用户不需要。


核心数据结构:Blob

Blob 的完整结构和各部分的关系:

Blob
├── BlobDesc (描述信息)
│   ├── device_type    → CPU / OPENCL / METAL / ...
│   ├── data_type      → FLOAT / HALF / INT8 / ...
│   ├── data_format    → NCHW / NC4HW4 / NC8HW8 / ...
│   ├── dims           → {1, 64, 56, 56}
│   └── name           → "conv1_output"
│
└── BlobHandle (内存句柄)
    ├── base           → 0x7f8a00010000 (内存基地址)
    └── bytes_offset   → 0 (偏移)

和其他框架的数据结构对比:

特性 ncnn Mat MNN Tensor ORT Tensor TNN Blob
拥有内存 可选
通道打包表达 elempack 整数 固定 NC4HW4 DataFormat 枚举
设备信息 内含 OrtMemoryInfo BlobDesc.device_type
格式转换 手动 手动 EP 内部 BlobConverter
复杂度

Blob 的设计在 ncnn Mat 和 MNN Tensor 之间找到了一个平衡点:它不像 Mat 那样过于简单(没有设备信息、没有格式标记),也不像 MNN Tensor 那样过于复杂(没有 Region、没有虚拟内存类型)。它用清晰的 BlobDesc + BlobHandle 分离,让”这块数据是什么”和”这块数据在哪里”成为两个独立的关注点。


推理流程

TNN 的推理流程分为四个阶段。

阶段一:模型加载

TNN tnn;
ModelConfig config;
config.model_type = MODEL_TYPE_TNN;
config.params = {proto_buffer, model_buffer};
tnn.Init(config);

TNN 的模型格式由 .tnnproto(纯文本网络结构)和 .tnnmodel(二进制权重)组成,和 ncnn 的 .param/.bin 类似。Init 阶段解析模型结构、加载权重,并运行 ConstFolder 做常量折叠。

阶段二:创建 Instance

NetworkConfig net_config;
net_config.device_type = DEVICE_ARM;
net_config.network_type = NETWORK_TYPE_DEFAULT;

auto instance = tnn.CreateInst(net_config, status);

CreateInst 创建一个推理实例,这个过程包括:为每一层选择合适的 LayerAcc(根据 device_type 从注册表中查找)、调用每层的 Init、做初始的形状推断和 Reshape。

阶段三:推理执行

// 填充输入
auto input_blob = instance->GetInputBlob("input");
BlobConverter converter(input_blob);
converter.ConvertFromMat(input_image, convert_param);

// 执行推理
instance->Forward();

// 读取输出
auto output_blob = instance->GetOutputBlob("output");
BlobConverter out_converter(output_blob);
out_converter.ConvertToMat(output_mat, out_param);

Forward 内部按拓扑序遍历所有层,依次调用 LayerAcc::Forward。BlobConverter 在推理前后处理数据格式转换。


关键差异:TNN 与其他框架

vs ncnn:架构翻新的得与失

TNN 从 ncnn 继承了”轻量级、移动端优先”的基因,但在架构上做了系统性的改进。BaseLayer/LayerAcc 分离让多后端扩展更加干净,BlobConverter 让数据格式转换更加自动化,ConstFolder 让模型加载时的优化更加彻底。

但 TNN 也失去了 ncnn 的一些优势:它的代码不如 ncnn 简洁(多了一层抽象),它的社区活跃度不如 ncnn(4.5K vs 22K star),它没有 ncnn 的 Vulkan GPU 后端(TNN 选择了 OpenCL)。

vs MNN:精简 vs 全能

TNN 在架构上比 MNN 更干净——它没有 Geometry Compiler 那样的复杂机制,也没有端侧训练和 LLM 支持。TNN 做的事情更少,但做得更清晰。如果你需要一个”刚好够用”的推理引擎(传统 CV 模型 + 多后端支持),TNN 是一个比 MNN 更好的选择——因为你不需要为用不到的功能付出代码复杂度的代价。

vs ORT:内置后端 vs 插件后端

TNN 的后端(ARM/OpenCL/Metal)是内置的,通过 LayerAcc 注册表管理;ORT 的后端是插件化的,通过 EP 接口加载。TNN 的方式更轻量(不需要动态库加载机制),ORT 的方式更灵活(可以运行时加载新后端)。


适用场景与局限

最适合 TNN 的场景

需要 OpenCL GPU 加速的 Android 应用。TNN 的 OpenCL 后端在高通 Adreno 和 ARM Mali GPU 上有不错的性能,配合 BlobConverter 可以实现从相机帧到推理结果的全 GPU 路径(输入在 GPU 做格式转换、推理在 GPU、输出直接用或传回 CPU)。

腾讯系应用的 AI 功能。TNN 在腾讯内部有广泛使用(微信、QQ、微视等),有大量的模型和使用场景经验。如果你在腾讯系的生态中工作,TNN 的内部文档和支持会比较完善。

想从 ncnn 迁移但需要更好的多后端支持。如果你之前用 ncnn 但觉得它的 Vulkan GPU 后端不够稳定、加新后端太麻烦,TNN 提供了一条平滑的迁移路径——核心概念类似,但架构更现代化。

TNN 的局限

不支持端侧 LLM。和 ncnn 一样,TNN 没有 LLM 推理能力。如果你需要在手机上跑大语言模型,应该看 MNN。

不支持端侧训练。TNN 是纯推理引擎,没有自动微分和反向传播。MNN 支持端侧训练。

社区活跃度较低。4.5K star 和相对较少的 GitHub 活动意味着遇到问题时获取社区支持可能比较困难。ncnn(22K star)和 ORT(21K star)在这方面好得多。

没有 Vulkan 后端。TNN 的 GPU 后端是 OpenCL 和 Metal,没有 Vulkan。在 Vulkan 驱动更稳定的新设备上,ncnn 的 Vulkan 后端可能是更好的选择。

不支持 NPU。和 ncnn 一样,TNN 没有 NNAPI 集成或专用 NPU 后端。如果需要 NPU 加速,应该看 Paddle-Lite(NNAdapter)或 ORT(NNAPI EP)。


代码结构

TNN 的源码约 800 个文件,组织清晰:

TNN/source/tnn/
├── core/                              # 核心抽象
│   ├── blob.h / .cc                   # Blob (BlobDesc + BlobHandle)
│   ├── abstract_device.h / .cc        # 设备抽象
│   ├── abstract_network.h             # 网络抽象
│   ├── default_network.h / .cc        # 默认网络(顺序执行)
│   ├── instance.h / .cc               # 推理实例
│   └── tnn.h / .cc                    # 顶层入口
│
├── layer/                             # 算子层
│   ├── base_layer.h / .cc             # BaseLayer 基类
│   ├── conv_layer.h / .cc             # 卷积(形状推断)
│   ├── relu_layer.h / .cc             # ReLU(形状推断)
│   └── ...
│
├── device/                            # 后端设备
│   ├── arm/                           # ARM 后端
│   │   ├── arm_device.h / .cc         # ARM 设备管理
│   │   ├── acc/                       # ARM LayerAcc 实现
│   │   │   ├── arm_conv_layer_acc.h / .cc
│   │   │   ├── arm_relu_layer_acc.h / .cc
│   │   │   └── ...
│   │   └── ...
│   ├── x86/                           # x86 后端
│   │   └── acc/
│   ├── opencl/                        # OpenCL 后端
│   │   ├── opencl_device.h / .cc
│   │   ├── acc/
│   │   └── opencl_kernels/            # OpenCL kernel 源码
│   ├── metal/                         # Metal 后端 (iOS)
│   │   └── acc/
│   └── cpu/                           # 通用 CPU(参考实现)
│
├── interpreter/                       # 模型解析
│   ├── tnn/                           # TNN 格式解析
│   └── ncnn/                          # ncnn 格式兼容
│
├── optimizer/                         # 图优化
│   ├── net_optimizer_manager.h / .cc
│   ├── const_folder.h / .cc           # 常量折叠
│   └── graph_matcher/                 # 子图匹配(算子融合)
│
└── utils/                             # 工具
    ├── blob_converter.h / .cc         # BlobConverter
    └── ...

关键阅读路径:

  1. 入门core/blob.h -> layer/base_layer.h -> core/default_network.ccForward 函数)
  2. 理解双层分离layer/conv_layer.cc(形状推断)-> device/arm/acc/arm_conv_layer_acc.cc(计算)
  3. 理解 BlobConverterutils/blob_converter.cc -> device/arm/arm_blob_converter.cc
  4. 理解 ConstFolderoptimizer/const_folder.cc
  5. 理解 OpenCLdevice/opencl/opencl_device.cc -> device/opencl/acc/opencl_conv_layer_acc.cc

小结

TNN 的设计哲学可以用一个词概括:重构。它不是从零开始设计一个全新的引擎,而是在 ncnn 的设计基因上做了系统性的架构升级。BaseLayer/LayerAcc 双层分离让多后端扩展变得干净,Blob 的 BlobDesc/BlobHandle 分离让数据描述和数据存储解耦,BlobConverter 让数据格式转换自动化,ConstFolder 让编译时优化更加彻底。

它的核心价值在于在 ncnn 的简洁性和 MNN 的全面性之间找到了一个平衡点:它比 ncnn 更适合多后端扩展,但没有 MNN 那么复杂;它比 MNN 更容易阅读和理解,但没有 ncnn 那么极致简洁。约 800 个文件的代码量,既不会让人望而生畏,也足以展示一个现代推理引擎应有的架构设计。

对于犀牛鸟竞赛的参赛者来说,TNN 的 BaseLayer/LayerAcc 双层设计是一个重要的学习对象——如果你在 ncnn 上开发新功能时遇到了”加新后端太痛苦”的问题,TNN 的解决方案值得借鉴。同时,如果你已经读懂了 ncnn 的源码,TNN 的代码会显得非常亲切——它们的核心概念和代码风格有很多相似之处。

下一章,我们将看到一个更加不同的设计思路:Paddle-Lite 如何用 NNAdapter HAL 和三维内核描述来解决”13+ 异构后端”的适配问题。


导读目录:README.md 上一章:Ch13: ONNX Runtime——标准集装箱港口 下一章:Ch15: Paddle-Lite——万能充电器