1. 推理为什么慢——瓶颈到底卡在哪一步先把结论放在前面所有能在网上看到的大模型推理加速方案本质上都在做同一件事——把生成一个token时实际上才必须发生的那一点点计算尽可能压缩同时把计算单元以外的时间尽量抹平。只要理解了这句话后面所有看起来花里胡哨的技术名词就都是同一逻辑下的不同解法。我从一个很朴素的问题开始说。你输入一句写一份关于周末露营的推荐清单模型返回了一整段文字。这个过程中发生了什么表面上是一次请求实际上内部是两段完全不同的计算第一段叫prefill把整段提示词一次性喂进模型并行算出所有token的中间状态第二段叫decode模型一个词一个词往外蹦每蹦出一个词都要基于之前所有的上下文重新做一次完整的矩阵运算。这两个阶段在GPU上的表现差异非常明显。prefill阶段因为可以并行处理整段输入GPU的利用率往往很高计算单元跑得很满。decode阶段则是另一个极端——每一步只生成一个token但每一步都必须读取当前全部KV Cache再用这些历史状态参与计算。这里KV Cache就是模型在生成过程中缓存的Key和Value矩阵你可以把它理解成到目前为止这句话的完整记忆。生成的内容越长这个缓存的体积就越大decode阶段的访存量就越大。用个生活化的类比prefill像是你一进餐厅就把整桌菜一次性点好后厨可以同时开几个灶一起做decode像是菜一道一道上每上一道菜你都要把整桌菜重新尝一遍才能决定下一道菜是什么。前者是并行快后者是串行等。大模型生成慢的体感99%来自decode阶段而不是prefill阶段。这引出了很多人在调优时的一个认知偏差盯着GPU算力百分比看半天发现利用率已经很高了但吞吐并没有明显提升。因为在decode阶段真正卡住你的不是浮点运算单元跑了多少而是数据搬运速度——显存带宽——能喂多少数据给计算单元。一个7B模型单次forward要读取的参数量大约是14GB按FP16算1000次decode就要搬运14TB数据。显存带宽再高这件事也要吃满毫秒级的时间。换句话说LLM推理在解码阶段是memory-bound不是compute-bound。这个判断是整个推理加速领域所有取舍的根基。2. 让KV Cache瘦身量化、GQA与稀疏化的真正价值KV Cache是decode阶段的大头开销之一而且它有一个让运维头疼的特点占用量随序列长度线性增长并发一多共享显存就告急。围绕它下手几乎是每个推理框架的必修课。这个方向上有三招比较常用但每一招的收益和代价都不同。2.1 KV Cache量化精度退让换来翻倍显存KV Cache的存储格式常见是FP16一个元素占2字节。如果把它压成INT8显存占用直接砍半带宽压力也随之下调。很多框架在实现时会选择只对Cache做量化而模型权重依旧保持较高精度这样精度的损失会被限制在生成下一个token的上下文计算这一层整体输出的稳定性通常可控。但这里有个经验教训KV Cache量化不能一刀切。不同层的Cache对精度敏感度差别很大。我之前试过对全部层统一压到INT8结果长文本生成的连贯性出现肉眼可见的下降后来改成了前几层量化、后半部分保留FP16的混合策略效果才稳定下来。现在主流推理引擎像vLLM、TensorRT-LLM里的实现很多也支持按层、按注意力头灵活设置量化策略而不是简单整模型统一处理。提示如果项目里需要处理超长上下文比如数万token的文档问答KV Cache量化的收益会比普通聊天场景明显得多值得优先做。2.2 GQA和MQA共享KV头减少重复劳动标准的MHAMulti-Head Attention里每个注意力头都有自己的KV投影头数一多常见32、48KV Cache就被撑大。GQAGrouped Query Attention的做法是把多个查询头共享同一组KV头MQAMulti-Query Attention则更进一步全部查询头共享一组KV。这会让Cache体积按头组比例下降同时减少decoding时的显存读取量。需要说明的是GQA/MQA这些结构在模型预训练阶段就决定了推理阶段改不了。所以这一招的实操意义体现在选型上——如果项目的核心负载是长文本和大量并发请求优先选用了GQA结构的模型例如Llama 2、Llama 3系列能天然省下不少Cache显存。反过来一个总喜欢用满KV精度的模型即使推理框架再努力优化也难抵消结构层面的劣势。2.3 稀疏化让注意力走神一下稀疏注意力或者KV Cache的裁剪思路是不是所有历史上下文都对生成下一个词有同等价值。比如滑动窗口注意力只保留最近若干个token的信息或者根据注意力权重分数把低分token的Cache丢掉。这个方向的代表性工作有StreamingLLM、H2O等。对这一类方法我的态度比较谨慎。文本生成是个很怕丢关键信息的任务一旦裁掉了某个前置线索后半段输出崩起来毫无征兆。它更适合用在对召回质量要求不极端、但延迟又必须压低的中长文本场景。真要上建议在离线评估里专门构造几类长程依赖的测试集比如需要引用文章开头细节的问答别只盯着指标好看。3. 算子层优化FlashAttention和它背后的IO思维注意力机制的公式本身不难——Query和Key算相似度再接Softmax再和Value加权。难的是GPU计算单元和显存之间的数据搬运。标准PyTorch实现里每一步中间结果都要写回显存再做下一步时又要读回来。一次注意力计算来回读写了多次中间矩阵时间全耗在路途上了。FlashAttention的思路是在SRAM芯片上的高速缓存里分块完成整个注意力的计算流程中间结果不落显存。用官方的话说叫IO-aware意思是你到底怎么少搬数据。这个思路的收益不完全体现在吞吐上更大的价值在于让长序列的注意力计算时间不至于爆炸。FlashAttention 2之后大部分主流框架已经把它作为标准算子接进去了单独拿出来说的意义不大。我想强调的反而是另一件事算子融合Kernel Fusion才是工程师日常要关注的。所谓算子融合是指把多个计算步骤在GPU上合成一个内核执行减少多次启动内核的开销。这个开销在短序列场景下尤其可感知——模型一浅、输入一短Launch Kernel的耗时占比就很刺眼。CUDA Graph技术做的就是更彻底的事把一整个decode步骤里所有算子提前捕获成一张执行图之后每次解码直接回放这张图跳过反复启动内核的CPU时间。vLLM和TensorRT-LLM里都默认或者默认推荐打开这个开关。实测下来小batch解码的延迟能降不少。注意CUDA Graph在动态shape场景下需要谨慎。它本质上是预先固定了一系列操作的尺寸如果输入长度或者并发batch频繁变化要么做padding统一shape要么在配置里开启合适的捕捉范围否则收益会打折甚至报错。4. 调度层的魔法连续批处理与PagedAttention这可能是近几年推理加速领域系统侧最出彩的两个成果也是普通优化碰不到、但收益最高的一块。它的出发点非常简单GPU跑推理的时候突发请求不是均匀到达的。如果用传统固定batch的调度方式要么把所有请求等齐了再一起算先来的请求被迫排队要么一有请求就单独算一个batchGPU吞吐又上不去。连续批处理Continuous Batching的做法是动态地往正在运行的batch里插请求、摘请求——某条序列一旦生成完就立刻离开让出位置给新请求进来。这样GPU总是在按最大batch能力运转而不是被最慢的那条链子拖住。这也是llama.cpp在客户端上做流式、服务端框架做高并发的共同根基。4.1 连续批处理的收益实测我做个比较朴素的对比固定批处理下当并发请求从32提升到64吞吐提升明显但单请求延迟会恶化的很快因为迟到的人被迫和前面的人结队而行而连续批处理下新请求只要GPU还有余量就能立刻接入单请求的尾延迟改善明显。生产环境中大部分框架默认走的都是这种迭代级调度——每个decode步骤结束都重新决定一批要计算的序列。这也是为什么同样一块A100不同调度框架的并发上限和延迟表现会有很夸张的差距。4.2 PagedAttention把虚拟内存管理搬进显存PagedAttention是vLLM的核心贡献。它借鉴了操作系统的分页机制传统做法是给每条请求的KV Cache一次性分配一整块连续显存但这些块往往用不满也容易碎片化。PagedAttention把KV Cache切成分页大小的块逻辑上连续的序列可以物理上分布在任意空闲块里再用一张块表做映射。这带来两个直接收益显存利用率显著提高——因为不需要为未来继续生成预留一整块连续空间以及共享前缀成了可能——同一个系统提示词对应的KV Cache可以跨请求复用这对很多带长System Prompt的线上应用是实打实的省钱。我在接入vLLM之前踩过一个很典型的坑用原生PyTorch写了推理服务并发一上去显存立刻爆batch稍微调大点就OOM。后来换到vLLM同样的显存容量把并发翻了近一倍。原因就是连续批处理把空闲时段的算力也塞上了而PagedAttention又把每条序列的Cache碎片全部挤出来了。这两件事叠加效果远大于各自单独抠出来的那点提升。5. 多卡协作张量并行、流水线并行与数据并行怎么分模型大到一个单卡装不下或者单卡推理速度不够时就要上多卡。但这三种并行策略解决的问题各不相同很多人容易混为一谈。数据并行是最容易理解的每张卡放一份完整模型各处理各的请求最后汇总结果。它不能降低单卡上的单请求延迟但能提升整体吞吐。张量并行是把一个Transformer层里的矩阵切成多块分别放在多张卡上协同计算每步计算都需要卡间通信所以网络延迟非常敏感。流水线并行则按层切分——GPU0算前几层GPU1算后面几层数据在层之间传递分批并行推进。实际部署中你很少看到只用其中一种的。常见组合是在同一台8卡机器内用张量并行因为NVLink带宽高跨机器集群则用流水线并行或者干脆做成多副本数据并行因为机器间网络带宽撑不起张量并行那种极频繁的同步。从推理框架的角度TensorRT-LLM对张量并行的优化非常细切分方式、通信原语都做过针对性调校。vLLM则把张量并行的接入门槛降了下来配置好张量并行大小它会自动处理层间的切分逻辑。很多时候我看到团队一开始为了省事直接上流水线并行结果卡间通信开销拖慢了整体速度。我的建议是能用张量并行优先张量并行除非模型大到单机8卡都塞不下再考虑流水线或跨机方案。6. 真正通用的加速配方采样到工程侧的一整套组合拳如果看完前面的内容还是纠结我到底该按什么顺序优化那我建议按下面这个顺序去试每做一步都重新做一次基准测试避免感觉优化了但实际没有变化的惨案。6.1 先看瓶颈是延迟还是吞吐服务的核心指标决定了优化方向。如果是聊天机器人类场景用户希望首token尽可能快那prefill阶段的并行优化、算子融合、小batch下的CUDA Graph回放是优先级最高的事。如果是批量离线推理、大量文档总结每秒钟处理多少条请求更重要那连续批处理、KV Cache量化、增大并发是收益来的最明显的地方。方向选错了后面每一步优化都可能白做或者相互掣肘。6.2 单机优化从这三步开始第一步确认推理框架该不该换。如果你还在用纯PyTorchHuggingFace跑服务建议先考虑迁移到vLLM或TensorRT-LLM这类专门为推理做过完整优化的引擎。多数情况下这一步能直接带来翻倍的吞吐提升因为仅在PagedAttention和连续批处理上的差距就非常可观。第二步开CUDA Graph。vLLM的启动参数里直接提供了相关开关TensorRT-LLM则为图模式做了深度定制。这一步免费且快速验证预期收益。第三步做量化。先只把权重从FP16换成INT8或INT4跑一遍业务侧的内置评测如果精度损失在可接受范围内就保留。然后再考虑KV Cache量化进一步挤显存。6.3 多卡和部署再往上提一层单卡优化到头了再考虑多卡。先做张量并行观察显存占用和延迟的变化。如果显存足够但延迟还高注意检查是否卡间通信占比过大。对于长序列、大并发场景确实存在单卡算得动但显存不够的情况这时张量并行能有效把单张卡的Cache负载放平。如果是要服务大规模生产流量最终的架构通常是入口做请求排队和负载均衡后端跑多个推理实例每个实例内部开连续批处理和KV Cache前缀缓存。外面再套一层自动扩缩容策略按GPU利用率或队列长度调节实例数。7. 基准测试里的坑如果测出来没有提升先检查这几件事最后必须专门腾出一节来聊测试因为优化做的再多测不准等于白做。很多人照着网上教程配好参数一跑benchmark发现延迟反升了第一反应是框架有问题。实际上更常见的干扰因素有几个。第一个坑是预热。GPU的频率提升需要时间CUDA上下文和显存分配也是第一次最慢。不预热直接计时测出来的数字没有参考价值。标准做法是先用几轮请求跑热通常几十次到百次然后再开始正式计时的测试。第二个坑是并发度的选择。只测单请求延迟和测100并发时的延迟看到的是两个世界。单token延迟偏低不代表高并发下吞吐就好反过来高吞吐压测时看着GPU跑满了但若批处理调度做的不够好单请求体验会非常不稳。所以报告的指标要成对出现延迟的分位值P50/P99和吞吐的大数。第三个坑是模型版本和输入长度的不一致。长输入例如几十页文档的总结和短输入日常对话的最优配置常常不同。KV Cache量化在大batch下收益大但短文本小额并发的时候根本没机会体现CUDA Graph对小模型短序列提升明显大模型长序列反而可能因为shape变化频繁导致回放命中率下降。要在目标场景的输入长度分布上测别拿自己平时随意聊的几条记录当benchmark。第四个容易被忽略的是GPU型号。显存带宽高的卡如A100、H100对量化带来的带宽收益会更敏感而在低带宽卡上同一个量化模型可能只是显存变小、速率提升有限。结论需要标注清楚测试环境否则换张卡就是另一番境地。老实说很多团队最后性能差距拉开的不是模型选型而是在这些测量细节上谁更较真。我在实际做过的多次优化里最能反复验证的一点是推理加速没有银弹它是一场把“计算、存储、调度、通信”四项成本逐个压低的组合工程。踩过几次坑之后我自己习惯每做一步优化都会先保存一份带完整环境信息的基准结果再去改配置然后回来对比同口径的数据。看着散乱的参数在一条条收敛比任何单点技巧都实在。如果你刚开始捣鼓自己服务的推理性能也建议按这个思路起步先量化瓶颈在哪再决定对哪一块下手。