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

Ch04: 神经网络推理的本质——从训练到部署

Part 2: 移动端推理基础(从零开始) 前置章节:Ch03: 前置知识检查清单 后续章节:Ch05: 为什么手机和服务器不一样


先讲一个做菜的故事

假设你是一家餐厅的主厨,正在研发一道新菜。研发阶段你会反复试错:放多少盐?火候多大?调料比例怎么调?每做一次,你尝一口,觉得”太咸了”就减盐,觉得”不够香”就加料。经过几十次甚至几百次试错,你终于找到了最完美的配方——盐3克、糖2克、酱油5毫升、大火翻炒90秒。

研发完成后,你把这个配方写成一张标准化菜谱卡,交给后厨的厨师们。他们不需要再试错了,只需要严格按照菜谱上的参数执行:称3克盐、量5毫升酱油、计时90秒。每一道端上桌的菜都应该味道一致。

这个故事就是深度学习”训练”与”推理”的本质区别。训练是”研发菜品”的过程——反复调整参数直到找到最优组合;推理是”按菜谱做菜”的过程——拿着已经确定好的参数,对新输入执行固定的计算流程,产出结果。

整本导读要讲的”推理引擎”,就是那个帮你在手机上”按菜谱做菜”的工具。它不关心菜谱是怎么研发出来的,它只关心一件事:怎么用最少的时间、最少的资源,把这道菜做出来。


训练 vs 推理:四个本质区别

训练和推理虽然都在运行同一个神经网络,但它们的运行模式截然不同。理解这些区别是理解”为什么需要专门的推理引擎”的前提。

区别一:方向不同——单程票 vs 往返票

训练是一个”往返”的过程。数据先从输入层流到输出层(前向传播),计算出预测结果;然后把预测结果和正确答案的差距(损失值)从输出层倒推回输入层(反向传播),计算每个参数应该怎么调整。这就像你做完菜后尝了一口,发现太咸,然后倒推”盐放多了0.5克”这个信息回去修改配方。

推理只有”单程”——数据从输入层流到输出层,计算出结果就结束了。没有反向传播,没有参数调整。这意味着推理的计算量大约只有训练的一半(甚至更少,因为反向传播通常比前向传播更耗时)。更重要的是,推理不需要存储中间层的梯度信息,内存需求也大幅降低。

回到做菜的类比:研发阶段你需要记住”这次放了多少盐、结果怎样”,这样才能调整下一次的用量。但按菜谱做菜时你不需要记这些——做完就做完了,端上桌就行。

区别二:精度要求不同——显微镜 vs 放大镜

训练时需要非常高的数值精度——通常使用 fp32(32位浮点数,也叫单精度)。为什么?因为反向传播计算的梯度值往往非常小,比如0.000001这样的数字。如果精度不够,这些微小的梯度就会被”四舍五入”成0,参数就调不动了——这叫做”梯度消失”。

推理就没这个限制了。既然不需要计算梯度,我们可以用更低的精度来表示数据和权重:fp16(16位浮点数,范围和精度都更小但够用)甚至 int8(8位整数,只有256个可能的值)。用更低精度意味着:

这就是为什么后面会有一整章(Ch10)专门讲”量化”——把 fp32 模型压缩成 int8 模型的技术。现在只需要知道:推理可以容忍精度损失,而这个特性是移动端部署的关键优化空间。

区别三:批大小不同——大锅饭 vs 小炒

训练时通常一次处理一大批数据(batch size = 32、64 甚至 256),因为GPU擅长并行处理大量相似的计算,大批量能让GPU的利用率更高——就像食堂做饭,一次炒一大锅比一份一份地炒效率高得多。

推理时通常一次只处理一张图片或一句话(batch size = 1)。原因很简单:用户拍了一张照片想知道这是什么花,他不会等你凑齐64张照片再一起识别。实时性是推理场景的核心需求。

这个区别直接影响了优化策略。训练主要优化”吞吐量”(单位时间处理多少数据),推理主要优化”延迟”(一条数据从输入到输出要多久)。同一个卷积算子,在 batch=64 时最优的实现方式,和在 batch=1 时最优的实现方式,可能完全不同。

区别四:运行环境不同——豪华厨房 vs 路边摊

训练通常在高配服务器上进行:一台训练服务器可能配备 8 张 NVIDIA A100 GPU(每张80GB显存、312 TFLOPS 算力),256GB 系统内存,不限电力,有专业水冷散热。这就像一个设备齐全的五星级酒店厨房。

推理——特别是我们关注的移动端推理——发生在用户的手机上。一部 2024 年的旗舰手机有 8GB 内存(其中大部分被系统和其他 App 占用),骁龙 8 Gen3 的 AI 算力约 45 TOPS,整机功耗只有 3-5W,被动散热(没有风扇),温度高了还会降频。这就像在路边摊做菜——灶台小、火力弱、还怕热。

这个巨大的环境差异,是移动端推理引擎存在的根本原因。我们不能简单地把训练框架搬到手机上——就像你不能把五星级酒店的工业灶台搬到路边摊上一样。我们需要专门为”路边摊”设计的灶具:更小巧、更省燃气、更适应露天环境。

维度 训练 推理(移动端)
方向 前向+反向 仅前向
精度 fp32 fp32/fp16/int8
批大小 32-256 通常 1
设备 服务器GPU集群 手机/嵌入式
内存 256-512GB 4-8GB(可用 2-4GB)
算力 312+ TFLOPS (A100) ~45 TOPS (骁龙8 Gen3)
功耗 300W+/GPU 整机 3-5W
散热 风扇/水冷 被动散热
优化目标 吞吐量(数据/秒) 延迟(毫秒/次)

计算图:神经网络的”菜谱”长什么样

前面反复提到”按菜谱做菜”,那这个”菜谱”到底长什么样?在深度学习中,这个菜谱就是计算图(Computational Graph)

想象你要做一顿丰盛的早餐:煎蛋、烤面包、煮咖啡、摆盘上桌。你可以画一张流程图来描述整个过程。流程图里有两种东西:一种是”操作”(打蛋、倒油、开火……),用方框表示;另一种是”数据”(鸡蛋、食用油、面包片……),用箭头表示数据从一个操作流向下一个操作。

计算图就是这样一张流程图。在神经网络中:

一个简单的图像分类网络的计算图可能长这样:

输入图片 [3, 224, 224]
    ↓
  [卷积] → 提取图像特征
    ↓
[BatchNorm] → 归一化特征值
    ↓
  [ReLU] → 过滤负值
    ↓
  [池化] → 压缩空间尺寸
    ↓
  [全连接] → 将特征映射到类别
    ↓
 [Softmax] → 输出概率分布
    ↓
输出: [猫: 0.92, 狗: 0.05, 鸟: 0.03]

为什么”计算图”这个概念重要?因为推理引擎做的第一件事,就是把你的模型文件解析成一张计算图。然后它可以对这张图做各种优化:合并节点(把卷积和BatchNorm合成一步)、删除无用节点、重排执行顺序。这些优化是推理引擎的核心价值之一,后续章节(Ch21)会详细展开。


常见算子:计算图里的”基本操作”

计算图中的每个节点是一个”算子(Operator,简称 Op)”。就像做菜有”切、炒、煮、蒸”几种基本操作一样,神经网络也有一组基本算子。理解最常见的几个,对后续章节的阅读很有帮助。

卷积(Convolution)是神经网络中计算量最大的算子,通常占总计算量的 70%-90%。它的作用是用一个小窗口(卷积核,通常 3x3 或 1x1)在输入图像上滑动,每滑到一个位置就做一次乘加运算,提取局部特征。可以类比为”用放大镜扫描一张照片的每个角落,在每个位置总结一下看到了什么”。卷积是六个推理引擎花最多精力优化的算子——ncnn 的 Convolution_arm.cpp 一个文件里就包含了五六种不同的卷积实现策略。

全连接(Fully Connected / Dense)把前面所有的特征”拉平”成一个一维向量,然后做一次大矩阵乘法。作用是把空间上的局部特征汇总成全局的判断依据。好比”把放大镜看到的所有细节汇总到一张报告里”。本质上就是矩阵乘法(MatMul),这是另一个推理引擎重点优化的算子。

池化(Pooling)用一个小窗口(比如 2x2)在特征图上滑动,每个窗口只保留一个值(最大值或平均值)。效果是把特征图的长宽各缩小一半,减少后续计算量。类比”把一张1000万像素的照片缩小到250万像素——关键信息还在,但数据量小了很多”。

激活函数(Activation)对每个数值做一个简单的非线性变换。最常见的是 ReLU(Rectified Linear Unit),它做的事情极其简单:负数变成0,正数不变。数学表达就是 output = max(0, input)。虽然简单,但没有它的话,无论你叠多少层网络,最终效果都等价于一个线性变换——加上激活函数才能让网络学到非线性的复杂模式。

BatchNorm(批归一化)把每一层的输出值”校准”到均值为0、方差为1的标准分布上,防止数值在层与层之间越来越大(爆炸)或越来越小(消失)。有一个重要的优化常识:推理阶段的 BatchNorm 可以和前面的卷积层”合并”成一个操作(称为”BN折叠”或”BN融合”),不需要单独执行。这是几乎所有推理引擎都会做的基础图优化。

Softmax 把一组任意实数转换成概率分布——所有值在 0 到 1 之间且总和为 1。通常出现在分类网络的最后一层,告诉你”这张图是猫的概率 92%,是狗的概率 5%”。

以上六种算子覆盖了绝大多数经典 CNN(卷积神经网络)模型的计算。现代的 Transformer 架构还会大量使用 MatMul、LayerNorm、Attention 等算子,但基本原理是一样的——都是计算图中的节点,输入张量、执行运算、输出张量。


前向传播的实际过程

理解了计算图和常见算子,现在可以看看”推理”到底是怎么一步步执行的了。

前向传播(Forward Pass)就是数据从计算图的输入端一路流到输出端的过程。假设我们有一个简单的三层网络要对一张 224x224 的 RGB 图片做分类:

输入: [1, 3, 224, 224]    → 1张图片, 3个颜色通道, 224x224像素
  ↓
Conv1: 64个3x3卷积核      → 输出 [1, 64, 224, 224]
  ↓
ReLU                       → 输出 [1, 64, 224, 224](负值变0)
  ↓
MaxPool: 2x2               → 输出 [1, 64, 112, 112](尺寸减半)
  ↓
Conv2: 128个3x3卷积核     → 输出 [1, 128, 112, 112]
  ↓
ReLU                       → 输出 [1, 128, 112, 112]
  ↓
GlobalAvgPool              → 输出 [1, 128, 1, 1](全局平均)
  ↓
Flatten                    → 输出 [1, 128]
  ↓
FullyConnected: 1000       → 输出 [1, 1000](1000个类别的分数)
  ↓
Softmax                    → 输出 [1, 1000](1000个类别的概率)

每一层做的事情本质上都是一次或几次矩阵运算。以第一个卷积层 Conv1 为例,它要执行大约 1.7 亿次乘加运算(FLOPs)——这只是一层。一个完整的 ResNet-50 网络有约 40 亿次 FLOPs。在骁龙 8 Gen3 的 CPU 上以 fp32 精度跑 ResNet-50 的单帧推理大约需要 30-50 毫秒。

这里有一个关键观察:前向传播的执行顺序是确定的——前一层的输出就是后一层的输入,必须严格按顺序来。但中间结果是临时的——一旦后一层读取了前一层的输出,前一层的输出就不再需要了。这个特性是内存优化的基础(Ch08 会详细讲):我们可以在后一层开始计算后,立即释放前一层输出所占的内存。ncnn 的 Light mode 就是这么做的。


推理引擎的职责

把上面的过程归纳一下,一个推理引擎需要做四件事,环环相扣。

第一步:加载模型。 推理引擎需要读取模型文件(不同引擎用不同的格式,Ch09 详解),解析出计算图的结构(哪些算子、怎么连接)和权重数据(每个算子的参数值)。这一步类似于”读懂菜谱”。模型文件可能只有几MB(YOLOv8n 约 6MB),也可能几个GB(LLM 模型)。

第二步:分配内存。 推理引擎需要为所有中间结果(激活值)分配内存空间。聪明的引擎不会给每个中间结果都分配独立的内存——它会分析哪些中间结果的”生命周期”不重叠,让它们共用同一块内存。这就像一个小餐厅只有三个盘子,但通过合理安排上菜顺序(吃完前菜洗盘子再装主菜),照样能服务一桌完整的宴席。

第三步:调度计算。 推理引擎按照计算图的依赖关系,一层一层(或更聪明地)执行每个算子。每个算子的执行要调用对应硬件上的优化实现——ARM CPU 上调用 NEON 优化版本,GPU 上调用 Compute Shader 版本。调度策略因引擎而异:ncnn 用按需递归(从输出反推需要哪些层),MNN 用 Pipeline 顺序执行,Paddle-Lite 用编译好的指令序列逐条执行。

第四步:返回结果。 推理完成后,把输出张量的数据返回给调用者。对于图像分类就是一个概率向量,对于目标检测就是一组检测框的坐标和类别。

用 ncnn 的代码来看,这四步在 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);    // 设置输入数据
ex.extract("output", output_mat); // 第二三四步:触发推理,返回结果
// ncnn 在 extract 时才真正执行计算(按需递归)

这四行代码背后,是推理引擎数万行的底层优化:内存池、SIMD向量化、算子融合、缓存优化、多线程调度。整本导读后续的二十多章,本质上都是在拆解这四行代码背后发生了什么。


为什么不直接用 PyTorch 做推理

到这里你可能会问:既然推理就是跑一遍前向传播,PyTorch 本身不就能做吗?为什么还需要 ncnn、MNN 这些专门的推理引擎?

确实,PyTorch 可以做推理。但在移动端场景下,直接用 PyTorch 有四个致命问题。

问题一:体积太大。 PyTorch 的运行时(runtime)包括 Python 解释器、libtorch 库、各种依赖库(NumPy、Protobuf等),安装包超过 100MB。而 ncnn 的核心库只有约 1MB(静态编译后甚至可以更小),因为它零第三方依赖——不依赖 Protobuf、不依赖 Eigen、不依赖 BLAS,所有代码手写。一个手机 App 如果因为 AI 功能就增加 100MB 体积,用户下载意愿会大幅下降。

问题二:启动太慢。 PyTorch 需要初始化 Python 运行时、导入各种模块、构建计算图。在服务器上这个过程可能只需要几秒,但在手机上可能需要十几秒甚至更久——用户打开相机准备拍照识别花卉,等十几秒才能用?不可接受。ncnn 的模型加载通常在 10 毫秒以内完成。

问题三:依赖关系复杂。 PyTorch 依赖 Python 运行时,而手机操作系统(Android/iOS)的原生开发语言是 Java/Kotlin/Swift/C++。在 App 中嵌入一个完整的 Python 运行时不仅增大体积,还引入大量兼容性问题。ncnn 是纯 C++ 库,可以直接通过 JNI(Java Native Interface)集成到 Android App 中,或直接在 iOS 项目中链接。

问题四:缺乏移动端专项优化。 PyTorch 的算子实现主要针对服务器端 GPU(CUDA)优化,对 ARM CPU 的 NEON 指令、移动 GPU 的 Compute Shader、NPU/DSP 等移动端专属硬件几乎没有优化。ncnn 的每个关键算子都有手写的 ARM NEON 版本,MNN 有专门的 Metal/OpenCL 后端,MACE 甚至针对高通 Hexagon DSP 做了整图下推优化。

维度 PyTorch ncnn(推理引擎)
核心库体积 >100MB ~1MB
模型加载时间 秒级 毫秒级
语言依赖 Python 纯 C++
ARM NEON 优化 基础 手写极致优化
移动 GPU 支持 Vulkan/OpenCL/Metal
NPU 支持 部分引擎支持
内存优化 通用 移动端专项(Light mode 等)

用一句话总结:PyTorch 是”什么都能做的通用工具箱”,推理引擎是”为特定场景打造的专业工具”。在服务器上用 PyTorch 做推理完全可以(事实上很多线上服务就这么做,或用 TorchScript/Torch-TensorRT 加速),但在手机上,你需要推理引擎。


训练框架 vs 推理引擎的分工

理解了上面的区别后,可以看到整个深度学习的工作流形成了一个清晰的分工:

┌────────────────────┐     ┌────────────────────┐     ┌────────────────────┐
│   训练框架          │     │   模型转换工具       │     │   推理引擎          │
│   (PyTorch/TF)     │ ──→ │   (ONNX/PNNX)      │ ──→ │   (ncnn/MNN/ORT)   │
│                    │     │                    │     │                    │
│ 研发菜品            │     │ 翻译菜谱            │     │ 按菜谱做菜          │
│ 服务器上运行         │     │ 格式转换桥梁         │     │ 手机上运行          │
│ fp32 高精度         │     │ 图优化+量化         │     │ fp16/int8 高效率    │
└────────────────────┘     └────────────────────┘     └────────────────────┘

训练框架负责用大量数据”教”出一个好模型(确定最优参数)。模型转换工具负责把训练框架的模型格式”翻译”成推理引擎能读懂的格式,同时做一些优化(算子融合、量化等)。推理引擎负责在目标设备上高效地执行推理。

这个分工的核心洞察是:训练和推理的需求差异太大,试图用一个框架同时满足两者,要么训练性能打折扣,要么推理性能打折扣。专业化分工让每个环节都能做到极致。

不过也有例外——MNN 试图打通这个链路,在一个框架中同时支持推理和端侧训练。这种”全栈”路线有其独特价值(比如在手机上做模型微调),但代价是代码量和复杂度显著增加(MNN 约 2000 个源文件,而 ncnn 只有约 500 个)。


推理的性能衡量标准

既然推理引擎的核心工作是”高效执行前向传播”,那怎么衡量”高效”呢?移动端推理有三个最重要的指标。

延迟(Latency):一次推理从输入到输出需要多少时间,通常以毫秒为单位。对实时应用来说,延迟是最核心的指标。比如手机上的实时目标检测,一般要求延迟 < 33ms(即帧率 > 30fps)。延迟又可以细分为:冷启动延迟(第一次推理,包含模型加载)和热推理延迟(模型已加载,只是执行计算)。

内存占用(Memory Footprint):推理过程中消耗的总内存,通常包括模型权重(常驻内存)和中间激活值(临时内存)。在移动端,内存是最稀缺的资源——超过系统阈值,App 会被强制杀死(OOM,Out Of Memory)。Ch08 会专门讨论这个话题。

功耗(Power Consumption):推理消耗的电量。这个指标在服务器端几乎没人关心(反正插着电),但在手机上至关重要。用户不希望用了一下 AI 功能,电量就掉了 10%。功耗和计算量、硬件选择(CPU vs GPU vs NPU)、运行频率都有关系。

这三个指标往往互相矛盾:要更低延迟就要更高频率运行,但频率高了功耗就大;要更省内存就要减少缓存和预计算,但这可能增加延迟。推理引擎的设计本质上就是在这三者之间寻找最佳平衡点。


本章小结

本章建立了几个核心概念。第一,训练是”研发菜品”(往返传播、高精度、大批量、服务器),推理是”按菜谱做菜”(单向传播、可低精度、单条数据、用户设备)。第二,计算图是神经网络


导读目录:README.md 上一章:Ch03: 前置知识检查清单 下一章:Ch05: 为什么手机和服务器不一样——移动端约束全解