大模型推理加速这事儿,我最近大半年一直在跟。从最初老老实实跑 FP16,到给模型做量化,再到折腾投机采样和 PD 分离,一路踩坑一路补课,总算把这套组合拳给打明白了。先给结论:这三项技术不冲突,它们分别从数据体积、解码步数、资源调度三个角度下手,条件允许的情况下完全可以叠加使用。
这篇文章不是论文复述,而是我实际部署过程中的理解和方法总结。适合已经在用 vLLM、llama.cpp 这类工具跑过模型、但还想往底层多挖一层的人,也适合正在为线上推理延迟和吞吐量发愁、准备优化部署方案的人。我会把每个技术讲清楚原理,给出选型和参数上的参考,再把我踩过的坑原原本本摆出来,希望能帮你少走弯路。
1. 先把推理慢这件事拆开看
1.1 自回归解码:一次一个 token 的“挤牙膏”
大模型生成文本的方式,很多人以为它像人一样“想好整段再说”,实际上它是一个字一个字往外蹦的。每生成一个新 token,模型都要基于已经生成的全部内容做一次前向计算,然后从词表概率分布里挑一个 token 输出。这个过程叫自回归解码,天生是串行的。
这意味着一个很现实的问题:生成 100 个 token,就要做 100 次前向计算。每次前向计算虽然是“增量”的(利用 KV cache 避免重复算历史 token),但依然要读取全部模型权重、更新缓存、输出概率分布。你把模型想象成只知道“下一个字是什么”的助手,你说一句,它猜一个字,然后把这个字记下来,再猜下一个字。这个“只能一步一步来”的解码方式,就是大模型推理延迟的根本来源。
推理过程内部还分成两个截然不同的阶段。Prefill(预填)阶段处理用户输入的那段 prompt,所有位置的 token 可以并行计算;Decode(解码)阶段则是逐 token 生成,只能串行。这两个阶段对算力和带宽的需求完全不同,后面讲 PD 分离时还会重点展开。
1.2 显存带宽才是真正的瓶颈
很多人第一反应是大模型慢是因为“计算量太大”,但实测下来,尤其是 Decode 阶段,真正的瓶颈是显存带宽。
拿一张 A100 80GB 举例,它的 HBM 带宽大概是 2TB/s。一个 7B 模型按 FP16 存,权重大约 14GB。每生成一个 token,至少要访问一遍这 14GB 的权重,光是搬运这些数据就需要 14GB 除以 2TB/s,算下来约 7 毫秒。GPU 的计算单元大部分时间都在等数据从显存搬进寄存器,算力再高也没用,数据喂不进来。
这个估算很粗糙,实际还会叠加 KV cache、batch、碎片化读取等因素,但它揭示了一条重要规律:Decode 阶段的耗时,基本由“每生成一个 token 需要从显存读多少字节”决定。只要能减少每次读取的数据量,Decode 速度就能近乎线性地提升。这也是为什么量化能成为最直接的加速手段——它把每次搬运的数据体积直接砍掉一半甚至四分之三。
1.3 三条加速路线背后的共同主线
理解了瓶颈,再看三条主流路线的逻辑就非常清晰了:
- 量化:减小每次读取的数据体积,把权重从 FP16 压到 INT8/INT4,顺带把显存占用降下来,让更大的 batch、更大的模型能塞进单卡。
- 投机采样:减少目标模型被调用的次数。用一个草稿模型连续猜一串 token,让大模型一次前向验证多个 token,相当于一次计算换来了好几个 token 的产出。
- PD 分离:把 Prefill 和 Decode 拆到不同的 GPU 资源池,避免两类任务互相干扰,让整体调度更稳定,延迟指标更可控。
这三条路线并不互斥,因为它们动的是不同的变量:量化改数据体积,投机采样改解码步数,PD 分离改任务调度方式。把这层主线理清楚,后面每一个技术细节都不会再被看成孤立的 trick,而是同一盘棋上的不同落子。
2. 量化:给模型做减法,收益最直接的加速
2.1 量化在做什么:精度、数值范围与缩放因子
量化的本质是降低数值精度。训练好的模型权重大多数是 FP16 或 BF16,每个数占 2 字节;量化成 INT8 变成 1 字节,INT4 变成 0.5 字节。显存占用直接按比例下降,每 token 需要搬运的字节数也跟着下降,Decode 速度自然就上来了。
但“降精度”不是简单地把高精度数值截断成低精度整数,中间要解决两个问题:数值范围怎么对齐,精度损失怎么控制。
模型权重的值通常集中在零点附近的小范围内,但偶尔会冒出一些绝对值很大的 outlier。如果直接把整个 tensor 的数值范围映射到 INT8 的 [-128, 127],大多数普通数值会损失大量精度。所以实际操作中要引入 scale(缩放因子)和 zero point(零点),把原始数值映射到目标精度能表达的范围内。映射的粒度越细,精度损失越小——按整个 tensor 量化最粗糙,按 channel 量化好一些,按 group(比如每 128 个权重一组)量化效果更好,但反量化开销也更大。
我自己的理解是,量化本质上是在“数值表达的精细度”和“数据体积”之间做权衡。group size 从 128 降到 64 或 32,生成质量通常会变好,但对应的计算开销上升,推理速度可能反而下降。这不是玄学,而是明摆着的工程取舍,实际使用时要针对你的模型和硬件跑一组对比再定。
2.2 主流量化方案怎么选
目前实际部署中用得最多的几类方案,我把它们整理成一张表,方便对照选型:
| 方案 | 精度 | 特点 | 适用场景 |
|---|---|---|---|
| GPTQ | INT4 / INT8 | 基于二阶信息逐层量化,需要校准集 | 单卡部署大模型,vLLM 支持成熟 |
| AWQ | INT4 | 按激活值重要性保护关键权重通道 | 与 GPTQ 同场景,通常精度更稳 |
| SmoothQuant | INT8 权重+激活 | 把激活值里的 outlier 迁移到权重侧 | 需要同时量化激活的在线服务 |
| FP8 | FP8(E4M3/E5M2) | 训练/推理同精度,H100 等新卡支持好 | 高端显卡、训练后直接部署 |
| GGUF Q4_K_M | INT4 混合 | llama.cpp 生态,量化格式成熟 | 本地 CPU / 边缘设备 / 个人部署 |
我个人的选型经验:如果模型要在 vLLM 上跑,优先看 GPTQ 和 AWQ;如果走 llama.cpp 或者需要在 Mac、CPU 上跑,直接上 GGUF;FP8 是高配集群里很自然的选择,因为不用做太复杂的校准,部署链路最省事。各方案之间的差异没有网上说的那么大,很多时候决定成败的是校准数据质量而不是量化算法本身。
2.3 量化提速与显存收益的实测参考
说几个我在 A100 40GB 上、7B 模型、batch 为 1 的条件下的实测数据,供参考:
- FP16 基线:Decode 约 20-25 tokens/s。
- INT8:约 35-40 tokens/s。
- INT4(GPTQ):约 55-65 tokens/s。
需要说明的是,不同框架、不同 GPU 后端的数字差异会很大,但这些数据至少说明一件事:量化在 Decode 阶段的收益是实打实的。Prefill 阶段提速效果通常没那么夸张,因为 Prefill 是计算密集型任务,INT4/INT8 的反量化开销在某些硬件上反而会让 Prefill 变慢。这也是为什么一些部署方案会做“选择性量化”,只优化 Decode 而不动 Prefill。
显存收益同样关键。7B 模型 FP16 要占约 14GB,INT4 只要 4-5GB,省下来的显存可以留给更长的上下文、更大的 batch,甚至直接换一个更大的模型。这个收益在成本上的价值比速度收益更直接,尤其是你按显存租卡的时候,同一张卡能“装下”的模型和并发量直接翻倍。
2.4 量化最容易踩的四个坑
第一个坑是校准数据选得不对。GPTQ、AWQ 都需要一小段文本做校准,用来统计权重分布。如果校准数据和真实业务分布差异太大,比如用英文通用语料校准,线上全是代码或合同文本,那么量化后效果会肉眼可见地变差。我的建议是至少从线上日志里捞 512 到 1024 条真实 prompt 做校准,别偷懒用现成的 c4 样本。
第二个坑是只看 perplexity 不看下游任务。perplexity 涨了 0.1 看起来不多,但某些对格式敏感的生成任务——代码补全、JSON 输出、工具调用——可能直接崩。量化后一定要拿关键下游任务做回归,尤其是结构化输出类场景。
第三个坑是激活值量化比权重量化难得多。权重的分布相对稳定,激活值会随输入剧烈变化,尤其是存在大量 outlier 时。所以很多方案只做 weight-only 量化,这不是偷懒,而是激活量化通常需要 SmoothQuant 之类的预处理,否则损失无法接受。
第四个坑是 KV cache 量化。长上下文场景下 KV cache 占显存很大,量化成 INT8 能省不少显存,但误差会随着序列长度累积,长上下文下质量衰减会很明显。建议先上权重量化,KV cache 量化作为后续优化项,并且务必在最长上下文长度上做质量回归。
这几个坑我全踩过。最典型一次是量化完模型生成 JSON 总是缺字段,排查半天发现校准集里全是散文,后来换成业务真实 prompt,问题立刻就消失了。
3. 投机采样:用小模型换大模型时间
3.1 为什么一个“笨”模型能帮上忙
投机采样(Speculative Sampling)的思路很有意思:与其让大模型一个字一个字慢吞吞地蹦,不如让一个小模型先按自己的判断连续写出一串 token,然后让大模型一次性验证这一整串。因为前面说过,大模型 Decode 是访存瓶颈,验证一串 token 的耗时和验证一个 token 差不多,所以只要小模型猜得够准,大模型一次前向就能“白赚”好几个 token。
为什么小模型“猜得准”这件事是可行的?因为自然语言里大量 token 的预测根本不难。“床前明”后面大概率是“月光”,“def pair”后面大概率跟变量名,“根据以上分析,结论是”这种套路化表达在模型输出里更是常见。大模型在这些“送分题”上花的时间和难题上差不多,这本身就是一种浪费。让一个小模型把这些容易的 token 先顺下来,大模型只负责把关,等于把算力集中到真正难的地方。
更关键的是数学保证:只要草稿模型和目标模型对 token 分布的判断不是完全对立,通过拒绝采样(rejection sampling)做验证,最终输出分布可以做到和直接用大模型采样完全一致。也就是说,投机采样是少数能做到“无损加速”的方案,不会因为加速而牺牲生成质量。
3.2 草稿+验证+拒绝采样的完整流程
标准流程分成四步,这里用实际例子说清楚:
- 草稿模型自回归生成 k 个候选 token。比如 k=4,小模型先写出“中国的首都是”,接下来四个 token 它猜“北、京、是、一”。
- 把这 4 个候选 token 接到大模型输入的末尾,做一次前向计算。因为是一次处理 4 个位置,大模型一次就能拿到这 4 个位置各自的目标分布。
- 从第一个候选 token 开始逐个做拒绝采样验证:如果目标分布对“北”的概率足够高,通过随机数判断后接受,继续看“京”;如果某个 token 不达标,就在这个位置用目标分布重新采样一个 token 作为修正,后面未验证的候选全部丢弃。
- 被接受的 token 全部作为输出,从修正 token 的位置开始,重新进入下一轮草稿生成。
关于第 3 步,要特别强调一点:接受条件不是“草稿模型 top-1 等于目标模型 top-1”,而是按概率接受。目标模型给出的分布中,某个 token 被采样到的概率高,就更容易被接受。这样即使草稿 token 不是目标模型最可能的 token,只要它落在高概率区域,仍然可能被保留,从而保证最终输出分布不被扭曲。
我在本地把整个流程跑通时感触最深的一点是:草稿模型和目标模型的分歧越大,接受率越低,加速比越小;如果两个模型完全一致,每一轮都能验证全部 k 个 token,速度会无限接近草稿模型本身——当然这种情况不太可能发生。
3.3 加速比怎么估算
假设每个草稿 token 的接受概率是 α(为便于理解,先按独立近似),每轮草稿长度是 k,那么每轮能产出的 token 数期望约为:
E ≈ (1 - α^(k+1)) / (1 - α)
这个公式看着唬人,算一下就明白了。比如 α=0.7,k=4,那么 E ≈ (1 - 0.7^5) / 0.3 ≈ 2.77。意思是,大模型每做一次前向,平均能产出接近 2.77 个 token,而传统解码一次前向只产出 1 个,理想情况下端到端速度就是原来的 2.77 倍。
但有两个现实修正必须考虑。第一,草稿模型本身也要耗时,如果草稿模型太大,它生成 k 个 token 的时间会把收益吃掉大半。第二,α 不是一个常数,序列开头、中间、结尾的 token 难度差别很大,遇到代码、公式、罕见词多的文本,接受率会明显下降。所以实际加速比通常落在 1.5 到 3 倍之间,宣称能到 4 倍以上的,基本是特定数据集上的理想值。
坦白说,我第一次用 7B 主模型配 0.5B 草稿模型时,实测只有 1.6 倍左右,一度怀疑是自己实现有问题。后来换了同家族的草稿模型,接受率从 0.5 左右涨到 0.7 附近,加速比才到 2 倍以上。模型家族的匹配度,比草稿模型本身的“聪明程度”更重要。
3.4 草稿模型选型与调参心得
草稿模型的选型是最关键的工程决策。我的经验是优先选和主模型同一家族、尺寸更小的模型,比如 7B 主模型配 0.5B 或 1B 的同系列模型。原因有两个:一是 tokenizer 一致,不用处理词表映射;二是同族模型的生成风格和分布更接近,接受率更高。跨家族搭配不是不行,但要额外处理 tokenizer 对齐问题,中间能踩的坑非常多。
几个调参建议:
- k 值不是越大越好。k 太大,草稿模型生成时间变长,而且长串候选很容易在中间段被拒,边际收益递减。我一般从 4 开始试,往上调到 6 收益就明显变小了。
- 采样参数必须匹配。如果目标模型用 temperature=0(贪心解码),草稿模型也得用贪心,否则两者的分布对不上,接受率会暴跌。
- 草稿模型同样建议量化。一个 1B 模型虽然不大,但加上 KV cache 后显存成本也不可忽视,INT8 量化后收益明显。
另外,如果你的场景是代码补全、RAG 检索结果生成这类“文本高度可预测”的任务,可以试试 prompt lookup decoding——直接从上下文里找重复片段当草稿,完全不需要额外的草稿模型,零额外推理成本,效果经常出奇地好。
4. PD 分离:把 Prefill 和 Decode 彻底拆开
4.1 Prefill 和 Decode 是两种“性格不合”的任务
前面多次提到 Prefill 和 Decode 两个阶段,这里展开讲讲它们为什么“性格不合”。
Prefill 阶段要并行处理整个 prompt。用户输入 2000 个 token,模型要一次性把这些 token 都算一遍,计算量巨大、访存量相对少,属于典型的计算密集型任务。它的特点是可以用大 batch 把 GPU 的 Tensor Core 喂饱,耗时随 prompt 长度近似线性增长,但一次能同时算很多 token。
Decode 阶段完全反过来。每生成一个 token,只做增量计算,主要开销是读取模型权重和 KV cache,属于访存密集型任务。它需要的是高显存带宽,而不是超高的浮点算力,而且延迟极其敏感——用户对“一个字一个字往外蹦”的速度变化感知非常明显。
对比一下硬件偏好就清楚了:Prefill 想要更多算力,Decode 想要更多带宽。这两种需求同时压在一张卡上,本质上是让一张卡同时干两种不相干的活,谁也别想干痛快。
4.2 混在一起跑为什么会互相拖累
单用户、单请求的情况下,Prefill 和 Decode 自然串行,问题不大。但线上服务几乎都是多并发的:一个请求在 Prefill,另外几个请求在 Decode。如果调度器把这两类任务塞进同一个 batch,Prefill 任务会抢占大量计算资源,导致 Decode 的 token 产出被拖慢,用户感受到的就是“打字卡顿”。
反过来,Decode 任务也会挤压 Prefill 的算力,导致首 token 时间(TTFT)变长。最典型的表现是:并发一上来,TTFT 和 TPOT(每个 token 的产出间隔)互相拉高,谁也满足不了服务等级协议(SLO)。
PD 分离的核心思想就是把 Prefill 和 Decode 拆到不同的 GPU 池,甚至不同的机器上。Prefill 池用算力强的卡,Decode 池用带宽高的卡,中间通过传输 KV cache 完成交接。这样两类任务各干各的,资源不互相抢,各自的延迟指标都能稳住。
4.3 工程落地:拆池、调度与 KV cache 传输
具体落地大致分为四步:
- 把 GPU 集群划分成 Prefill 池和 Decode 池,各自跑独立的推理实例,比如分别启动两个 vLLM 实例。
- Prefill 实例处理完整条 prompt 后,得到该请求对应的 KV cache,通过高速网络(RDMA 优先)传给 Decode 实例。
- Decode 实例加载 KV cache 后,从第一个 token 开始逐 token 生成回复。
- 由一个轻量调度器负责把请求分发给不同的池,统一管理实例生命周期和负载。
这个架构的难点集中在两个地方。第一个是 KV cache 传输延迟,网络速度直接决定端到端延迟,KV cache 越大,传输越慢,所以长上下文场景对网络要求更高。第二个是池间负载均衡,Prefill 池太快会把请求全部倾泻给 Decode 池,让 Decode 池过载;太慢则会把 Decode 池饿死。工程上一般通过预分配缓存池、动态扩容、准入控制等手段来缓解。
vLLM 等开源项目已经提供 PD 分离的实验性支持,我在测试环境里跑通过,配置步骤并不复杂。但要真正在集群里稳定运行,网络、调度、容错的细节比单机推理多得多。如果是单机双卡或者个人研究环境,真没必要一上来就上这套,收益不明显还徒增运维复杂度。
4.4 什么场景值得上 PD 分离
我的判断标准很简单:看你的性能目标里是否同时要求很低的 TTFT 和很低的 TPOT,并且并发量足够大。如果只是个人部署、离线批量推理、或者在单块 GPU 上自用,PD 分离的收益几乎为零;如果是线上多租户服务、有明确的 SLO 承诺、GPU 规模在几十张以上,PD 分离就是最值得做的架构改造之一。
配套动作也要跟上:prefix caching(公共前缀复用 KV)、动态 batch 大小、请求准入控制,这些和 PD 分离配合起来才能发挥最大价值。PD 分离不是银弹,它是把“资源隔离”做到极致的思路,能不能发挥价值,取决于你的流量模型是否足够“多而杂”。流量不够大、需求不够复杂的场景,上了 PD 分离只会增加系统复杂度,徒增排查问题时的难度。
5. 三招组合起来用,以及我的避坑记录
5.1 组合优先级与叠加收益
如果只允许我先做一件事,我做量化。理由很实在:收益直接、改动小、几乎适用于所有场景,而且量化后的模型还能让后续的投机采样和 PD 分离都受益。第二件事做投机采样,因为在 Decode 阶段它能再拉一到两倍,而且对已有架构基本无侵入。最后才考虑 PD 分离,因为它是架构级改动,适合流量和规模都起来之后再动。
组合使用时的收益是可以叠加的:量化缩短每 token 的生成时间,投机采样减少需要生成的 token 步数,PD 分离让整体调度更稳定。三者乘起来效果很可观——量化带来 2 倍、投机采样再带来 2 倍、PD 分离解决稳定性问题,叠加之后单卡能承接的并发和吞吐完全不在一个量级。
不过我也要泼一盆冷水:组合越多,排查问题的难度越大。量化后的精度变化、投机采样的接受率波动、PD 分离下的网络延迟抖动,任何一个环节出问题,表现都可能是端到端延迟升高。如果你对系统的每一层都没有足够把握,建议先把量化这一层做扎实,再逐步叠加。
5.2 基准测试别只盯着吞吐量
跑 benchmark 时最容易犯的错是只看 throughput(tokens/s),忽略延迟分布和生成质量。我自己的习惯是同时记录四个指标,缺一不可:
- TTFT:用户等待首 token 的时间,Prefill 慢或者调度不稳定都会让它飙升。
- TPOT:相邻两个 token 的产出间隔,Decode 慢或者被其他任务抢占时它会变大。
- 端到端吞吐:一定并发下的总体 tokens/s,反映系统整体产能。
- 质量回归:相同 prompt 集合下的输出对比,哪怕只是人工抽检,也要做。
我遇到过的情况包括:量化后速度上去了但输出开始出现格式错误;投机采样开启后接受率低到几乎没收益;PD 分离后吞吐好看但 TTFT 抖动变大。只测一个指标,这些问题一个都发现不了。测延迟时也别只看平均值,p95、p99 才能真正暴露调度和抢占问题。
5.3 我的个人经验与建议
最后分享一点经验。学习这三项技术,最好的方式不是先啃论文,而是先动手。把 vLLM 或 llama.cpp 跑起来,对一个 7B 模型做 INT4 量化,对比速度变化;再加一个小草稿模型跑投机采样,观察接受率;最后有条件再尝试 PD 分离。每一步都亲眼看数据变化,比读十篇综述都管用。
我自己就是从“量化会不会严重损失质量”的怀疑开始,到把三个技术串成一套完整部署方案,前后用了不到一个月。最大的体会是:这些技术没有一个要求你成为数学天才才能用,但每一个都要求你理解瓶颈在哪。想不清楚瓶颈,出了问题连从哪里排查都不知道;想清楚了,你会发现这些加速手段本质上都在做同一件事——让 GPU 每一次计算都花在刀刃上。