ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型推理加速实战:量化、投机采样与PD分离

大模型推理加速实战:量化、投机采样与PD分离 上周有个朋友在群里扔了个问题“我的Qwen2.5-7B部署在3090上并发一上来了TTFT直接飙到4秒多每秒输出也就20个token左右显存还剩一点但不敢继续加并发怎么办”这几乎是每个跑大模型推理的人都会撞上的墙。单机部署大模型碰到的性能问题无外乎就这几类放不下、出字慢、首字慢、一并发就崩。而对应的落地解法恰恰就是这三年里圈内讨论最多的三条技术路径——量化、投机采样、PD分离。想把它们真正搞透光看博客和论文是不够的。我强烈建议你找一个迷你框架边读边改比如nano-vllm。它的代码量比vLLM小一个数量级但把连续批处理、页式KV缓存、调度器这些核心骨架都保住了非常适合拿来当推理加速的“人体解剖样本”。这篇内容我就用nano-vllm当主线把量化、投机采样、PD分离这三个加速手段从原理到实现再到部署参数一次讲清楚。适合正在做模型部署的工程师、刚转推理优化的算法同学以及想在简历上写“我做过推理加速”的人。1. 先把大模型推理的“慢”拆明白1.1 nano-vllm是什么为什么要拿它当“解剖样本”vLLM在大模型推理优化领域几乎是教科书级的存在但它为了做高性能塞了大量工程细节多级调度、自定义CUDA kernel、异步引擎、分布式通信……代码量很大。一个初学者直接读vLLM源码很容易迷失在连续批处理、页式KV、投机执行这些概念里。nano-vllm就是来解决这个问题的它用极少的核心代码实现了vLLM最精华的几个功能。我的理解是它保留了“每个请求的生命周期”这条主线——从请求进入调度器、分配KV页、喂给模型执行、采样输出、再回到调度器整条链路清晰可读。没有那么多并行分支和分布式血缘关系适合一条一条走读。所以在后面讨论每个加速技术时我会刻意沿用nano-vllm的代码结构来组织思路。它的做法未必是性能最优解却是在“理解原理”和“动手改造”之间最平衡的那一个。1.2 一次请求在GPU上究竟发生了什么大模型推理不是一次性完成的“输入→输出”而是两个截然不同的阶段Prefill预填充把用户输入的prompt整体喂给模型逐层计算生成完整的KV Cache。它的任务可以理解成“把上下文读完并记住”。Decode解码模型每次只生成一个token再把新token和已有KV Cache一起送进模型更新KV Cache然后继续生成下一个token循环往复。这两个阶段的计算特性完全相反。Prefill阶段一个token要跟整个上下文做注意力计算计算密度是Decode阶段的上百倍属于算力密集型Decode阶段每个step只算一个token的完整前向但每一层都得把模型权重从显存搬到计算单元权重搬一遍就只出一个token典型的访存密集型。如果你留意过GPU使用情况你会发现两种场景下卡的特性完全不一样Prefill吃满SM计算单元NVIDIA-smi显示算力利用率很高而Decode阶段感觉很“闲”算力利用率不高显存搬运压力却很大。1.3 别用“快不快”评价要看TTFT和TPOT衡量推理性能只看“每秒多少token”远远不够。业内习惯拆成两个指标TTFTTime To First Token从发出请求到收到第一个token的耗时主要由Prefill阶段决定。大模型聊天界面里“打字机”迟迟不出第一个字用户感知到的就是TTFT太长。TPOTTime Per Output Token或ITLInter-Token Latency单个token之间的间隔由Decode阶段决定。所谓“一秒出几个token”就是看这个指标。很多朋友只看总吞吐tokens/s在高并发在线推理场景下容易误判。比如某服务的总吞吐很高但每个请求的TTFT都很差用户照样觉得卡。上面提出问题的朋友TTFT 4秒、每秒20 token就是典型的Prefill和Decode互相拖后腿的结果。1.4 瓶颈三要素算力、带宽、容量GPU上影响推理性能的三个资源约束算力FLOPS决定Prefill阶段能跑多快。显存带宽GB/s决定Decode阶段每搬运一遍模型权重要多久。显存容量GB决定模型能不能放进去KV Cache能放多少。三个加速技术刚好对号入座量化主打容量和带宽双杀投机采样主打带宽利用率PD分离则让算力和带宽各司其职。记住这三句话后面所有细节都不会跑偏。另外在nano-vllm里你还需要提前把握两个基础概念连续批处理Continuous Batching和页式KV缓存Paged Attention。连续批处理指的是调度器在token粒度上做插入和退出不用等整个请求完成就能调度新请求Paged Attention则把一块请求的KV Cache切分成固定大小的页类似操作系统的虚拟内存减少显存碎片。这两个机制是后面讲调度和优化的地基。2. 量化加速最暴力也最划算的第一板斧2.1 量化的本质是压缩不是丢精度FP16/BF16每个权重占2字节INT8占1字节INT4占0.5字节。模型缩小了自然能装进更小的显存。更重要的是Decode阶段每个step都要从头到尾搬运模型权重权重字节数缩小一半TPOT理想情况下也能提升近一半。所以量化对TTFT和TPOT都有帮助但最大收益体现在Decode。量化的数学表达很简单x_q round(x / scale) zero_point x_approx (x_q - zero_point) * scalescale是缩放因子zero_point是零点用来让整数偏移到合适的范围。实际工程里每个通道、每个token组都会选择不同的scale目标是让量化前后的数值分布尽量相似。打个比方量化就像把无损音乐压成MP3。码率越低文件越小播放时越省流量但音质会有损失。大模型量化也是这样核心工程问题就是怎么用尽可能低的“码率”保留足够好的“听感”。2.2 主流量化方案怎么选GPTQ、AWQ、GGUF、MLX量化算法和格式很多初学者容易懵。我按实际使用场景整理一下GPTQ用Hessian矩阵的近似来做权重寻优把量化误差压到全局最小。实现成熟、现成库多、效果好是目前大模型INT4/INT8权重量化的主流。AWQ根据激活值的分布做“重要度感知”对重要的权重通道保留较高精度不需要Hessian速度更快有些任务上精度表现比GPTQ更稳。GGUFllama.cpp定义的格式支持CPU和GPU混合推理适合本地单机折腾。注意Q4_K_M这类命名方式和主流vLLM不通用。MLXApple Silicon生态的专用格式标题里出现的“mlx 4-bit推理”指的就是它主要跑在Mac的MPS路径上。常见档位参考以7B模型为例档位显存占用精度损失适用场景FP16约14GB无基准线INT8约7GB轻微显存宽裕、精度优先INT4约3.5GB可接受单卡部署、高并发优先这里说的主要是Weight-Only量化也就是只压缩权重KV Cache和激活函数保持原精度。这也是nano-vllm这类引擎最常做的量化。更激进的方案会继续量化KV Cache甚至激活值收益更大但复杂度也指数级上升。2.3 在nano-vllm里怎么接量化逻辑nano-vllm的教学重点在模型加载和前向计算。如果你想在它里面实现INT4权重量化最直接的做法是在模型构建时用自定义Linear替换原来的Linear层class QuantizedLinear(nn.Module): def __init__(self, in_dim, out_dim, bits): super().__init__() # 真实场景下qweight通常是int4打包存储 self.qweight None # 打包后的整数权重 self.scales None # 每组缩放因子 self.zeros None # 零点可选 self.bits bits def forward(self, x): # 教学写法先反量化回fp16再做矩阵乘 weight self.dequantize() return torch.nn.functional.linear(x, weight)教学版本直接把dequantize写完整就行。工程版本要考虑的则是整组共享缩放因子减少存储、反量化时的SIMD优化、以及直接用INT4张量核计算避免显式反量化。这也是为什么实际部署多数时候直接吃vLLM、llama.cpp这样已经优化好的引擎——自己重写一遍量化内核的成本实在太高。我在实测中比较推荐的做法是先加载FP16权重再调用HuggingFace生态里的量化库转成目标格式最后注入nano-vllm的自定义Linear层。这样既能理解量化原理又不会陷入底层格式解析的泥潭。2.4 量化实操的四个注意点校准数据要贴合真实用户输入。用通用语料校准出来的scale拿到代码推理或多语言聊天上精度丢失往往比预期大。想精细化就切一段线上日志做校准集。评估不能只看Perplexity。量化后困惑度变化不大不代表实际问答不出现胡言乱语。需要在业务样例上做回归对比至少抽几十到上百个代表性case看得失。提速预期要理性。纯Weight-Only量化Decode平均吞吐一般提升1.5到3倍。如果之前显存不够只能靠交换收益可能更大。别信“量化后必定翻4倍”的宣传。别把“量化交易”和“大模型量化”混淆。标题热词里的“python量化交易策略代码”、“比特币量化”属于金融统计套利产物是交易信号不是模型压缩。3. 投机采样用小模型“代笔”大模型“审稿”3.1 核心思想不是“猜更准”而是“并行校验”Decode慢的根本原因是每步只算一个token却要完整搬运一遍模型权重。投机采样的思路是准备一个速度快很多的小模型做草稿模型让它先快速生成一小段候选token序列大模型作为目标模型一次前向并行校验这批候选token。如果小模型猜对了大模型一个step就能产出多个token相当于“一次搬运多个产出”。用邮件审批来类比小模型是秘书大模型是老板。秘书先写好几封邮件的初稿老板一次过目多封。只要大多数初稿能直接盖章整体效率就大幅提升。关键是老板并没有因为看了草稿而改变自己“最终拍板”的身份对外发布的件都有老板背书。这里的数学保证很关键即使小模型的分布和大模型不一致通过rejection sampling修正在错误位置的采样分布最终输出仍然严格满足大模型自身的概率分布不会因为投机采样而“跑偏”。3.2 接受率与草稿长度两个决定收益的参数接受率Acceptance Rate是投机采样的生命线。如果草稿模型和正式模型的概率分布完全一致接受率为1分布差异越大接受率越低。草稿长度num_speculative_tokens直接影响单步收益。关于草稿长度有个简单直觉当接受率等于p时大模型一步最多产出约1/(1-p)个有效token。比如p0.7理论上最优草稿长度大约是3到4所以默认参数从4起步再微调很合理。太长会引入额外计算开销太短又吃不满并行的收益。草稿模型的选择通常比草稿长度更关键。我建议优先选同族的更小模型比如Qwen2.5-7B配Qwen2.5-1.5B或0.5BLlama3-8B配Llama3-1B。原因是同族训练的tokenizer、词表分布和知识表达天然一致草稿命中率会明显高于“拉郎配”式的跨模型组合。不同tokenizer是两个模型无法对齐候选token的硬伤。3.3 nano-vllm里投机采样的最小实现思路先不要急着扑到vLLM完整的speculative engine上。理解投机的核心逻辑足够在nano-vllm里做三处改动Scheduler支持一步为某个请求分配多个token位置。增加一个DraftModelWorker先用草稿模型生成候选token序列。目标模型一次前向拿所有候选token位置对应的logits做批量校验并决定接受哪些token。核心伪代码长这样def speculative_sampling(draft_logits, target_logits, draft_tokens, num_spec): accepted_tokens [] for i in range(num_spec): p_d draft_logits[i].softmax(dim-1) p_t target_logits[i].softmax(dim-1) r random.random() ratio p_t[draft_tokens[i1]].item() / p_d[draft_tokens[i1]].item() if r min(1, ratio): accepted_tokens.append(draft_tokens[i1]) else: # 修正采样重新抽一个token但要求满足大模型分布 accepted_tokens.append(corrected_sample(p_t, p_d, draft_tokens[i1])) break return accepted_tokens这个循环看起来不复杂真正实现时最大坑在batch维度不同请求的接受/拒绝发生在不同位置如果写成Python逐请求循环性能立刻退化。正确做法是把所有位置的接受决策做成mask矩阵一次性做整体处理这也是nano-vllm里值得动手练的工程点。另外草稿模型的KV Cache同样占显存。如果草稿模型和目标模型在同一张卡上显存预算需要提前扣掉这部分。不要量化完主模型发现草稿模型塞不下了。3.4 为什么有些场景提速不明显投机采样不是银弹下面几种情况经常让收益变弱草稿模型差别太大且接受率跌破0.5大模型在校验时频繁回退多出来的“返工”开销反而拖慢速度。目标模型已经跑得很满草稿模型占用的算力会让整体变慢甚至净亏损。GPU并行度不够时尤其明显。数据流里有大量代码、生僻词、混合语言等长尾token时小模型猜不准接受率下降明显。MoE模型比如Mixtral因为激活参数少Decode本来就不太吃权重带宽投机采样的边际收益没有稠密模型那么高。实测里我会先打印接受率再决定要不要继续用投机采样。如果接受率稳定在0.6以上收益可观低于0.5先换草稿模型或者调草稿长度别硬上。4. PD分离把两种性格的活分给不同的人4.1 为什么混在一起会互相拖累高并发的在线服务里Prefill和Decode混在同一张卡上矛盾非常典型计算密集的Prefill会抢占GPU算力资源把正在排队的Decode请求卡住Decode延迟抖动随之暴涨。反过来Decode请求占用的KV Cache和调度上下文又让Prefill难以连续拿到计算资源。如果业务平均输入很长比如几千到上万tokens的RAG场景Prefill占GPU时间超过30%甚至50%混在一起的结果就是用高算力的GPU生产Decode吞吐不理想用高带宽的GPU生产Prefill又太慢。PD分离Prefill-Decode Separation就是为了解决这个问题把两个阶段分配到不同GPU实例。P实例专职算Prefill产生完整的KV CacheD实例专职做Decode拿到KV Cache后开始自回归生成。4.2 架构与KV Cache传输的三条路PD分离的结构通常包含调度端、P实例组、D实例组。核心难点不在“拆开”而在“搬运KV Cache”。KV Cache传输常见三种手段CPU内存中转P实例把KV Cache读到内存再通过内存拷贝给D实例。实现简单但带宽低、延迟高适合实验环境。InfiniBand/RDMA低延迟高带宽适合大规模集群部署。共享显存/统一内存如果P和D在同一物理节点或同一芯片池可以通过NVLink或厂商统一内存空间直传延迟最低。实操中需要注意的是KV Cache体积很大。一个64K上下文的请求KV Cache可能有几百MB甚至几个GB。传输时间会直接成为TTFT的一部分所以PD分离最需要压的就是这块传输成本。4.3 从nano-vllm出发理解调度状态变化想理解PD分离的调度变化不一定要真搭多台机器。在nano-vllm上写一个“业务级KV传输”抽象就够了EngineCore在P模式下负责Prefill把prompt编码成KV Cache暂存到CPU内存池。EngineCore在D模式下恢复KV Cache继续解码。对应的调度器增加两个状态KV_TRANSFER和D_PREFILL_SCHEDULED。一个请求在P侧完成Prefill后进入KV_TRANSFER挂在传输队列D侧收到KV后才进入连续批处理调度。理解到这一层后你去看vLLM新版代码里的多实例调度就会发现思路是相通的。另外提醒一句Chunked Prefill是PD分离的“轻量替代版”它把长Prefill切成小块和Decode token交替执行让计算密度平滑下来不需要拆GPU。没条件上PD分离的团队先打开vLLM的chunked prefill选项通常也能缓解TTFT波动。4.4 到底值不值得上PD分离业务特征建议长上下文为主RAG、文档问答值得上收益最明显短上下文、高并发聊天先调chunked prefill和量化单机双卡或以上可以实验PD分离单卡独立部署先别想量化和投机采样做到位再说评估PD分离时不要只看峰值tokens/s。我的经验是分开度量三个数据P侧Prefill的GPU利用率、D侧Decode的排队深度、KV传输耗时。很多时候PD分离优化的是整个服务的长尾延迟而不仅是某个单指标极限。P和D的卡数配比也不一定一开始就准确常见起步区间是1:2到1:4随着平均上下文长度和并发模型不断调。5. 实战中踩过的坑与排查思路5.1 量化后模型越跑越“呆”症状量化后回答质量明显下降特别是代码生成、多步推理、中文生僻字。排查方向确认用的量化档位和算法。INT4用GPTQ还是AWQ不同任务表现有差异。校准数据是否覆盖了线上真实输入。通用语料校准的结果通常偏“乐观”。对比Perplexity和业务指标。Perplexity略升但100个业务测试只退化1到2个case可接受退化成规模出现就降到INT8或做混合精度。有个小技巧算力不够又想要精度时只量化MLP层、保留Attention层的FP16权重经常能在精度和速度之间找到更好的折中。5.2 投机采样的“加速比”不升反降排查思路先检查草稿模型和主模型的tokenizer是否完全一致。这是最容易被忽略的硬错误。在代码里打印实际接受率。如果长期低于0.5换草稿模型或调整草稿长度。确认草稿模型和目标模型是否在同一卡上草稿生成的额外开销是否吃掉了减速收益。需要时把草稿模型放CPU或单独一张卡很多时候比硬凑在同一张卡更稳。5.3 PD分离下TTFT反而变高排查方向KV传输耗时是不是占了TTFT的大头。传输后端带宽不够就换RDMA或shared memory。D侧是否要等当前批处理结束后才把KV Cache调度进来调度时隙太长也是常见原因。D实例队列是否积压严重。PD分离的前提是流量规划不是把大量Decode任务全堆给同一台D机。D机过载照样会打回原形。5.4 显存OOM与缓存抖动排查方向KV Cache池大小和模型显存分配是否冲突。很多OOM不是模型权重占满而是KV池分配不足。PagedAttention的复用策略有没有打开。开启prefix caching后相似前缀能命中KV缓存长RAG场景收益很明显。量化和投机采样组合使用时草稿模型的KV Cache也会占显存这是预算里容易忽略的一项。5.5 别迷信“看起来快了”先量化指标做任何优化前我至少会先记录这么几组数据TTFT、TPOT、吞吐tokens/sDecode阶段的显存带宽利用率Prefill阶段的SM利用率投机采样的接受率PD分离时KV传输耗时占比有了这些基线每个改造方案上线后都能用数据说话而不是“感觉快了一点”。这也是从“看懂代码”到“真正会调优”之间的关键一步。6. 给学习者的路线图和几个压箱底建议如果你正打算系统啃下大模型推理加速这块我给一条亲测有效的路径第一步把nano-vllm的scheduler读通。理解一个请求从进入引擎到输出最后一个token的完整生命周期这是所有优化的地基。第二步亲手实现一个最简单的greedy decode baseline测量TPOT。这一步会让你直观感受到Decode阶段“权重搬来搬去”的瓶颈。第三步按“量化 → 投机采样 → PD分离”的顺序逐个引入。量化最容易看到效果投机采样需要理解概率验收的细节PD分离则先画清楚拓扑图再动手。最后保持和GPU metrics、profiler数据较真的习惯。任何一个“优化”如果没有指标支撑就一律当作玄学处理。最后分享一个我踩过多次的教训入门阶段最容易犯的错是想一次性把三个技术全加进去结果每次改完都不知道是哪个环节起作用遇到问题也不知道回滚到哪一步。现在我坚持“一次只加一项优化跑同一组基准case记录同一组指标收益不明显就回滚”。看起来慢实际是最快能让你通透理解这些加速机制的方式。
返回列表