主题
量化与吞吐调优
一句话:量化用精度换显存和带宽,从而在同一张卡上跑更大模型或更多并发;调参则是在延迟与吞吐之间选折中,二者都必须用真实负载评测,不能只看“模型能启动”。
为什么需要
推理成本有两个硬约束:显存装不下,或者装得下但并发上不去。量化直接缓解前者,调参缓解后者。常见误区是先调一堆参数,最后发现模型权重就占了八成显存;或者为了省显存上了激进量化,业务指标掉得比成本省得多。
正确顺序是:先算显存账,决定是否需要量化;再测量化后的质量;最后才调引擎参数。顺序反了,调参是在一个错误的基础上做局部优化。
这张图的顺序是排障顺序:先判断是权重装不下还是 KV 不够,前者才考虑量化或多卡,后者优先降并发或加显存;两者都够才轮到调参。量化之后必须过业务评测,不达标就换方案,不要继续调参掩盖质量损失。
核心机制
量化到底省了什么
| 精度 | 每参数字节 | 13B 权重 | 相对 FP16 |
|---|---|---|---|
| FP16 / BF16 | 2 | 约 26 GB | 1× |
| FP8 | 1 | 约 13 GB | 1/2 |
| INT8 | 1 | 约 13 GB | 1/2 |
| INT4 | 0.5 | 约 6.5 GB | 1/4 |
权重省下来的显存可以直接变成 KV 容量,提升并发;同时 decode 每步要读权重,字节数减少也降低带宽压力,TPOT 通常改善。但量化不是无损压缩:激活、注意力和长尾输入可能放大误差,必须用业务评测集验证。
常见方案
| 方案 | 粒度 | 特点 | 注意 |
|---|---|---|---|
| FP8 | 权重 / 激活 | 新卡支持好,精度损失小 | 依赖硬件与引擎支持 |
| INT8 | 权重 / 激活 | 成熟,通用性好 | 校准数据要贴近真实分布 |
| GPTQ | 权重量化 | 离线量化,生态广 | 不同任务精度差异大 |
| AWQ | 权重量化 | 保护显著通道,小模型表现好 | 仍需业务评测 |
| KV cache 量化 | KV | 直接提升并发 | 长上下文精度风险更高 |
选择顺序:硬件支持 FP8 时优先 FP8;否则 INT8 / AWQ;INT4 只在显存极度受限且业务能接受精度损失时使用。不要仅凭困惑度(perplexity)选方案,生成质量、指令遵循和工具调用参数准确性都要测。
量化后的显存账
text
总显存 = 权重 + KV + 激活峰值 + 固定开销量化的收益主要在第一项。举例:80 GB 卡,13B FP16 权重 26 GB,KV 可用约 48 GB;换成 FP8 后权重 13 GB,KV 可用约 61 GB,并发上限提升约四分之一。如果模型是 70B,FP16 需要多卡,INT4 可能单机装下,这时量化决定的是“能不能上线”,不只是“快不快”。
一个具体的三步账:14B + 32 GB 卡
参数表上的数字放到卡上,经常是“能加载、不能服务”。以 14B 模型、32 GB 卡为例,三步算完就知道该不该量化:
- FP16 权重约 28 GB(14B × 2 B)。扣掉 CUDA context、激活峰值和通信 buffer 约 2 GB,只剩 2 GB 给 KV,按 192 KiB/token 折算约 1 万 token:一条 8K 请求就占掉八成,第二路挤不进来。权重装下了,有效服务区间装不下。
- GPTQ-Int4 权重约 8.5 GB。名义值是 14B × 0.5 B = 7 GB,实际文件还要带分组 scale/zero,embedding 和输出层往往不量化,按 8.5 GB 估更接近真实占用。
- 32 − 8.5 − 2 ≈ 21.5 GB 留给 KV。按 192 KiB/token 折算约 11 万 token,平均 4K 序列约 26–28 路并发,8K 约 13–14 路,32K 长上下文只剩约 3 路。
这张账说明量化的收益不是“模型变小了”,而是把 KV 池从“几乎为零”变成“能排下几十路请求”;同时也说明第三步之后仍要按序列长度规划,同样是 21.5 GB,4K 和 32K 的并发差一个数量级。
前提:V100 没有 FP8 路径,实际可选项是 FP16 / INT8 / GPTQ / AWQ;固定开销那 2 GB 随卡型、驱动、引擎版本变化,账要按实测重算。
调参的四个核心旋钮
| 参数 | 控制什么 | 调大 | 调小 |
|---|---|---|---|
gpu_memory_utilization | KV 池上限 | 并发高,OOM 风险高 | 安全,浪费显存 |
max_num_batched_tokens | 每步 token 预算 | prefill 快,decode 可能被挤 | decode 稳,长 prompt 慢 |
max_num_seqs | 同时运行序列数 | 并发高,尾延迟差 | 排队多,TTFT 高 |
enable_prefix_caching | 前缀复用 | 固定前缀场景省算力 | 前缀多变时只增开销 |
gpu_memory_utilization 不是“KV 能用多少”,而是“KV 池的保险丝”。设得越接近 1,越容易被 CUDA context、通信 buffer 和激活峰值顶到 OOM。生产通常从 0.85 起步,每次加 0.05,用固定负载压 30 分钟,找到稳定上限后退回一档。
max_num_batched_tokens 是最容易被忽略的参数:它同时决定 prefill chunk 大小和 decode 预算。设得太大,长 prompt 会挤占 decode,TPOT 尾延迟变差;设得太小,prefill 步数多,TTFT 变长。
max_num_seqs 要和 KV 容量匹配。设得比实际能容纳的序列多,多出来的请求只是在等待队列里排队,TTFT 更高,不会提升吞吐。
场景化配方
| 场景 | 目标 | 倾向 |
|---|---|---|
| 在线对话 | 低 TTFT、稳 P99 | 中等 batch,开分块 prefill,前缀缓存开 |
| 离线批处理 | 总吞吐 | 大 batch,大 token 预算,前缀缓存按需 |
| 代码补全 | 极低延迟 | 小 batch,短输出,限制并发 |
| 长文档摘要 | prefill 吞吐 | 大 token 预算,按长度分池 |
| 多轮客服 | 首字快、前缀复用 | 前缀缓存开,会话粘性路由 |
配方只是起点。同一场景的 prompt 长度分布变了,最优参数就会变;用生产采样做压测,不要用几条固定 prompt。
评测:质量和性能一起看
| 维度 | 指标 | 判据 |
|---|---|---|
| 质量 | 任务成功率、指令遵循、工具参数准确率 | 不低于基线的可接受区间 |
| 延迟 | P50 / P95 / P99 TTFT、TPOT | 满足 SLA |
| 吞吐 | tokens/s、请求/s | 达到容量目标 |
| 成本 | 每千 token 成本、每次成功成本 | 低于预算 |
| 稳定性 | OOM、抢占、超时率 | 长时间压测无异常 |
只看吞吐会忽略质量,只看质量会忽略成本。量化版本上线前至少跑一遍业务 Golden Set,并对比基线的失败类型;如果失败集中在长尾输入,说明量化对边界样本更敏感。
实战场景
场景:14B 工单抽取模型 Int4 后困惑度只涨 0.5%,业务通过率掉 12 个点
现象:14B 客服加工单抽取模型,FP16 基线困惑度 5.79、1,200 条 Golden Set 通过率 93.4%;换 GPTQ-Int4(group_size 128)后困惑度 5.82(+0.5%),业务通过率却只剩 81.2%,失败集中在金额、日期抽取和工具参数 JSON(经验值,需校准)。上线三天质检抽样才发现,其间 2.4 万次调用里约 4,500 次输出错误。
排查路径:
| 排除对象 | 判定证据 | 结论 |
|---|---|---|
| 采样参数漂移 | 两侧 temperature=0、seed 固定,同一 prompt 复跑 3 次输出逐字相同 | 排除 |
| 截断或 OOM | finish_reason 全为 stop,num_preemptions_total 为 0 | 排除 |
| 模板被改 | 网关记录的 prompt hash 与基线一致,git diff 无变化 | 排除 |
| 量化误差集中在长尾 | Golden Set 分桶:常规问答 96.1%、数字抽取 74.0%、工具参数 JSON 68.5%;失败样本与基线的 logit 差集中在数字 token | 确认:均值掩盖了长尾 |
对症做法:把验收门从困惑度换成“分桶通过率 + 结构化输出合法率”,不达标就换方案。
python
GATE = {"ppl_delta": 0.05, "golden_pass": 0.90, "bucket_floor": 0.85, "json_valid": 0.99}
def accept(base, cand, buckets):
if cand["ppl"] / base["ppl"] - 1 > GATE["ppl_delta"]: return "PPL 超限"
if min(buckets.values()) < GATE["bucket_floor"]: return "分桶低于红线"
if cand["json_valid"] < GATE["json_valid"]: return "结构化输出不合法"
return "通过" if cand["golden_pass"] >= GATE["golden_pass"] else "整体通过率不足"困惑度是所有 token 的平均交叉熵,被高频常见 token 拉平;金额、日期、JSON 键名是低频 token,量化误差在这里放大,均值看不出来。分桶把长尾从均值里拆出来,本例改用 W8A8 INT8(权重约 14 GB)后最低分桶回到 88.4%、整体 91.8%,显存占用仍低于 FP16 基线。
复盘要点:判据是“量化验收以最低分桶通过率和结构化输出合法率为准,困惑度只作准入粗筛”,红线是任一分桶低于基线 5 个点不得上线。更早的发现点是灰度期就按桶看通过率——本次是全量替换,没有灰度,指标口径再对也来不及。
场景:14B 上 32 GB 卡,三步账漏了权重占比,上线只有 1,184 token 的 KV
现象:14B 模型部署到 32 GB 卡,FP16 权重、gpu_memory_utilization=0.95,启动直接报 The model's max seq len (8192) is larger than the maximum number of tokens that can be stored in KV cache (1184);把 max_model_len 降到 1024 能起,但 max_num_seqs=8 时第二路就触发抢占,P99 TTFT 从 1.2 s 涨到 9 s。现场判断是“KV 参数没调好”。
排查路径:
| 排除对象 | 判定证据 | 结论 |
|---|---|---|
| 引擎或版本问题 | 同镜像跑 7B FP16,KV 池 26 GB、max_model_len 32768 正常 | 排除 |
max_model_len 太大 | 8192 × 192 KiB = 1.5 GB,而报错说明池子只有 0.23 GB,任何 len > 1,184 都起不来 | 排除:len 只是暴露了池子太小 |
| 权重实际占用 | 加载后 nvidia-smi 显示 28.6 GB,与 14B × 2 B 吻合,占卡容量 89% | 确认:权重挤掉了 KV |
| 固定开销 | 30.4 GB 预算 − 28.6 GB 权重 − 0.23 GB 池 ≈ 1.6 GB,正好是 context 与激活峰值 | 确认:余量不足 |
对症做法:按本章三步账重算,并把 gpu_memory_utilization 乘进第一步。
text
① 预算 32 × 0.90 = 28.8 GB(起点 0.85,每次 +0.05 压测)
② Int4 权重 14 × 0.5 = 7.0 GB,含分组 scale/zero 与未量化层按 8.5 GB 估
③ KV 池 28.8 − 8.5 − 2.0 = 18.3 GB ÷ 192 KiB/token ≈ 9.3 万 token
4K → 22 路 8K → 11 路 32K → 2 路
max_num_seqs = 22(对齐 4K 段;8K 请求由块数自然限流) max_model_len = 8192量化只改第一项,固定开销和“按序列长度折算并发”的规则不变:28.6 GB 已经吃掉 89% 的卡,gpu_memory_utilization 再怎么调也补不出 KV,只能把权重从 28.6 GB 压到 8.5 GB。第三步之后同一个 18.3 GB 在 4K 和 32K 下的并发差十倍,所以 max_num_seqs 必须按目标长度段定,不是按平均值定。
复盘要点:判据是“权重 ÷ 卡容量 > 0.75 就必须量化或多卡”,剩下 25% 要同时装 KV、激活峰值和 CUDA context,这条比任何调参都先决。启动日志里那句 maximum number of tokens that can be stored in KV cache 就是现成的容量结论,应写进部署验收清单——本次它出现后仍被当成参数问题排查了四小时。
场景:14B 服务放大显存上限与长度,并发从 23 掉到 9、首字翻三倍
现象:14B 服务原来 gpu_memory_utilization=0.90、max_model_len=8192、max_num_batched_tokens=8192,稳定 23 路并发,P99 TTFT 1.1 s、TPOT 62 ms。为接长文档把显存上限提到 0.94、长度放到 32768,同时把 token 预算压到 4096 保护 TPOT,结果并发掉到 9 路、Waiting 积压 27,P99 TTFT 涨到 3.4 s,TPOT 反而升到 71 ms(经验值,需校准)。
排查路径:
| 排除对象 | 判定证据 | 结论 |
|---|---|---|
| 显存泄漏 | 8 小时 nvidia-smi 曲线平稳,无单调上升 | 排除 |
| 负载结构变了 | 网关侧长度直方图:32K 档占 9%,主体仍是 4K | 排除:是单请求上限变了 |
| 长请求占满 KV | 日志 Running: 9, Waiting: 27,抢占 41 次/分钟,num_preemptions_total 斜率与 32K 请求数同步 | 确认:块被长文吃干 |
| prefill 预算 | trace 里 8K prompt 在 4096 预算下走 2 个 chunk,prefill 段占 TTFT 的 2.6 s | 确认:首字被 chunk 数拖慢 |
对症做法:短文本池回退参数,长文本单独分池,两个池各自按长度定 max_num_seqs。
yaml
# 短文本池(约占九成流量)
gpu_memory_utilization: 0.90
max_model_len: 8192
max_num_batched_tokens: 8192 # 一条 8K prompt 单步 prefill,TTFT 不再按 chunk 累加
max_num_seqs: 22 # 18.3 GB ÷ 192 KiB ÷ 4096 ≈ 22 路
# 长文本池(32K 独立副本)
max_model_len: 32768
max_num_seqs: 2 # 18.3 GB ÷ 6.4 GB/条 ≈ 2 路
max_num_batched_tokens: 16384四个旋钮是乘在一起的:max_model_len 定单请求块上限,max_num_seqs 定同时占用者,两者相乘不能超过 KV 池——32768 × 192 KiB = 6.4 GB,两条长文就占到 12.8 GB,第三条挤不进来。max_num_batched_tokens 决定一条 prompt 走几个 prefill 步,TTFT 大致正比于 chunk 数,4096 让 8K 变成 2 步,恢复到 8192 后单步完成,TTFT 回到 1.2 s。0.94 把卡上仅剩的 1.9 GB 留给 CUDA context 与激活峰值,而这段实测要 2 GB,长压必然顶到 OOM。
复盘要点:判据是“一次只动一个旋钮,并同时看 running/waiting、抢占数、TTFT 三个数”,红线是 max_model_len × 单 token 块占 × 预期并发 > KV 池 的配置直接拒绝启动。更早的发现点是 Waiting 连续 5 分钟大于 Running:那说明瓶颈已从算力变成块容量,此时再调 token 预算只会让首字更慢。
最小上手
用同一组负载做 A/B,不要跨时间比较:
text
基线:FP16,默认参数
实验:FP8 / INT8,固定 gpu_memory_utilization=0.85
负载:真实 prompt 采样,固定并发 1/8/32,各 10 分钟
记录:P99 TTFT、P99 TPOT、吞吐、显存峰值、质量通过率判据:质量通过率下降在可接受范围内,P99 不退化,吞吐或并发有明显提升,才算量化有效。任何一项明显变差,都要回到方案选择,而不是继续调参掩盖。
生产动作
- 先算账再量化:明确是权重装不下,还是 KV 不够;两种问题的解法不同。
- 校准数据真实:量化校准集要覆盖生产输入分布,避免长尾失真。
- 保留回退:量化版本与基线版本同时可部署,出问题能切回。
- 分池部署:不同量化精度、不同上下文长度的实例分开,避免互相挤占。
- 监控 OOM 与抢占:显存峰值、空闲块、抢占次数、换出量一起看。
- 版本固定:量化格式与引擎版本强相关,升级要重新验证兼容性。
边界
- 量化不能修复容量规划错误:KV 需求随并发和长度增长,量化只是把起点降低。
- 精度损失不均匀:通用 benchmark 正常不代表业务长尾正常,必须用业务集评测。
- 低精度不总是更快:某些硬件对 FP8 / INT4 的支持不完整,可能插入反量化开销。
- KV 量化风险更高:长上下文下误差会累积,检索和引用类任务要重点验证。
- 调参有交互:四个旋钮互相影响,一次只改一个,否则无法归因。
讲清楚
量化用更少的字节存权重,省下的显存变成 KV 容量,同时降低 decode 的带宽压力;代价是精度损失,必须用业务评测集验证,不能只看困惑度。方案按硬件支持选 FP8、INT8、AWQ,INT4 只在极端受限时用。调参围绕四个旋钮:显存上限决定并发天花板,token 预算决定 prefill 与 decode 的分配,序列上限要和 KV 容量匹配,前缀缓存按场景开关。正确顺序是先算显存账、再选量化、再调参,最后用真实负载同时看质量、延迟、吞吐和成本。
适用:2026 年主流推理引擎的量化与调优实践;具体格式支持以引擎和硬件文档为准 | 初稿 2026-10