1. 大模型推理加速的核心逻辑拆解
聊到大模型推理加速,很多人第一反应是“换更快的显卡”或者“上更激进的量化”。但真正在生产环境里摸爬滚打过的人都知道,推理速度的瓶颈往往不在算力峰值,而在显存带宽、KV Cache 管理、算子融合和批处理策略这四个维度的协同优化。我见过太多团队花大价钱买了顶级卡,结果推理吞吐还不如别人用中端卡跑出来的成绩,问题就出在只盯着算力,忽略了数据搬运的开销。
大模型推理的本质是什么?用一句话概括:自回归生成过程中,每一步都要把整个模型的权重从显存里读一遍,再和当前 token 的激活值做矩阵乘法。这就意味着,当 batch size 较小的时候,计算单元大部分时间在等数据从显存搬过来,算力利用率可能连 20% 都不到。所以“最快推理”的第一性原理不是算得快,而是让数据搬运的次数和量级降到最低。
基于这个逻辑,推理加速的路线可以分成几个层次来理解。最底层是数值精度层面的优化,比如 FP16、BF16、INT8、INT4 量化,直接减少权重和激活值的位宽,从而降低显存占用和带宽压力。往上一层是算子层面的优化,包括 FlashAttention、PagedAttention、算子融合等技术,目的是减少中间结果的读写次数。再往上是调度层面的优化,比如 Continuous Batching、Speculative Decoding、Chunked Prefill,让 GPU 尽可能不空闲。最顶层是系统架构层面的优化,比如张量并行、流水线并行、PD 分离部署,解决单卡放不下或单卡吞吐不够的问题。
这四个层次不是互斥的,真正的最快实现往往是多层组合。举个例子,你用 INT4 量化把权重压到原来的四分之一,再用 FlashAttention 把注意力计算的显存访问降下来,最后用 Continuous Batching 把多个请求拼在一起跑,整体吞吐可能比朴素实现高出十几倍甚至几十倍。但每一层都有坑,量化太狠会掉精度,算子融合要考虑硬件兼容性,批处理策略要平衡延迟和吞吐。下面我会逐层拆开讲,把每个环节的关键参数、实操要点和踩坑经验都摊开来说。
注意:推理加速没有“一招鲜”的方案。不同模型结构(Dense vs MoE)、不同硬件(消费卡 vs 数据中心卡)、不同场景(离线批处理 vs 在线低延迟)对应的最优策略完全不同。先明确你的瓶颈在哪,再选工具。
2. 精度量化:用更少的位宽换更大的带宽
2.1 量化方案怎么选:从 FP16 到 INT4 的取舍
量化是推理加速里性价比最高的一招,没有之一。原因很简单:大模型推理是显存带宽瓶颈型任务,权重占用的显存少了,每步推理需要搬运的数据就少了,速度自然就上去了。以 LLaMA 架构的 70 亿参数模型为例,FP16 精度下权重占用约 14GB,而 INT4 量化后只需要约 3.5GB,显存带宽压力直接降到四分之一。
但量化不是免费的午餐。FP16 到 INT8 通常精度损失很小, perplexity 上升不到 0.1,基本可以无脑上。INT8 到 INT4 就要小心了,尤其是权重量化(Weight-Only Quantization)在低比特下容易出现明显的精度下降。我实测过几个主流方案,在 7B 模型上,GPTQ 的 INT4 量化在大多数任务上能保持 97% 以上的原始精度,AWQ 稍微好一点,能到 98% 左右,但 GGUF 的 Q4_K_M 在长文本生成上偶尔会出现重复和逻辑断裂。
选择量化方案时,核心看三个指标:精度保持率、推理速度提升比、硬件兼容性。GPTQ 和 AWQ 都需要校准数据集,校准集的质量直接影响量化效果,一般用 128 到 512 条覆盖多领域的文本就够了。GGUF 则是 llama.cpp 生态的格式,适合 CPU 或混合推理场景,GPU 上跑不如 GPTQ/AWQ 快。如果你用的是 vLLM 或 TensorRT-LLM 这类推理引擎,它们对 GPTQ 和 AWQ 的支持最好,直接加载量化后的权重就能跑。
| 量化方案 | 典型位宽 | 精度保持 | GPU 推理速度 | 适用场景 |
|---|---|---|---|---|
| FP16 | 16 bit | 100% | 基准 | 精度要求极高、显存充足 |
| INT8 | 8 bit | 99%+ | 1.5-2x | 通用场景,几乎无精度损失 |
| GPTQ | 4 bit | 97% | 2.5-3.5x | GPU 推理,vLLM/TensorRT 生态 |
| AWQ | 4 bit | 98% | 2.5-3.5x | GPU 推理,对激活值敏感场景 |
| GGUF Q4_K_M | 4 bit | 95-97% | CPU 为主 | 本地部署、CPU 推理 |
2.2 量化实操:校准集构建与精度验证
自己动手做量化的时候,最容易翻车的地方是校准集。很多人随便找几百条通用文本就跑 GPTQ,结果量化出来的模型在代码生成或数学推理上表现一塌糊涂。校准集的核心原则是分布匹配:你的模型将来要处理什么类型的输入,校准集就要覆盖什么类型。如果模型主要用于对话,校准集里就要有足够多的多轮对话样本;如果用于代码补全,就要放代码片段。
具体操作上,以 GPTQ 为例,校准集大小建议 128 到 256 条,每条长度在 512 到 2048 token 之间。太短了覆盖不了长距离依赖,太长了量化过程会非常慢。校准集的数量也不是越多越好,超过 512 条之后精度提升边际递减,但量化时间线性增长。我一般用 256 条,在 7B 模型上量化一次大约 20 到 30 分钟(单卡 A100),精度损失控制在 2% 以内。
量化完成后必须做精度验证,不能只看 loss 曲线。我通常跑三组测试:第一组是标准 benchmark(如 MMLU、HumanEval),看整体能力有没有崩;第二组是领域内测试集,看具体任务表现;第三组是人工抽检,生成 50 到 100 条输出,肉眼检查有没有明显的重复、乱码或逻辑错误。三组都过了才算量化成功。如果发现精度掉得厉害,优先检查校准集是否覆盖了触发问题的输入类型,其次考虑换 AWQ 或提高量化位宽。
实操心得:量化后的模型在 temperature 较高时更容易暴露问题。验证时把 temperature 设到 0.8 到 1.0,top_p 设到 0.9,这样能更快发现潜在的生成缺陷。
3. 算子优化:把显存访问次数压到最低
3.1 FlashAttention 为什么能提速:从 IO 感知说起
标准注意力计算的问题在于,它需要把 Q、K、V 三个矩阵完整地写回显存,再读出来做 softmax,再写回去做加权求和。这个过程涉及多次显存读写,而显存带宽是有限的。FlashAttention 的核心思想是分块计算 + 在线 softmax,把注意力矩阵切成小块,在 SRAM(片上缓存)里完成计算,避免中间结果写回显存。这样一来,显存访问次数从 O(N²) 降到 O(N²/M),其中 M 是 SRAM 的大小。
实测数据很能说明问题:在序列长度 2048、head dim 128 的配置下,FlashAttention-2 比标准注意力实现快 2 到 4 倍,显存占用降低 5 到 10 倍。序列越长,优势越明显。当序列长度到 8192 时,标准注意力可能直接 OOM,而 FlashAttention 还能正常跑。这也是为什么现在几乎所有主流推理引擎都默认集成 FlashAttention。
但 FlashAttention 不是万能的。它对 head dim 有要求,通常需要是 8 的倍数,且最好不超过 256。如果 head dim 太小(比如 32),分块计算的收益会下降。另外,FlashAttention 目前对某些特殊 mask 模式(如滑动窗口注意力、因果 mask 的变体)支持不够完善,需要检查所用版本是否覆盖了你的需求。vLLM 和 TensorRT-LLM 都内置了高度优化的 FlashAttention 实现,直接用就行,自己手写反而容易踩坑。
3.2 PagedAttention 与 KV Cache 管理:显存碎片化的终结者
KV Cache 是大模型推理的另一个显存大户。在自回归生成中,每生成一个 token,就要把对应的 Key 和 Value 缓存下来,供后续 token 计算注意力时使用。传统实现里,KV Cache 需要预分配连续显存,按最大序列长度来算,这会导致严重的显存碎片化和内部碎片。比如你预分配了 2048 长度的空间,但实际只用了 300,剩下的 1748 就浪费了。
PagedAttention 的思路借鉴了操作系统的虚拟内存分页机制:把 KV Cache 切成固定大小的块(block),每个块可以存放在不连续的显存位置上,通过页表来索引。这样一来,显存利用率能从 20% 到 40% 提升到 90% 以上,同时支持不同请求之间共享 KV Cache 块(比如 system prompt 的缓存复用)。vLLM 就是靠 PagedAttention 把吞吐做到了同级别引擎的 2 到 4 倍。
配置 PagedAttention 时,关键参数是block_size,通常设为 16 或 32。block_size 太小,页表开销大;太大,内部碎片多。16 是大多数场景下的甜点值。另外要关注gpu_memory_utilization参数,vLLM 默认是 0.9,意思是预留 90% 的显存给模型权重和 KV Cache。如果显存紧张,可以调到 0.85 到 0.95 之间,但不要超过 0.95,否则容易 OOM。
注意:PagedAttention 对显存对齐有要求,如果模型权重不是按 block 对齐的,可能会触发额外的拷贝。用 vLLM 加载模型时,它会自动处理对齐问题,但如果你自己写推理代码,需要手动确保张量形状是 block_size 的整数倍。
4. 调度策略:让 GPU 每一毫秒都在干活
4.1 Continuous Batching:从静态批到动态批的跃迁
静态批处理的问题在于,一个 batch 里所有请求必须等最长的那个生成完才能一起返回。如果 batch 里有一个请求要生成 500 个 token,其他请求只要 50 个,那 GPU 在后面的 450 步里都在为那一个请求服务,其他计算资源全浪费了。Continuous Batching(也叫 In-flight Batching)的思路是:每个解码步都重新组 batch,已经生成完的请求立刻退出,新来的请求立刻加入,GPU 永远在跑满 batch 的状态。
这个策略对吞吐的提升是数量级的。在混合负载场景下(长短请求混杂),Continuous Batching 能把吞吐提升 5 到 10 倍。vLLM、TensorRT-LLM、TGI 都默认开启了这个功能。但要注意,Continuous Batching 会增加首 token 延迟的波动,因为新请求加入时可能需要等待当前步计算完成。如果对延迟极其敏感,可以设置max_num_seqs限制并发请求数,或者用 Chunked Prefill 把长 prompt 的预填充拆成多个小块,避免阻塞解码步。
4.2 Speculative Decoding:用小模型加速大模型
Speculative Decoding 的思路很巧妙:用一个小的 draft 模型先快速生成 K 个 token,然后让大模型一次性验证这 K 个 token 是否接受。如果 draft 模型的预测和大模型一致,就一次性接受多个 token,相当于把 K 步解码压缩成 1 步。实测下来,在 draft 模型和大模型分布接近的情况下,加速比能到 2 到 3 倍。
但 Speculative Decoding 的坑也不少。首先,draft 模型的选择很关键,它必须和大模型有相似的分布,否则接受率太低,反而拖慢速度。通常用同系列的小模型(比如 7B 配 1.5B),或者用蒸馏过的 draft 模型。其次,K 值的选择要平衡:K 太小,加速效果不明显;K 太大,验证成本高,且接受率下降。一般 K 取 4 到 8 比较合适。最后,Speculative Decoding 对显存有额外要求,因为要同时加载两个模型,显存不够的话反而会触发 swap,速度暴跌。
| 调度策略 | 吞吐提升 | 延迟影响 | 显存开销 | 适用场景 |
|---|---|---|---|---|
| 静态批处理 | 基准 | 稳定 | 低 | 离线批处理 |
| Continuous Batching | 5-10x | 首 token 波动 | 中 | 在线服务 |
| Chunked Prefill | 2-3x | 降低长 prompt 阻塞 | 中 | 长 prompt 混合负载 |
| Speculative Decoding | 2-3x | 略增 | 高(双模型) | 低并发低延迟 |
5. 系统架构:单卡不够就多卡,但别乱并行
5.1 张量并行 vs 流水线并行:怎么选怎么配
当模型大到单卡放不下时,就必须上多卡并行。张量并行(Tensor Parallelism)是把每一层的权重矩阵按列或按行切分到多张卡上,每张卡计算一部分,然后通过 All-Reduce 汇总结果。流水线并行(Pipeline Parallelism)是把模型按层切分,不同卡负责不同层,数据像流水线一样依次通过。两种方式各有优劣:张量并行通信量大但延迟低,适合节点内多卡;流水线并行通信量小但有气泡(bubble),适合跨节点。
实际部署中,最常见的是张量并行 + 流水线并行混合。比如 8 卡节点内用张量并行度 8,跨节点用流水线并行度 2,总共 16 卡跑一个 70B 模型。配置时要注意,张量并行度不要超过单机 GPU 数,否则跨机通信会成为瓶颈。流水线并行度则要考虑 micro-batch 的数量,micro-batch 太少气泡占比高,太多则显存压力大。一般 micro-batch 数设为流水线并行度的 2 到 4 倍比较合适。
5.2 PD 分离:预填充和解码分开部署
预填充(Prefill)和解码(Decode)的计算特征完全不同。预填充是计算密集型,需要大量矩阵乘法,GPU 算力利用率高;解码是显存带宽密集型,每步只生成一个 token,算力利用率低。把两者放在同一个实例上跑,会导致资源争抢:预填充来了,解码就得等;解码在跑,预填充又占不满算力。
PD 分离的思路是把预填充和解码拆到不同的实例组上,预填充组用高算力配置,解码组用高带宽配置,中间通过 KV Cache 传输连接。这样预填充组可以批量处理长 prompt,解码组可以专注低延迟生成。实测下来,PD 分离能把整体吞吐提升 30% 到 50%,同时降低尾延迟。但实现复杂度高,需要解决 KV Cache 的高效传输和调度问题。目前 vLLM 和 TensorRT-LLM 都在逐步支持 PD 分离,但生产环境部署还需要不少定制工作。
实操心得:PD 分离的 KV Cache 传输是瓶颈,建议用 RDMA 或 NVLink 做跨实例通信。如果只能用普通网络,传输延迟可能吃掉分离带来的收益。
6. 常见问题与排查技巧实录
6.1 推理速度不达预期?先查这五个地方
第一个要查的是 batch size。很多人跑 benchmark 时用 batch size 1,然后抱怨速度慢。实际上大模型推理在 batch size 到 8 到 32 之间时,吞吐会随 batch 增大而线性增长,直到算力打满。如果你只跑单条请求,GPU 利用率可能只有 10% 到 20%,速度自然上不去。先确认你的场景能不能合批,能合批就尽量合。
第二个要查的是 KV Cache 是否开启。有些推理框架默认不开启 KV Cache,或者配置里写错了导致没生效。没有 KV Cache 的话,每生成一个 token 都要重新计算整个序列的注意力,速度会慢几十倍。检查方法很简单:看显存占用是否随序列长度增长,如果不增长,大概率是 KV Cache 没开。
第三个要查的是量化是否真正生效。有些框架加载量化模型时,如果配置不对,会反量化回 FP16 再计算,等于白量化了。检查方法是看推理时的显存占用和计算精度,如果显存占用和 FP16 一样,说明量化没生效。
第四个要查的是是否有 CPU 回退。某些算子如果 GPU 不支持,框架会自动回退到 CPU 执行,速度直接掉两个数量级。用 profiler 抓一下 timeline,看看有没有 CPU 上的算子。
第五个要查的是显存是否打满。如果显存利用率超过 95%,可能会触发显存交换或 OOM 重试,速度会剧烈波动。用nvidia-smi或框架自带的监控工具看显存使用曲线,保持在 85% 到 92% 之间比较健康。
6.2 精度掉了怎么办?按这个顺序排查
精度问题比速度问题更难查,因为影响因素多且隐蔽。我的排查顺序是:先确认量化是否是元凶,再查 KV Cache 精度,最后查算子实现。
第一步,把量化关掉,用 FP16 跑同样的输入,看精度是否恢复。如果恢复了,说明是量化的问题,需要调整校准集或换量化方案。第二步,如果 FP16 下精度也有问题,检查 KV Cache 是否用了低精度存储。有些框架为了省显存,会把 KV Cache 存成 INT8,这会引入额外误差。第三步,如果前两步都没问题,检查是否有自定义算子或融合算子引入了数值不稳定。比如某些 FlashAttention 实现在极端序列长度下会有精度损失,换回标准注意力就能确认。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 速度慢但 GPU 利用率低 | batch size 太小 | 看 GPU 利用率 | 增大 batch 或开启 Continuous Batching |
| 显存占用不随序列增长 | KV Cache 未开启 | 检查配置和显存曲线 | 开启 KV Cache 并确认生效 |
| 量化后速度没提升 | 量化未生效或反量化 | 看显存占用和计算精度 | 检查量化配置,确认加载的是量化权重 |
| 精度下降明显 | 量化校准集不匹配 | 换 FP16 对比 | 重建校准集或换 AWQ |
| 长序列 OOM | KV Cache 预分配过大 | 看序列长度和显存关系 | 用 PagedAttention 或减小 block_size |
| 多卡加速比不线性 | 通信瓶颈 | 看 NCCL 通信耗时 | 调整并行策略,减少跨机通信 |
6.3 独家避坑技巧:那些文档里不会写的事
技巧一:预热很重要。推理引擎第一次加载模型时,会有编译和初始化开销,第一次推理可能比稳定状态慢 10 倍以上。生产环境一定要做预热,用几条典型请求跑几轮,等速度稳定后再接流量。
技巧二:动态调整 max_num_seqs。vLLM 的max_num_seqs默认是 256,但在显存紧张时会导致频繁的 KV Cache 换入换出。根据你的显存大小和平均序列长度,把它调到 64 到 128 之间,往往能获得更稳定的吞吐。
技巧三:监控首 token 延迟和每 token 延迟。这两个指标要分开看。首 token 延迟主要受预填充影响,每 token 延迟主要受解码和 KV Cache 影响。如果首 token 延迟高但每 token 延迟正常,说明预填充是瓶颈,可以考虑 Chunked Prefill 或 PD 分离。如果每 token 延迟高,说明解码阶段有问题,查 KV Cache 和批处理策略。
技巧四:不要迷信 benchmark 数字。很多论文和博客里的加速比是在理想条件下测的,实际场景中因为请求模式、序列长度分布、硬件差异,加速比可能打对折。自己动手测,用真实流量做压测,才是唯一可信的数据。
技巧五:版本兼容性是个大坑。推理框架、CUDA 驱动、量化工具链之间的版本兼容性非常脆弱。升级任何一个组件之前,先在测试环境验证,确认精度和速度都没有回退再上生产。我见过太多次因为升级 vLLM 导致量化模型加载失败或速度暴跌的案例。
7. 从零搭建最快推理服务的实操路线
如果你现在要从零搭一个推理服务,我建议按这个顺序来:先跑通 FP16 基线,再上量化,然后开 Continuous Batching 和 PagedAttention,最后根据瓶颈决定是否上多卡或 Speculative Decoding。每一步都做压测,记录吞吐、首 token 延迟、每 token 延迟和显存占用四个指标。不要一次性把所有优化都打开,否则出了问题根本不知道是哪个环节导致的。
具体工具选型上,GPU 推理首选 vLLM 或 TensorRT-LLM。vLLM 上手快、社区活跃、对量化支持好;TensorRT-LLM 极致性能但配置复杂、编译时间长。如果模型是 LLaMA 架构或类似结构,vLLM 基本是默认选择。CPU 推理用 llama.cpp,GGUF 格式配合 Q4_K_M 量化,在消费级硬件上也能跑到可用的速度。
最后分享一个我常用的压测方法:用 100 条混合长度的请求(短中长各三分之一),并发数从 1 逐步加到 64,每个并发级别跑 5 分钟,记录 P50 和 P99 延迟以及吞吐。这样能画出完整的性能曲线,找到吞吐和延迟的平衡点。生产环境的并发上限就设在这个平衡点附近,留 20% 的余量应对突发流量。这套方法帮我避免了好几次上线后才发现性能不达标的情况。