训练是把参数调到能拟合数据,推理是用调好的参数回答一次请求。听上去只差一步,工程上却是两种问题:训练关心"多久收敛",推理关心"一次请求等多长、花多少钱、成不成"。
很多人接推理需求时默认"权重训好了,跑一次 forward 不就行了"。这在单条请求、没人在等、不计成本时成立。一旦有人盯着屏幕等第一个字、几百个请求同时进来、月底有账单,forward 就只是整件事里最不重要的一环。
1. 训练与推理的根本差异
一句话:训练改参数,推理不改。训练每步算梯度、更新权重、维护优化器状态;推理期间权重是只读常量,只做前向。
| 维度 | 训练 | 推理 |
|---|---|---|
| 目标 | 最小化损失、收敛 | 延迟与成本约束下给对答案 |
| 对参数做什么 | 每步更新 | 只读,不写 |
| 计算组成 | 前向 + 反向 + 优化器更新 | 只有前向 |
| 显存主体 | 权重 + 梯度 + 优化器状态 + 激活 | 权重 + KV Cache + 激活 + 预留 |
| 批的形状 | 固定张量形状、长周期 | 变长输入、逐 token 输出、动态到达 |
| 失败代价 | 一个 step 可重跑 | 用户当场看到报错 |
| 优化指标 | 样本/秒、收敛步数 | TTFT、TPOT、成功率、单位成本 |
显存结构直接写成两行:
训练显存 ≈ 权重字节 + 梯度字节 + 优化器状态字节 + 激活字节
推理显存 ≈ 权重字节 + KV Cache 字节 + 激活字节 + 框架与通信预留
优化器状态跟着参数量走,每参数多少字节取决于优化器类型与精度,按具体实现核对。这就是同一份权重,训练要的卡数常是推理数倍的原因。
2. 两个阶段的计算特征
| 阶段 | 主要算子 | 算术强度 | 瓶颈 | 加批带来什么 |
|---|---|---|---|---|
| 训练 | 前向 + 反向 + 优化器 | 高 | 算力与互联 | 更充分用满算力 |
| 推理 prefill | 大矩阵乘、整段注意力 | 高 | 算力 | 算力利用率上升 |
| 推理 decode | 单 token 矩阵向量乘、增量注意力 | 低 | 显存带宽 | 摊薄权重读取,吞吐上升 |
算术强度 = 该步浮点运算次数 / 该步访存字节数
decode 单步读取量 ≈ 权重字节 + 该请求的 KV Cache 字节
decode 每步要把权重几乎完整读一遍,却只产出 1 个 token。所以它天生带宽受限:卡再强,读不动也白搭。这是后面所有优化(批处理、量化、KV 压缩)的共同出发点。
3. prefill 与 decode 为什么决定后面一切
| 维度 | prefill | decode |
|---|---|---|
| 单次处理的 token 数 | 整段 prompt 一次过 | 1 个 |
| 耗时随什么变 | prompt 长度、卡算力 | 输出长度、权重字节、并发数 |
| 资源瓶颈 | 算力密集 | 显存带宽密集 |
| 对应指标 | TTFT | TPOT |
| 对并发敏感度 | 低 | 高(共享带宽与 KV 显存) |
| 典型优化方向 | 前缀缓存、chunked prefill | 连续批处理、量化、KV 压缩 |
两段瓶颈不同,优化手段就不可能通用:量化把权重变小,主要帮 decode;批处理把请求拼起来,主要帮 decode;长上下文同时压两头——prefill 变长、KV 变大、并发被挤掉。
所以看到任何"提速 X 倍",先问:它提的是 prefill 还是 decode,在什么输入输出长度分布下测的。答不上来,数字对你就没有参考价值。
4. 四个关键指标与它们互相拉扯
| 指标 | 定义 | 单位 | 谁在看 | 变差的原因 |
|---|---|---|---|---|
| TTFT | 请求到达至第一个 token 返回 | 毫秒 | 交互式产品 | 排队、prompt 太长、prefill 被挤 |
| TPOT / ITL | 首 token 之后每个 token 的平均间隔 | 毫秒/token | 阅读流畅度 | 批太大、带宽打满、KV 换出 |
| 吞吐 | 单位时间产出的 token 数 | tokens/s | 成本与容量 | 批太小、调度空转 |
| 成功率 | 成功完成的请求占比 | % | 稳定性 | 超时、OOM、上游限流 |
t0 请求到达;t1 第一个 token 返回 TTFT = t1 - t0
tn 第 n 个 token 返回 TPOT = (tn - t1) / (n - 1)
单请求总时长 ≈ TTFT + (n - 1) × TPOT
吞吐 ≈ 观测窗口内成功产出的 token 总数 / 窗口秒数
四个指标是互相抢资源的关系,不是并列关系:
| 动作 | 吞吐 | TTFT | TPOT | 成功率 |
|---|---|---|---|---|
| 增大最大批大小 | 升 | 变差 | 变差 | 可能降(显存紧) |
| 提高并发序列上限 | 升到饱和点 | 变差 | 变差 | 可能降 |
| 降精度 / 压缩 KV | 升 | 略好 | 略好 | 可能降(质量) |
| 收紧超时 | 表面升 | 略好 | 不变 | 降(被砍算失败) |
入门该记住的一句:没有参数能同时改善四个指标。真有的话,默认值早就不是现在这个了。
5. 三种交付形态与各自的约束
| 形态 | 调用方式 | 关键约束 | 取舍 |
|---|---|---|---|
| 在线 API | 同步或流式,按请求计费 | 延迟与并发,必须能取消 | 体验优先,单位成本高 |
| 批处理任务 | 提交任务、离线取结果 | 吞吐与成本,可容忍排队 | 成本优先,延迟不敏感 |
| 边端部署 | 本地进程内调用,常离线 | 内存、功耗、无网 | 模型必须小,量化几乎必选 |
形态会反向决定选型:在线 API 不能"排队十分钟再出结果",所以必须有准入与背压;批处理不能一次只跑一条,要尽量拉满批;边端不能背 40GB 权重,所以先量化再谈效果。
6. 推理引擎在管什么
不必先读源码,但要知道它替你管四件事,否则调参时不知道为什么参数长这样:
- 调度:请求排队,决定谁先算、谁等、谁被拒。公平性、优先级、超时都在这一层。
- 批处理:把不同请求拼批一起算,并每步重新组批。批大小由显存与 token 上限共同约束。
- KV Cache 管理:把显存切块分给各请求,做前缀复用、做换出。这一层决定"能同时服务多少条"。
- 算子与图优化:融合算子、选 kernel、走量化 kernel、减少中间张量。
一句话:它不是"跑得更快的库",而是"把有限显存切成多份并决定谁先用"的分配逻辑。调引擎参数本质是在调分配策略。
7. 为什么"训练脚本包一层 HTTP"不能当服务
# 能返回结果,但不是服务:阻塞、无并发、无流式、不可取消
@app.post("/chat")
def chat(req: ChatRequest):
return {"text": model.generate(req.prompt, max_new_tokens=req.max_tokens)}
它缺的正好是推理服务的四份契约:
| 缺什么 | 后果 | 应有做法 |
|---|---|---|
| 并发与组批 | 每请求独占一次前向,卡利用率极低 | 队列 + 连续批处理 |
| 流式输出 | 用户等整段生成完才看到字 | SSE 增量返回,先出首 token |
| 取消与超时 | 用户断开后仍在生成,白烧卡 | 感知断连,立即释放 KV |
| 显存与准入 | 并发一多就 OOM、进程崩 | 按 KV 预算做准入,满了排队或拒绝 |
还有两条不显眼但致命:没有指标(出问题只能靠用户反馈)、没有副本与健康检查(一台挂了整站挂)。判断标准很简单:同时来 50 个请求,它会变慢、报错还是排队?答不出来,它只是脚本。
8. 入门者先回答的五个问题
| # | 问题 | 为什么先问 | 要拿到的东西 |
|---|---|---|---|
| 1 | 模型多大 | 决定权重显存下限 | 参数量、精度、config 中层数与头数 |
| 2 | 卡有多少显存 | 决定还剩多少给 KV | 卡型号、单卡显存、可用卡数 |
| 3 | 要多少并发 | 决定 KV Cache 规模 | 峰值并发、平均输入/输出长度 |
| 4 | 延迟目标多少 | 决定批上限与是否流式 | TTFT/TPOT 目标值、P50 还是 P99 |
| 5 | 成本上限多少 | 决定卡型、副本数与是否量化 | 每月卡时预算、可接受单位成本 |
权重显存 ≈ 参数量 × 每参数字节数 # FP16/BF16 约 2,INT8 约 1,INT4 约 0.5
可用 KV 显存 ≈ 卡显存 × 目标利用率 − 权重显存 − 激活与预留
每 token KV 字节 ≈ 层数 × 2 × KV头数 × 头维度 × 每元素字节数
可并发序列数 ≈ 可用 KV 显存 / (每 token KV 字节 × 平均上下文长度)
每参数字节数、层数、KV 头数都要按模型 config 与卡规格核对,GQA/MQA 模型差别极大,抄别人的数字必然错。
9. 三个常见误解
| 误解 | 实际 |
|---|---|
| 推理就是跑一次 forward | forward 只是计算;排队、组批、KV 管理、取消、指标才是服务主体 |
| 用训练吞吐推算推理吞吐 | 训练是样本/秒且含反向,推理按 token 计且受带宽限制,不可换算 |
| 只看总时长不看首 token 延迟 | 用户感知的是"多久出第一个字";总时长相同、TTFT 不同,体验天差地别 |
10. 排错:五种"看起来在跑"的故障
| 现象 | 原因 | 处理 |
|---|---|---|
| 单请求快,多请求集体变慢 | 没组批,串行执行 | 开连续批处理,设最大批与 token 上限 |
| 首 token 很慢,之后很快 | prefill 排队,长 prompt 挤占算力 | 看 prompt 长度分布,考虑前缀缓存或限制输入 |
| 中间卡顿,末段突然变快 | KV 不足触发换出/重算 | 降并发上限、压缩 KV 或加卡 |
| 用户断开后 GPU 仍满载 | 没处理断连,生成没被取消 | 在流式循环里检测断连并释放资源 |
| 进程周期性 OOM 重启 | 按请求数而非 KV 字节做准入 | 改按剩余 KV 容量准入,留框架预留 |
11. 边界:这些判断什么时候不成立
| 场景 | 说明 |
|---|---|
| 单用户、本地、离线跑一次 | 不需要服务化,脚本足够,本文指标意义不大 |
| 模型极小、CPU 能实时跑 | 瓶颈不在显存带宽,GPU 侧结论不适用 |
| 只做离线批处理 | TTFT 不重要,吞吐与成本是唯一目标,策略完全不同 |
| 直接买托管 API | 引擎不可见,能调的只有并发、超时、重试与预算 |
12. 自测
- 训练和推理的显存构成,最大的结构差异是哪一项?为什么它决定了卡的数量级差异?
- 用算术强度解释:为什么 decode 是显存带宽密集而不是算力密集?
- 一个"提速 2 倍"的优化,你要问哪两个问题才能判断它对你有用?
- TTFT 与 TPOT 分别由哪一段决定?它们为什么会互相挤?
- 把训练脚本包一层 HTTP 不能算服务,至少说出三条缺失的契约。
- 接到新需求,你先问哪五个问题?各自对应哪项资源估算?