外观
推理:从前向传播到推理引擎
一句话定义:推理(inference)是模型上线之后、每次收到输入时执行的那一次前向传播(forward pass)——把训练好的权重参数固化为只读资产,对新的输入计算出输出。
行业洞察:绝大多数公司的 GPU 预算中,推理的计算量最终会超过训练。一个在内部训练了 3 个月的推荐模型,上线后每天被调用上亿次;OpenAI 曾披露推理成本约占总基础设施支出的 40% 以上,而随 LLM 爆发,推理在总 GPU 采购中的占比只会更高。这也是为什么"部署工程师"的核心议题不是"把模型跑起来"(那是训练的延伸),而是"把模型跑得又对、又快、又省"——前向传播只做一次,但要做对、做到极致。
一、推理:一次只读的前向传播
1. 训练与推理的本质差异
训练(training)和推理共享同一套前向传播数学,但工程的约束完全不同。理解这张差异表,是读懂后面所有部署优化的前提:
| 维度 | 训练 | 推理 |
|---|---|---|
| 传播方向 | 前向 + 反向(backward) | 仅前向 |
| 权重状态 | 持续更新(可写) | 冻结(只读) |
| Batch 大小 | 大(64~1024),追求吞吐 | 多变:在线以 1 为主,离线可大 |
| 延迟敏感度 | 不敏感,分钟级可接受 | 极敏感,P99 要毫秒级 |
| 数值精度 | FP32 / BF16(需保梯度精度) | FP16 / INT8 / INT4 皆可 |
| 显存需求 | 权重+梯度+优化器状态+激活(约 16~20 倍参数量) | 权重+激活(约 1.2~2 倍参数量) |
| 核心瓶颈 | 算力(FLOPs) | 带宽(memory-bound,见下文) |
| 正确性判断 | 损失函数收敛 | 输出与预期一致(有时还要保证可复现) |
| 失败代价 | 重训 | 线上事故 |
一个常见的显存误区
训练一个 7B 模型需要约 200GB+ 显存(含梯度与优化器状态),而部署它只需约 14GB(FP16 权重)——"训练这么大,部署肯定更吃显存"是错的。推理不用保存梯度,权重冻结后还可以量化压缩,显存压力反而小一个数量级。详情见 GPU 与硬件选型。
2. 推理只做一件事:算 y = f(x)
无论模型多复杂,推理的计算图都是把输入逐层变换到输出。以一个 BERT 分类模型为例:
text
输入 token 序列
│ embedding 查表
▼
12 层 Transformer 编码器(每层含 Self-Attention + FFN)
│ LayerNorm / residual
▼
[CLS] 向量 → 线性层 → softmax
▼
类别概率分布(推理产物)关键工程含义有三条:
- 权重只读:上线后权重不再变化,因此可以提前做图优化、算子融合、甚至编译成专用二进制(如 TensorRT engine),把运行时开销降到最低。
- 输出可缓存:训练时同一输入只出现一次;推理时同一输入(如热门商品的 embedding)会被反复查询——这让结果缓存成为推理服务最重要的优化手段之一。
- 离线可预计算:凡是与输入无关的计算(常量折叠、权重重排),都能在部署阶段完成,运行时只执行必要算子。
3. 推理的三种形态
按调用方式,推理可以分为三类,后文 部署架构模式 会展开:
- 在线推理(online inference):收到请求立即返回,毫秒级,如推荐、翻译、对话。本文第 三 节的延迟构成就是讲它。
- 离线推理(offline / batch inference):一次性处理大批量数据,小时级,如生成用户画像、日报。
- 流式推理(streaming inference):事件驱动、逐条处理,如实时风控、欺诈检测。
二、一次在线推理的延迟构成
1. 端到端时间线:延迟藏在"非模型"部分
很多团队优化了大半年模型,压测发现 P99 没降下来——原因常常是瓶颈根本不在模型。一次典型在线推理的延迟构成如下:
text
客户端 API 网关 推理服务 GPU
│ ① 请求序列化(JSON) │ │ │
│───────────────────────►│ ② 网络传输(RTT) │ │
│ │──────────────────────►│ │
│ │ ③ 反序列化/参数校验 │ │
│ │ ④ 预处理(特征拼接/归一化) │ │
│ │ ⑤ 排队等待(batch/调度) │──► ⑥ 前向传播 │
│ │ ⑦ 后处理(softmax/argmax)│ │
│ │ ⑧ 响应序列化 │ │
│◄───────────────────────│──────────────────────────│ │
│ ⑨ 网络回传(RTT) │ │ │以推荐场景一个典型数字为例(单卡 T4,单条请求,batch=1):
| 阶段 | 典型耗时 | 占比 | 说明 |
|---|---|---|---|
| ① 序列化/网络(各一次) | 0.5~2 ms | 5~10% | 大 payload 时显著 |
| ④ 预处理 | 0.5~3 ms | 5~15% | 特征拼接、归一化、embedding 查表 |
| ⑤ 排队等待 | 0~100 ms | 波动最大 | 高并发时是主要延迟来源 |
| ⑥ 模型前向 | 2~50 ms | 40~80% | batch=1 时实际被带宽卡住 |
| ⑦ 后处理 | 0.1~0.5 ms | <5% | softmax / 过滤 / 截断 |
先测后优化的顺序
优化延迟的第一原则是先打点再动手:用 OpenTelemetry 把 ②~⑧ 各阶段都埋上点(详见 监控与可观测性)。经常发现"模型只占 40% 延迟,排队占 40%"——那该优化的是调度而不是模型。
2. 决定前向延迟的两个物理量:算力与带宽
模型前向(⑥)的快慢由两个硬件物理量决定:
- 算力(FLOPs):每秒能算多少次浮点运算,单位 TFLOPS。
- 带宽(memory bandwidth):每秒能从显存读出/写入多少数据,单位 GB/s。
对每个算子,存在一个"算术强度"(arithmetic intensity)比值:每字节数据要配多少次运算。比值低于硬件平衡点(Roofline 模型中的 ridge point)就是 memory-bound(带宽受限),高于才是 compute-bound(算力受限)。
推理几乎总是 memory-bound:一次矩阵乘 [1, 4096] × [4096, 4096] 需要读 64MB 权重、只做 34 亿次运算;A100 的 FP16 算力约 312 TFLOPS、带宽约 2TB/s,其平衡点约 156 FLOP/byte,而这笔计算只有 52 FLOP/byte——时间全花在把权重从显存搬进计算单元上。这就是"推理吃带宽、不吃算力"这句话的出处,也是 量化 能带来接近线性提速的根本原因:INT8 权重占一半字节,搬运时间几乎减半。
3. batch=1 的残酷现实
在线推理默认 batch=1(一条请求一次前向),此时没有足够的并行计算量喂饱 GPU,GPU 利用率常只有个位数百分比。这是推理与训练最大的工程落差:训练工程师追求"把 GPU 打满",推理工程师追求的恰恰是"在小 batch 下把延迟压到最低"。解法是动态批处理(dynamic batching)、多请求共享,详见 性能优化与容量规划。
三、推理引擎在做什么
1. 为什么 PyTorch 直接跑推理不够快
模型在 PyTorch 里"能跑",但部署时通常要过一道推理引擎(inference engine,如 ONNX Runtime、TensorRT、OpenVINO、TFLite)。原因是 PyTorch 的 eager mode(即时执行模式)为训练做了大量让步:
- 每个算子独立派发、动态调度,Python 解释开销巨大;
- 算子边界保留了大量中间张量分配;
- 不做内存复用,显存申请/释放频繁;
- 泛化的 kernel 没有针对具体 shape 特化。
推理引擎则把这些"训练时无所谓、推理时昂贵"的开销全部消灭。它做的事可以归纳为四步。
2. 计算图优化(graph optimization)
先拿到整张计算图,再做与输入无关的变换:
text
原始图 优化后
┌──────────────────┐ ┌──────────────────┐
│ conv ── bn ── relu│ 融合 │ conv+bn+relu │
│ add ─────────────│ ───► │ (单 kernel) │
│ mul(常量) │ 折叠 │ 常数 8.0 │
│ softmax(常输入) │ ───► │ 预计算 0.5 │
└──────────────────┘ └──────────────────┘具体手段包括:
- 算子融合(operator fusion):把
conv + batch_norm + relu合并为一个 kernel,省去中间张量的写回与再读取。对 memory-bound 场景,融合省下的带宽往往就是提速本身。 - 常量折叠(constant folding):权重与输入无关的运算在部署阶段预先算掉,例如 BatchNorm 在推理期可以折叠进卷积的权重与偏置(业界标准做法,PyTorch 里
torch.quantization.fuse_modules就是这个)。 - 内存规划(memory planning):提前分析各张量的生命周期,复用同一块显存,避免运行时反复
malloc。典型例子是 ONNX Runtime 的内存 arena 与 TensorRT 的 memory pool。 - 死代码消除与冗余消除:删掉训练专用算子(dropout、gradient 相关节点)。
3. kernel 选择:同一个算子,几十种实现
同样一个矩阵乘,不同 shape、不同布局、不同硬件上最快的实现截然不同。引擎会在部署时做 kernel autotuning:对一组候选 kernel 做 benchmark,选出最快者并缓存。典型差异来源:
- 输入 shape:
[1, 4096]×[4096, 4096](推理常见)与[64, 4096]×[4096, 4096](训练常见)的最优 tile 划分完全不同。 - 数据布局:NCHW 与 NHWC 在部分硬件上差异可达 30%+;TensorRT 甚至会按需插入布局转换(layout transform)。
- 张量核心(tensor core)可用性:FP16 走 tensor core 还是 CUDA core,性能差 5~10 倍。
- tile 大小与并行策略:算子内切分到线程块、warp 的方式。
这就是"为什么引擎能比原框架快 2~10 倍"的答案——不是数学变了,而是同样的数学被换成了更贴近硬件的执行方式。TensorRT 在 fp16 下对 ResNet-50 的延迟可以做到 PyTorch 的 1/3 到 1/5,正是图优化 + kernel 特化 + 编译三者的叠加。
4. 引擎的代价:可移植性换性能
推理引擎的优化越激进,对输入 shape、模型结构、硬件型号的假设就越强。因此:
- 动态 shape 受限:TensorRT 允许动态 shape,但要在预设范围内(如 batch 1~32),超范围需重新优化;
- 构建一次、部署一处:为 A100 构建的 engine 不能直接跑在 T4 上;
- 精度可能漂移:算子重排、低精度 kernel、不同的 accumulation 顺序会让结果与训练框架有微小差异。
关于格式层面的选择,见 模型格式与转换;关于"为什么引擎结果和原框架不一样"的排查,见 常见陷阱与反模式。
四、CPU 推理与 GPU 推理
1. 两者的物理本质不同
| 维度 | CPU | GPU |
|---|---|---|
| 核心数 | 数十(大核) | 数千~数万(小核) |
| 缓存 | 大(L2/L3 数十 MB) | 小(主要靠显存带宽) |
| 擅长 | 串行逻辑、分支、小 batch | 大规模并行矩阵运算 |
| 典型延迟 | 单 token 快,吞吐有限 | batch 起来后吞吐极高 |
| 精度 | FP32 / INT8(AVX-512、AMX) | FP16/INT8/INT4 + tensor core |
2. 什么时候该用 CPU
低 QPS + 小模型是 CPU 的舒适区:一个 100MB 的分类模型在 CPU 上延迟 5~10ms,而 GPU 上也只有 2~3ms——为了省那几毫秒花几万块买 GPU 不划算。LLM 时代 CPU 推理在 llama.cpp 的优化下(AVX2/AVX-512、oneDNN)也能跑到可用的速度,例如 7B 量化模型在 8 核服务器上约 5~10 token/s,适合个人与内部工具场景。
3. 一个简单的决策规则
text
模型前向 < 20ms 且 QPS < 50 且模型 < 1GB → CPU 足够
模型是纯矩阵运算、batch 可合并、延迟敏感 → GPU
边缘设备 / 无 GPU 环境 → CPU 或 NPU(见 /concepts/hardware)五、推理精度的来源差异
1. 同一模型,不同引擎,结果为什么不完全一样
这是部署工程师几乎必然遇到的困惑:ONNX Runtime 和 PyTorch 跑同一个模型,输出有 1e-4 量级差异。来源有三:
- 算子实现不同:不同框架对同一数学运算(如 layernorm、softmax)采用不同的浮点计算顺序与中间精度(FP32 accumulation vs FP16 accumulation)。
- 融合改变中间精度:
conv+bn融合后,中间张量不再写回显存,可能全程保持 FP16,省了精度损失反而也可能改变结果。 - 低精度 kernel:FP16/INT8 计算本身就有舍入误差,误差会沿网络累积。
2. 数值校验的标准姿势
- 设定阈值:经验值——FP32 基线对比下,输出最大绝对误差 < 1e-3(CV 任务)、余弦相似度 > 0.999 通常可接受;INT8 量化后要求精度损失 < 1%(相对基线指标)。
- 用真实数据抽样:不能用训练集拍脑袋造样本,要采线上同分布数据,详见 模型格式与转换 的数值校验一节。
- 对"业务指标"负责:最终该对比的是 AUC、BLEU、Rouge 这类任务指标,而非逐位一致。
权衡与取舍
| 决策点 | 选项 A | 选项 B | 怎么选 |
|---|---|---|---|
| 框架直接跑 vs 过引擎 | 快、省事 | 快 2~10 倍、可编译 | 线上必须过引擎,开发环境可直跑 |
| 通用引擎 vs 专用引擎 | ONNX Runtime/OpenVINO | TensorRT/TFLite | 追求硬件极限选专用,追求可移植选通用 |
| 高精度 vs 低精度 | FP16/FP32,省心 | INT8/INT4,快但需校验 | 先用 FP16 上线,延迟不够再量化(见量化) |
| batch=1 快还是 batch 大快 | 延迟优、GPU 空转 | 吞吐优、延迟劣 | 在线选小 batch + batching,离线选大 batch |
一句话总结:推理的工程本质,是把"一次可复现的前向传播"优化成"硬件上最快的那一次"——先保证数学正确,再逐层榨取硬件性能。
延伸阅读
- 模型格式与转换 —— 从 .pt 到 ONNX/TensorRT,推理引擎优化的入口
- GPU 与硬件选型 —— 算力与带宽如何决定前向传播的上限
- 性能优化与容量规划 —— 延迟、吞吐、成本的三角权衡
- 常见陷阱与反模式 —— 数值不一致、shape 问题等实战坑
- 量化 —— 用更低位宽换带宽,推理提速的杠杆之王