一张A100的算力接近千T级别,可你在上面跑一个7B模型,每秒生成的token数常常只有二三十个。更离谱的是,打开nvidia-smi看GPU利用率,很多时候连20%都没到。这个现象我一开始怎么都想不通:明明算力这么强,为什么我花钱买的显卡在“摸鱼”?
直到我开始系统学习大模型推理的加速技术,把量化、投机采样、PD分离这三样东西彻底搞明白,才意识到:大模型推理慢,问题的根源压根不在“算力不够”,而是“数据搬运的速度跟不上”。这篇文章是我把这块重新啃了一遍之后的学习笔记加实测经验,适合正在做大模型部署、调优推理引擎、或者被线上token/s指标折磨的工程师。我会尽量把每个技术背后的“为什么”讲明白,再给出可以直接上手的配置思路和避坑经验。
1. 先搞清楚推理慢在哪:权重搬运比矩阵乘法更花钱
很多人在优化大模型推理的时候,第一反应是“模型太大、计算量太大”,于是拼命加GPU、堆算力。但真实情况恰恰相反。在推理的decode阶段,GPU的算力利用率经常低得可怜,瓶颈根本不在FLOPs,而在显存带宽——也就是GPU从HBM里面把权重搬到计算单元的速度。
1.1 从prefill到decode:两种负载的“性格”完全不同
大模型在线推理的核心流程拆开看,其实只有两个阶段。
第一个阶段是prefill(预填充):用户把一整段prompt丢进来,模型一次性并行处理所有输入token,计算每个位置上的隐藏状态和KV cache。这个阶段是典型的计算密集型,输入token越多,矩阵乘法越大,GPU的tensor core能跑得满满当当。
第二个阶段是decode(解码):模型每生成一个token,都要把这个token拼回输入序列,然后重新跑一次前向传播。注意,这次前向传播只为了算“下一个token”的概率分布,而参与计算的序列长度比prefill阶段长得多。也就是说,decode阶段每前向一次,都要把整个模型的权重从显存搬到计算单元里过一遍,但真正要算的新数据只有一个位置。
所以decode阶段本质上是带宽密集型负载。GPU算力再强,大部分时间都花在“等数据从显存送到寄存器”上了。
1.2 算一笔账:光重复读权重就要花掉7毫秒
我们用7B模型、FP16精度来算一笔账。7B参数乘以每个参数2字节,权重一共是14GB。A100 80G的HBM带宽大约是2TB/s。那么光是把这14GB权重完整读一遍,就需要:
14GB / 2TB/s ≈ 7ms这意味着即便GPU完全不计算,decode阶段每生成一个token,光“读权重”这个动作就已经花了7毫秒。实测里7B模型在A100上大概每秒生成20到40个token,算下来每token耗时25到50毫秒,权重搬运占了大头。
反过来看prefill为什么快?因为prefill一次性算几百上千个token,同样一次读权重的开销被摊到了很多token头上,计算密度高,GPU利用率自然就上去了。
这个认知是整个推理加速的基石:**decode阶段,谁能让“每次前向要读的权重字节数”变少,谁就能直接提升token/s;谁能让“串行前向的次数”变少,谁也能提升token/s。**量化和投机采样,恰好分别击中这两个点。
2. 量化提速的底层逻辑:把一次前向要读的字节数砍到原来的四分之一
量化的思路说白了很简单:模型权重本来是用FP16存储的,每个参数占2字节。如果能用INT8存,就变成1字节;用INT4存,就是0.5字节。7B模型的权重从14GB降到3.5GB,同样2TB/s的带宽,读一遍权重只需要1.75ms。decode的token/s理论上能翻近一倍甚至更多。
2.1 量化的基本公式与三种粒度
量化的本质是把连续的高精度浮点数映射到离散的低精度整数。最常见的是对称量化:
r_quant = round(r_float / scale)其中scale是缩放因子,通常由权重或激活值的绝对最大值决定。更完整的形式还会带上zero_point做零点对齐,用于非对称量化。
关键在scale怎么算、按什么粒度算。常见的粒度有三档:
| 粒度 | 说明 | 精度损失 | 实现复杂度 |
|---|---|---|---|
| per-tensor | 整个矩阵共用一个scale | 最大 | 最低 |
| per-channel | 每个输出通道一个scale | 中等 | 中 |
| per-group | 每128个元素一组一个scale | 较小 | 较高 |
现在主流的GPTQ、AWQ基本都做到了per-group粒度。group越小,量化误差越小,但也需要更多的元数据存储和计算开销,所以实际中128是一个常用的折中。
2.2 权重量化与激活量化:难度差了一个量级
这里有个新手最容易踩的坑:把“量化”当成一锤子买卖。
权重量化只动模型的参数,不动计算过程中的激活值。推理时依然用FP16算激活,只是把权重反量化回来再算。这种做法实现简单,不需要重新训练,用一小部分校准数据就能搞定,GPTQ和AWQ都属于这一类。
激活量化要复杂得多。它要求计算过程中的中间激活也变成INT8或INT4,这样才能真正用到低精度矩阵乘法的加速tensor core。问题在于激活值的分布远不如权重稳定,尤其在attention层会出现明显的离群值,直接量化会把信息打没。SmoothQuant这类方法的思路是先把激活里的离群值“平滑”到权重侧,再统一量化,实现W8A8。
从实际收益看:如果只做权重量化(W8A16或W4A16),主要省的是显存和带宽;如果做权重+激活量化(W8A8),才能吃到低精度矩阵乘法带来的计算加速。
2.3 我踩过的坑:敏感层、校准数据与评估方法
我最早自己用GPTQ量化一个7B模型,跑通用对话感觉还行,但一跑代码生成和数学推理,输出质量明显下滑。排查下来有两个原因。
第一,校准数据集和业务场景偏差太大。GPTQ需要一小批文本去统计权重误差补偿,如果校准数据是维基百科/新闻,而业务是代码,量化会把代码相关的分布压坏。后来换成业务相关的代码语料做校准,质量立刻回来不少。
第二,敏感层没有保护。attention输出投影和FFN的某些层对量化特别敏感,强行压到4bit会放大误差。现在很多方案支持混合精度,比如“大部分层用INT4、少数敏感层保留FP16”。我实测在代码生成任务上,只保护最后2层,质量损失就明显减小,速度损失却很小。
所以量化的正确打开方式不是“无脑全量化”,而是:先跑一份业务baseline,量化后再跑同样的用例,逐层对比,找出质量崩塌的层,再决定哪些层要豁免。
3. 投机采样:让大模型从“逐个写”变成“批改作业”
量化解决的是“每次前向读多少字节”的问题。投机采样解决的则是另一个问题:“能不能减少串行前向的次数”。
大模型decode的痛点在于每生成一个token都必须完整跑一次前向,而前向是串行的,第N个token没算出来之前,第N+1个token根本没法开始。那有没有可能一次前向算出好几个token?从数学上讲做不到精确并行,但可以“猜”。
3.1 整体流程拆解:草稿、验证、接受与拒绝
投机采样的流程分四步。
第一步,用一个很小的草稿模型(draft model)快速生成K个token的候选序列。草稿模型一般只有几亿到十几亿参数,每步生成只要几毫秒。
第二步,把草稿序列拼接进原有上下文,交给大模型(target model)做一次并行前向。注意这里是一次前向同时算出K个位置的logits,等价于一次prefill,而不是K次decode。
第三步,逐个位置做接受/拒绝判定。规则大概是:如果大模型对草稿token的置信度比草稿模型更高,就直接接受;如果更低,则以一定概率接受,拒绝后从大模型的分布里重新采样一个token补上。
第四步,把接受的部分当作正式输出,然后重新从草稿模型开始下一轮。
关键点在于:一次大模型前向如果运气好,能“顺带确认”多个草稿token,这就把原本K次串行decode压缩成了一步并行前向加K步草稿前向。
3.2 接受率与期望收益:一个简单的公式
投机采样能不能加速,完全取决于草稿模型和大模型分布的一致程度,数学上用一个接受率α来表示。如果每个位置接受概率都是α,那一次大模型前向期望接受的token数是:
E = 1 + α + α² + ... + α^K = (1 - α^(K + 1)) / (1 - α)我拿一个常见配置算一笔账。假设target模型每步40ms,草稿模型每步5ms,草稿长度K=6。
| 接受率α | 期望接受token数 | 一轮总耗时(草稿6步+大模型1步) | 等效每token耗时 | 对比基线40ms |
|---|---|---|---|---|
| 0.4 | 约1.66 | 6×5 + 40 = 70ms | 约42ms | 无加速甚至更慢 |
| 0.6 | 约2.43 | 70ms | 约29ms | 约1.4倍 |
| 0.8 | 约3.75 | 70ms | 约19ms | 约2.1倍 |
| 0.9 | 约5.22 | 70ms | 约13ms | 约3倍 |
这个表说明一个残酷事实:**投机采样不是无脑加速,接受率上不去反而可能拖慢。**α低于0.5时,草稿模型的成本甚至抵消不了收益。所以草稿模型的质量才是整个方案的关键变量。
3.3 草稿模型选型与参数对齐的实操建议
草稿模型怎么选呢?有一个必要条件:草稿模型和target模型的tokenizer必须一致,否则产生的是乱码token,接受率直接崩盘。
比较经典的搭配是Llama-2 7B配一个68M的草稿模型,社区里跑下来的效果普遍不错。现在的趋势是用EAGLE这类方法,直接复用target模型倒数第二层的hidden state来训练草稿头,接受率能比随机小模型高不少。
还有一个特别容易被忽略的细节:采样参数必须严格对齐。草稿模型和大模型推理时的temperature、top_p、top_k、repetition penalty都要保持一致,差别一丁点都会让分布偏差变大,接受率骤降。我一开始只对齐了temperature,忘对齐top_p,结果接受率掉了十几个百分点,排查半天才发现。
vLLM里开投机采样很简单,大致是这个形式:
vllm serve meta-llama/Llama-2-7b-chat-hf \ --speculative-model JackFram/llama-68m \ --num-speculative-tokens 6不同vLLM版本参数名可能略有差异,但核心就三个:草稿模型路径、草稿token数K、是否开启。K一般取4到8,太大会让草稿环节拖长,太小又吃不到并行红利。
4. PD分离:把Prefill和Decode拆到两拨GPU上,各干各的
量化解决带宽,投机采样解决串行,PD分离解决的是更上层的问题:两种负载混在一起,互相拖累。
4.1 为什么混在一起会互相拖累
传统的GPU推理引擎里,prefill和decode请求是在同一批GPU上混跑调度的。问题很明显。
第一,prefill和decode的“性格”完全不同,一个是计算密集型,一个是带宽密集型。把它们放在同一张卡上,无论调度策略怎么设计,总有一方在受罪。prefill大请求进来时,会抢走大量显存带宽和SM资源,正在跑的decode请求就会被卡住,用户的TPOT(单token生成时间)瞬间飙升,流式输出的体验变得一卡一卡的。
第二,长prompt的prefill会长时间独占GPU。比如一个10000 token的prompt进到引擎,prefill可能持续几百毫秒甚至更久,期间其他用户的decode都在排队。这就是为什么线上服务偶尔会出现“所有人一起卡顿”的灵异现象。
4.2 分离后带来的三个直接收益
PD分离的做法是把prefill阶段和decode阶段放到不同的GPU池子上。prefill节点拿到prompt,算完KV cache,把KV cache和中间结果传给decode节点;decode节点接着从KV cache开始逐token生成。
这件事有三个直接收益。
一是两个池子可以独立扩缩容。prefill节点可以用A100/H100这种算力怪物,decode节点可以用带宽充裕的卡,或者根据在线并发量动态调整decode节点数量。
二是用户侧的流式体验更稳定。decode节点不再被突如其来的prefill打断,TPOT的抖动大幅度减小,输出速度变得平稳。
三是每类节点都可以做针对性优化。prefill节点基本无需投机采样,把张量并行做满就行;decode节点则可以叠加量化和投机采样,把带宽瓶颈压到极致。
有意思的是,PD分离做得越彻底,KV cache的“跨节点搬家”就越重要。一个8B模型、GQA策略下,每个token的KV cache大约128KB到512KB。4K上下文长度的请求,KV cache就能到几百MB甚至上GB级别。要在节点间传输这么大块的数据,没有高速网络和高效的KV cache序列化格式,纯属白折腾。
4.3 落地需要付出什么:KV cache传输与调度复杂度
PD分离不是免费的午餐,它把原本在单机内部解决的问题推到了集群层面。
KV cache传输就是最大的新开销。现在的开源方案里,有把KV cache统一成标准序列化格式来绕开模型结构差异的,比如NVIDIA的NIXL;也有直接在框架层做管理的,比如vLLM加LMCache的组合,或者Moonshot开源的Mooncake方案。这些工具的思路都是把KV cache当成一等公民来管理:缓存、压缩、异步搬运、按请求路由。
另外,prefill节点和decode节点之间需要一套调度和队列逻辑。请求要先到prefill池算完KV cache,再被路由到decode池的某张卡上。这个路由如果做得糙,比如只按轮询,很容易出现部分decode节点排队、部分闲置的情况,需要按显存余量、token进度做细粒度调度。
对于没有大流量的团队,我的建议是先别直接上全套PD分离,而是用chunked prefill(分块预填充)做轻量替代:把一个长prompt切成多个块依次处理,避免单个长prefill长期独占GPU。效果上没有完全分离那么强,但架构改动小得多。
5. 三件套怎么组合:我的选型优先级与实测参考
很多人学完这三个技术会问:那我到底该上哪个?我的答案是:先看业务到底痛在哪,再决定优先级。
5.1 三个技术各自的“攻击面”
用一张表把三个技术的机制和适用场景理清楚:
| 技术 | 主要解决的瓶颈 | 核心机制 | 典型收益 | 主要代价 |
|---|---|---|---|---|
| 量化 | 显存带宽压力 | 降低每次前向读取权重的字节数 | token/s提升、显存占用下降、吞吐量提升 | 精度损失,需要校准和保护敏感层 |
| 投机采样 | 串行decode次数 | 用草稿模型+大模型一次并行验证,减少前向次数 | TPOT显著下降,单用户体验提升 | 需要额外草稿模型,接受率不稳定时可能负优化 |
| PD分离 | 混跑时的资源争抢 | prefill和decode分池调度、独立扩缩容 | 吞吐量提升、TPOT抖动减小 | KV cache跨节点传输、架构复杂度高 |
这三个技术不是互斥的,而是打在三个不同的瓶颈上。量化降低单次前向的成本,投机采样减少前向的次数,PD分离让整个引擎的调度不再互相拖累。
5.2 不同场景的优先级建议
如果你是在单卡上做个人部署或小团队内部服务,优先级很明确:先把量化跑通。INT4量化之后,7B模型在单卡上的显存占用从14GB左右降到4GB到5GB,很多之前跑不动的场景直接就跑起来了,速度也有实打实的提升。之后再评估上不上投机采样,前提是你找得到tokenizer兼容的草稿模型,并且能接受额外的调度复杂度。
如果你是在做企业级在线服务,流量大、并发高、对TPOT稳定性有要求,那PD分离反而是第一个该考虑的。它决定了整个系统的底座能不能水平扩展。底座稳定之后,再在decode节点上叠加量化和投机采样,把每一路请求的速度榨到极限。
5.3 我的最终推荐:先量化,再采样,最后做架构
我个人的调优顺序是固定的:先量化,再投机采样,最后才做PD分离。
原因很简单:量化的收益最确定,只要精度损失可控,基本是稳赚不赔;投机采样的收益取决于接受率,必须实测验证,但配置成本低,可以快速试;PD分离收益上限最高,但涉及整个集群的改造,应该放在最后动刀。
每一步做完都要用三个指标验证:TTFT(首token延迟)、TPOT(每token生成时间)、整体吞吐量。如果TTFT高,问题大概率在prefill侧,优先考虑chunked prefill或PD分离;如果TPOT高,优先考虑量化和投机采样;如果单路都正常但整体吞吐上不去,那就是调度和资源池的问题了。
另外想多说一句学习上的体会。大模型推理加速这门课,最忌讳的是上来就背各种框架参数。只要把“decode阶段带宽瓶颈”这条主线吃透,量化、投机采样、PD分离甚至后面那些新出的加速手段,本质上都是在围绕这条主线做文章:要么少读点数据,要么少跑几趟,要么把路分开走。搞懂这一点,看到任何新的加速方案,你都能很快判断出它到底在解决哪个环节的什么问题。