☰
大模型推理加速实战:量化、投机采样与PD分离组合拳
2026/10/6 17:54:21 网站建设 项目流程

大模型推理加速这事儿,我最近大半年一直在跟。从最初老老实实跑 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 主流量化方案怎么选

目前实际部署中用得最多的几类方案,我把它们整理成一张表,方便对照选型:

方案精度特点适用场景
GPTQINT4 / INT8基于二阶信息逐层量化,需要校准集单卡部署大模型,vLLM 支持成熟
AWQINT4按激活值重要性保护关键权重通道与 GPTQ 同场景,通常精度更稳
SmoothQuantINT8 权重+激活把激活值里的 outlier 迁移到权重侧需要同时量化激活的在线服务
FP8FP8(E4M3/E5M2)训练/推理同精度,H100 等新卡支持好高端显卡、训练后直接部署
GGUF Q4_K_MINT4 混合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 草稿+验证+拒绝采样的完整流程

标准流程分成四步,这里用实际例子说清楚:

  1. 草稿模型自回归生成 k 个候选 token。比如 k=4,小模型先写出“中国的首都是”,接下来四个 token 它猜“北、京、是、一”。
  2. 把这 4 个候选 token 接到大模型输入的末尾,做一次前向计算。因为是一次处理 4 个位置,大模型一次就能拿到这 4 个位置各自的目标分布。
  3. 从第一个候选 token 开始逐个做拒绝采样验证:如果目标分布对“北”的概率足够高,通过随机数判断后接受,继续看“京”;如果某个 token 不达标,就在这个位置用目标分布重新采样一个 token 作为修正,后面未验证的候选全部丢弃。
  4. 被接受的 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 传输

具体落地大致分为四步:

  1. 把 GPU 集群划分成 Prefill 池和 Decode 池,各自跑独立的推理实例,比如分别启动两个 vLLM 实例。
  2. Prefill 实例处理完整条 prompt 后,得到该请求对应的 KV cache,通过高速网络(RDMA 优先)传给 Decode 实例。
  3. Decode 实例加载 KV cache 后,从第一个 token 开始逐 token 生成回复。
  4. 由一个轻量调度器负责把请求分发给不同的池,统一管理实例生命周期和负载。

这个架构的难点集中在两个地方。第一个是 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 每一次计算都花在刀刃上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询