☰
LLM推理优化技术地图:从KV Cache到量化与投机解码
2026/10/15 21:55:55 网站建设 项目流程

从第一次把 7B 模型接进内部问答系统开始,我就被一个现实问题反复折磨:模型效果没问题,但生成速度永远跟不上业务预期。单条请求还好,并发一上来,显存先爆,接着延迟飙升,最后整个推理服务像被卡住一样。那段时间我几乎把 LLM 推理优化相关的框架、论文、源码翻了个遍,踩了不少坑,也慢慢把整个优化体系串起来了。如果你也在做 LLM 推理、部署或相关工具链,这篇文章应该能帮你建立一张完整的技术地图——从自回归生成的根本瓶颈,到 KV Cache、连续批处理、量化、投机解码这些主流优化手段的原理,再到生产环境到底怎么选型、怎么调参。

1. 推理慢的根源:自回归生成与显存带宽瓶颈

很多刚开始接触 LLM 部署的人会有一个直觉:模型的算力越强,推理就应该越快。但在实际压测里你会发现,哪怕 A100 的 FP16 算力高达 312 TFLOPS,单卡跑一个 7B 模型做 decode 时,每秒能生成的 token 数依然远低于你的预期。这里面的关键不是算力不够,而是 LLM 的生成方式和传统模型完全不同。

1.1 一次只吐一个词:自回归让吞吐天然受限

LLM 的生成本质是自回归(autoregressive):每生成一个 token,都要把它拼接到已有的序列后面,然后重新送入模型,计算下一步的条件概率分布。这意味着从结构上它就无法像图像分类那样"一次前向传播出全部结果",而是被迫执行 N 次串行的前向传播来生成 N 个 token。

单看一次前向传播其实很快,但问题在于这个循环是严格串行的。第 i+1 个 token 必须等第 i 个 token 算完才能开始,中间没有任何并行空间。这就像一条只有一个人的生产线:每个工位都快得要命,但整个流水线一次只能处理一个零件。所以 LLM 推理的时间下限,被"必须串行执行的 decode 次数"死死卡住。

这里还要区分两个阶段:prefill(预填充)和 decode(解码)。prefill 是处理输入 prompt 的阶段,一次前向传播吃下整段 prompt,并行度极高;decode 是逐 token 生成的阶段,每一步只有 1 个新 token 参与计算。两者在计算特性上是完全不同的物种,后面所有优化手段,几乎都是围绕如何分别优化这两个阶段展开的。

1.2 预填充阶段与解码阶段:两种完全不同的计算形态

从计算量看,一个输入长度为 L 的请求,prefill 阶段的计算量大约是 2 × N × L(N 是模型参数量),而 decode 阶段每生成一个 token 的计算量大约是 2 × N。假设一个 7B 模型,输入 prompt 是 2048 个 token,那么 prefill 的计算量是 2 × 7e9 × 2048 ≈ 28.7 TFLOPs;而 decode 每步只有约 14 GFLOPs。两者差了上千倍。

但从访存量看,decode 阶段每一步都需要把模型的全部权重从显存搬到计算单元里。7B 的 FP16 权重是 14GB,意味着每生成一个 token,至少要读 14GB 的权重。就算 A100 有 2TB/s 的 HBM 带宽,光是搬运权重就需要约 7 毫秒。而实际单步计算本身只需要零点几毫秒。也就是说,decode 阶段的绝大多数时间都花在"搬数据"而不是"算数据"上。

prefill 阶段则相反,由于 L 很大,同样的权重被反复用了 L 次,计算密度极高,属于典型的算力受限(compute-bound)场景。这也是为什么现代推理框架普遍只对 prefill 做矩阵乘法优化,而对 decode 阶段则想尽办法减少访存量、提高 GPU 利用率。

1.3 用 Roofline 模型解释为什么"算力过剩"

Roofline 模型是理解这个问题的最好工具。它定义一个关键指标:算术强度(arithmetic intensity)= 总计算量 / 总访存量,单位是 FLOP/byte。GPU 存在一个"转折点":当算术强度低于这个点时,性能受内存带宽限制;高于这个点,性能受算力限制。

以 A100 为例,BF16 算力约 312 TFLOPS,内存带宽约 2TB/s,转折点就是 312e12 / 2e12 ≈ 156 FLOP/byte。prefill 阶段的算术强度很容易达到几千 FLOP/byte,稳坐算力受限区;而 decode 阶段,生成一个 token 需要读 14GB 权重,只做 14 GFLOPs 计算,算术强度大约只有 1 FLOP/byte,远低于转折点。这意味着在 decode 阶段,哪怕算力再提升十倍,对速度也几乎没有帮助,真正卡脖子的是显存带宽。

这个结论改变了我的整套优化思路:既然 decode 受带宽限制,那就从两个方向下手——要么减少每次读取的数据量(量化、更紧凑的 KV Cache),要么提高每个 token 的算术强度(增大 batch、投机解码让一次前向传播验证多个 token)。后面几章的所有技术,本质上都是这两个方向的具象化。

2. KV Cache 机制:为什么显存总是捉襟见肘

自回归生成还有一个隐藏代价:每次前向传播都要重新计算注意力层的历史 K 和 V。如果不做缓存,序列越长,重复计算量越大,最终会慢到无法接受。所以主流推理框架都会用一块显存专门缓存历史 token 的 Key 和 Value,这就是 KV Cache。

2.1 注意力计算里的 K 和 V 为什么要缓存

在自注意力机制中,当前 token 的 Query 需要和序列中所有历史 token 的 Key 做点积,再对 Value 加权求和。从数学上是这样,但从工程角度,历史 token 的 K、V 向量一旦算出来,在后续每一步都是不变的——因为它们是历史 token 的投影结果,不会随当前 token 变化。既然不变,就没必要每步重算,直接存下来复用就行。

KV Cache 就是干这个的。它的存在让 decode 阶段只需要计算当前 token 的 Q、K、V,然后用当前 Q 去和缓存里的历史 K、V 做注意力运算。这带来两个好处:一是省掉了历史 token 的全部重复计算,二是每步需要读取的数据量从"全部序列"降为"全部序列的 KV"。

但天下没有免费的午餐。KV Cache 把计算压力转嫁成了显存压力,而且这个压力会随着 batch 大小、序列长度、层数、注意力头数线性增长。在长对话、长文档这类场景里,KV Cache 的显存占用往往会超过模型权重本身,成为显存的第一大消耗者。

2.2 一张表算清楚 KV Cache 占多少显存

KV Cache 的显存占用可以用一个公式估算:

显存(字节)= 2 × 层数 × KV头数 × Head维度 × 序列长度 × Batch大小 × 字节数

为什么前面有个 2?因为 K 和 V 各一份。下面我用一个 7B 模型(32 层、8 个 KV 头、Head 维度 128)、FP16(每元素 2 字节)来算不同场景下的占用:

场景序列长度BatchKV Cache 显存
短对话102412×32×8×128×1024×1×2 = 1GB
中等长度409682×32×8×128×4096×8×2 = 4GB
长文档+RAG819282×32×8×128×8192×8×2 = 8GB
极限长上下文32768162×32×8×128×32768×16×2 = 32GB

注意,模型 FP16 权重本身约 14GB。也就是说,在 batch=8、序列长度 4096 的场景下,KV Cache 占用 4GB,虽然还没超过权重,但已经不容忽视;到了 batch=16、上下文 32K 时,KV Cache 高达 32GB,是权重的两倍还多。如果你用的是 A100 80GB,光 KV Cache 就占了近一半。这还是 8 个 KV 头的情况,如果是原始 MHA 结构的 32 个头,数字要翻四倍,直接爆掉。

这就是为什么长上下文场景下,显存规划的第一优先级不是模型权重,而是 KV Cache。我见过不少团队在本地部署长文档 RAG 服务时,模型本身加载没问题,一跑长 prompt 就 OOM,基本都是没算清楚这笔账。

2.3 MHA、GQA、MQA:从注意力头结构省显存

既然 KV Cache 的显存压力和 KV 头数线性相关,那么最直接的优化方向就是把 KV 头数降下来。这里有三条路线:

  • MHA(Multi-Head Attention):每个注意力头都有独立的 K、V。效果最好,显存开销最大。
  • MQA(Multi-Query Attention):所有注意力头共享一组 K、V,KV Cache 直接缩小到原来的 1/头数,但质量损失有时明显。
  • GQA(Grouped-Query Attention):把所有头分成若干组,组内共享 K、V,效果和显存取折中。比如 32 个 Query 头配 8 个 KV 头,KV Cache 就是 MHA 的 1/4。

现在的开源模型基本都转向了 GQA 或 MQA 结构,LLaMA 2/3、Mistral 等都是这个路线。作为使用者,这个结构选择是模型训练时定的,你改变不了,但在买卡、做显存规划时必须把它算进去:同样参数量下,GQA 模型能支撑的并发和上下文长度,就是比 MHA 模型高出一截。

不过 KV Cache 的优化不止停留在模型结构层面。后面章节会讲的 PagedAttention 解决的是 KV Cache 的碎片化问题,KV Cache 量化则是在精度可接受的前提下把它的字节数再砍一刀,这些都是在工程侧继续压低显存压力的手段。

3. 批处理与显存管理:连续批处理和 PagedAttention 的思路

在面试或者技术评审里,我经常被问到"LLM 推理为什么需要专门的框架"。最核心的答案其实就两个:动态批处理和显存管理。传统深度学习推理框架在这两件事上的表现,距离"好用"差得很远。

3.1 静态批处理为什么浪费

传统推理框架处理请求的方式是静态批处理:把多个请求凑成一整个 batch,然后整个 batch 一起前向传播,等全部请求生成完毕后,再释放整个 batch 的资源。如果你的服务是图像分类这类"一次前向传播出结果"的场景,这套机制没有任何问题。

但换到 LLM 场景,情况完全不同。不同请求的 prompt 长度不一样,生成的目标长度也不一样。有的请求生成 50 个 token 就停了,有的要生成 500 个。静态批处理只能按最长的那个请求统一推进,短的请求生成完了也只能在 batch 里干等着。更糟糕的是,新来的请求必须等当前这个 batch 彻底清空才能进来,哪怕 GPU 此刻有一半的计算单元在空转。

这会带来两个直接后果:一是 batch 内的计算浪费,短请求占着位置不干活;二是 batch 间的调度死板,吞吐峰值上不去。在 LLM 这种 decode 阶段本就是带宽受限的场景里,这种浪费等于把本就不富裕的算术强度进一步摊薄。

3.2 连续批处理的工作方式

连续批处理(Continuous Batching)的核心思路,是把"整批进入、整批退出"改成"逐请求进入、逐请求退出"。框架在每步 decode 时都会问一遍:batch 里有没有请求已经生成结束了?有的话就把它踢出去,释放它占用的 KV Cache 显存和计算槽位,同时从等待队列里拉一个或多个新请求进来,参与下一轮前向传播。

这样做的好处非常直观:GPU 上永远跑着尽可能多的活跃请求,显存和带宽的利用率被拉满。即使某个请求只生成 30 个 token 就结束,也只占用 30 步的资源,不会拖累别人。在实际部署中,连续批处理能让单卡吞吐提升数倍到十倍不等,具体取决于请求的长度分布——请求越长、长度差异越大,收益越明显。

这套机制原本是 Orca 论文提出来的,现在已经是 vLLM、TensorRT-LLM、SGLang 这些主流框架的标配能力。但要注意一点:连续批处理会显著提高单卡上的并发请求数,这要求调度器能精确管理每个请求的显存占用,于是 PagedAttention 就应运而生了。

3.3 PagedAttention:把显存当虚拟内存页来管理

在 PagedAttention 出现之前,KV Cache 是按"请求"为单位连续分配的。一个请求从开始到结束,它在显存里占用的是一整块连续空间。问题是这个空间必须按最大可能长度预留——比如你设定最大序列长度 8192,那么每个请求从一开始就要占满 8192 个 token 的 KV Cache 空间,哪怕它实际只用了 500。

这种分配方式带来两个致命问题:一是内部碎片,预留的空间利用率经常不到一半;二是由于显存里塞满了预留空间,外部碎片也在不断积累,最终导致明明显存还有不少剩余,却找不到一块足够大的连续空间给新请求用。

PagedAttention 借鉴的是操作系统虚拟内存的思路:把 KV Cache 切分成固定大小的块(block),每个请求的 KV Cache 由若干块组成,这些块在物理显存上可以是不连续的。用一个块表记录逻辑块到物理块的映射就行。只有当请求真正产生新的 KV 时,才分配新的块;请求结束,块立即回收。

这个设计的精妙之处在于,它几乎消灭了显存浪费:不再需要按最大长度预分配,块粒度足够小,内部碎片可以忽略。同时它还顺带支持了类似 copy-on-write 的机制——比如多个采样结果共享同一段前缀的 KV Cache 时,只需要复制块表而不复制实际显存数据。实测中 PagedAttention 能让显存利用率提升到接近原始方案的 90% 以上,这直接决定了单卡能承载的并发请求上限。

3.4 调度时机与流量控制

有了连续批处理和 PagedAttention,框架的吞吐上限大幅提高了,但随之而来的是一类新问题:调度策略和流量控制。

首先是调度时机。每个请求的最优并发接受量,要看显存是否有足够的空闲块给新请求分配 KV Cache。调度器需要在每步 decode 之后检查显存水位,决定是否从等待队列拉新请求。拉得太激进,可能因为显存不足导致前向传播失败;拉得太慢,GPU 利用率又不够。主流框架都允许你配置max_num_seqs和gpu_memory_utilization这两个参数来调节水位,前者控制最大并发序列数,后者控制显存使用率上限。

其次是队头阻塞问题。连续批处理中,如果一个请求突然生成了极长的输出,它会持续占用批量槽位,导致后面短请求的等待时间被拉长。实际部署时,我一般会把请求按优先级分队列,或者给单请求设置max_tokens上限,避免极端情况拖垮整体延迟。

这里还有一个很多人忽略的点:连续批处理提升的是吞吐(throughput),而不是单请求延迟(latency)。如果你接的是实时对话场景,需要的是单 token 延迟足够低,这时候反而要控制并发量,给每个请求留足显存和带宽。吞吐和延迟的权衡,是 LLM 推理调优里永远绕不开的主题。

4. 量化压缩:精度与吞吐的权衡艺术

如果说 KV Cache 和批处理解决的是"显存和并发"问题,那么量化解决的就是"带宽和容量"的底层瓶颈。这也是为什么量化几乎是所有 LLM 推理优化文章里必谈的话题——它直接作用于我们第一章说的那个算术强度公式的分母:每次读取的字节数。

4.1 为什么要量化:带宽瓶颈下的必然选择

回到第一张图的结论:decode 阶段每生成一个 token,都需要把模型权重完整读一遍。7B 模型 FP16 权重是 14GB,如果量化到 4bit,权重直接缩到 3.5GB。这意味着每一轮 decode 的访存量下降到原来的四分之一,在带宽受限的前提下,理论生成速度也能相应提升接近四倍。

你可以把量化理解成"用更低的数字精度来存储和计算"。FP16 每个数占 2 字节,INT8 占 1 字节,INT4 只占 0.5 字节。位数越低,模型文件越小,加载和推理时读写的数据量越少。对 LLM 而言,权重中的冗余信息很多,用低精度表示并不会明显损害生成质量——这是量化的理论基础。

不过量化不是免费午餐。位数越低,数值表示的精度越低,模型输出质量下降的风险越大。不同的量化方法和校准策略,对最终效果的损伤程度差别非常大。这也是为什么量化领域会有那么多不同的方案,它们本质上是精度、速度、显存三者之间的不同权衡点。

4.2 主流量化方案与实测对比

目前主流方案大概分两类:训练后量化(PTQ)和量化感知训练(QAT)。LLM 领域由于训练成本极高,绝大多数实践都选 PTQ,也就是拿一个训练好的模型,用少量校准数据计算缩放因子,然后直接转换。以下是几个常见方案的特点:

方案位宽量化对象代表实现精度损失适用场景
GPTQ4bit权重AutoGPTQ、vLLM较低通用部署,对速度要求高的服务
AWQ4bit权重vLLM、TensorRT-LLM较低激活值分布敏感的模型
SmoothQuant8bit权重+激活TensorRT-LLM低需要高吞吐且不想太大精度损失
GGUF Q4_K_M4bit权重llama.cpp、Ollama中等本地部署、CPU/边缘设备

GPTQ 和 AWQ 是目前服务端部署最常见的两个 4bit 权重量化方案。它们的核心区别在于处理"离群值"(outlier)的思路:GPTQ 用近似算法逐层寻找最小化量化误差的权重矩阵,AWQ 则通过观察激活值分布,保护对模型输出影响最大的那部分权重通道。实测中两者效果接近,但 AWQ 在激活值分布存在明显尖峰的模型上往往更稳健。

SmoothQuant 的思路值得一提:它不直接砍权重的位宽,而是把激活值里的离群值"平滑"转移到权重中,让激活值分布更均匀,从而实现对权重和激活同时做 INT8 量化。因为激活值量化往往比权重量化更容易掉精度,SmoothQuant 在这类场景的价值在于提供了一条"不那么痛"的路径。

4.3 量化容易踩的坑

量化真正落地时,坑远比想象中多。我按踩过的频率排序,说几个最典型的。

第一,只看权重文件大小不看实际显存。很多人以为模型量化后显存占用就变成 3.5GB 了,其实推理时还要额外算上激活值、KV Cache、临时计算缓冲。另外,有些框架在量化后仍然以 FP16 精度做部分计算,实际显存并没有你想象中省那么多。

第二,KV Cache 不量化,长上下文时依然会爆显存。很多团队对一个 70B 模型做了 AWQ 4bit 量化,权重确实从 140GB 降到了 35GB,但序列一长,KV Cache 照样把显存吃穿。这时候需要考虑像 KV Cache INT8 量化这类技术,它能再砍掉 KV Cache 的一半字节数。当前主流框架基本都支持对 KV Cache 做量化,但精度损失在不同模型上表现差异很大,建议上线前使用业务真实数据单独验证。

第三,校准集和业务数据分布不一致,导致量化后某些输出质量崩坏。GPTQ、AWQ 都需要选校准数据。有些人图省事直接用论文里的公开数据集,结果在中文、代码、医疗等特定领域上效果明显退化。这时的解法是准备几百条贴近真实业务场景的样本,重新校准一遍,往往能救回来不少质量。

第四,量化后的评测不能只看困惑度(perplexity)。困惑度是一个整体指标,对局部任务的说谎、格式、逻辑错误并不敏感。量化前后应该跑一套和你业务强相关的评测集,比如开放域问答的准确率、代码生成的通过率,而不是只看那个全局的 PPL。

4.4 从模型结构侧理解量化:激活值为什么比权重难搞

权重量化之所以能做到 4bit 仍维持不错的效果,是因为权重矩阵里绝大多数元素的值域很集中,少数异常大的离群值只要单独保护起来,整体误差就可控。但激活值的问题在于,它的值域是每层动态变化的,且随输入不同而波动,无法像权重一样提前离线分析。

这就解释了为什么"权重+激活"一起量化的方案难度远高于纯权重量化。你可以用一个类比来理解:权重量化像给一张静态照片压缩成低分辨率图,细节损失有限;激活量化则像给一段每个镜头亮度都在变的视频做压缩,很容易过曝或欠曝。

所以如果你的场景对延迟要求极高(比如流式对话),希望把权重和激活都压到 8bit 以同时减少访存和计算量,建议走 SmoothQuant 这类经过验证的方案,而不是自己拍脑袋直接 INT8,效果大概率会让你失望。

5. 投机解码与并行策略:从算法层面省步骤

前面说的 KV Cache、批处理、量化,都是在优化"每一步访存和执行的成本"。但自回归生成还有另一个层面的优化空间:能不能少做几步?投机解码(Speculative Decoding)和并行解码就是从这个角度切入的,思路非常巧妙。

5.1 投机解码:小模型写草稿,大模型批改

投机解码的核心思想很反直觉:用一个又快又小的小模型(draft model)先快速生成若干候选 token,然后让大模型一次前向传播验证这些候选是否正确。如果小模型的候选大部分被大模型接受了,那么一次前向传播就"赚到"了多个 token 的生成量,总体延迟和吞吐都得到改善。

这里的关键在于:验证是非自回归的。大模型可以在一次前向传播里,同时计算多个候选位置各自的下一个 token 分布,不需要逐个串行等待。这跟老师批改学生作业很像——学生(小模型)先把答案写出来,老师(大模型)一次性批改所有答案,而不是老师每写一个字就叫一次学生。

接受率是投机解码的核心指标。如果小模型和大模型行为差异大,候选 token 经常被拒绝,那么每次验证只能接受一两个 token,甚至需要重新生成,反而比直接跑大模型更慢。论文和工业界的实测表明,在用一个小一号的同族模型做 draft 时,接受率能到 0.7-0.8,端到端加速通常有 2-3 倍。但选 draft 模型不能只图小,和主模型的分布相似度比模型大小更重要。

5.2 多 token 并行:Medusa 和独立解码头

投机解码需要一个单独的小模型,这不是所有团队都愿意接受的额外负担。Medusa 提供了另一条思路:在原始模型之上添加多个平行的解码头,每个头各自负责预测"当前位置之后第 n 个位置的 token"。训练时用原始模型生成的数据微调这些解码头,推理时同样采用"草稿+验证"的模式。

这个方案的工程好处是不需要维护两个独立的模型,所有解码头都挂在同一个骨干网络上。缺点是解码头需要针对每个模型单独训练,属于"一次工作,多次收益"的类型。目前 vLLM、SGLang 等框架都已经对 Medusa 提供了一定支持,实测在短文本生成场景里能获得 2 倍左右的加速。

另外还有一种思路是并行采样(parallel sampling),在某些解码策略下,同一 prompt 的一次前向传播可以产生多个候选分支,然后挑选最优分支继续推。它和投机解码的数学目标不完全一样,但同样利用了"一次前向传播计算多个位置的分布"这个能力,适合对延迟极其敏感的搜索或规划类任务。

5.3 张量并行与流水线并行:单卡放不下的解法

当模型大到单卡放不下时,并行策略是绕不开的话题。张量并行(Tensor Parallelism)是把一个 Transformer 层的矩阵运算切分到多张卡上,每张卡持有权重的一部分,计算时通过 all-reduce 通信同步结果。这适合单节点多卡,因为通信量非常大,跨节点用会直接被网络带宽拖垮。

流水线并行(Pipeline Parallelism)则是按层切分,不同的层放在不同的卡上,数据像流水线一样从前到后流过所有卡。它的通信量比张量并行小得多,更利于跨节点部署,但会引入流水线气泡(bubble),也就是某些卡在等待上游数据时处于空闲状态。

实际生产里,7B 到 13B 的模型单卡基本能放下,主要用不到模型并行;70B 以上的模型通常会采用张量并行 + 流水线并行的组合,比如 8 卡张量并行、4 组流水线并行。这里有一个反直觉的经验:一张 80GB 的卡放得下 70B 模型权重时,很多人会觉得不需要并行,但 decode 阶段单卡带宽有限,多卡张量并行可以把权重访存分摊到多张卡的带宽上,单个 token 的生成延迟能随之下降。所以并行不只是为了"放得下",也是为了"跑得快"。

5.4 收益评估与适用场景

投机解码不是所有场景都能稳定拿收益。如果业务主要是短文本生成(比如翻译、摘要),每段只有几十个 token,投机解码的前期开销占比太高,收益很容易被摊薄;而长文本生成、代码补全这类场景,序列越长,省下的步数越多,收益越明显。

我个人的经验是:上线前先做一个 30 分钟的小实验,用业务真实流量测一下有无投机解码的 TPOT(单个 token 的生成时间)和吞吐。如果提升不足 30%,那就不如把精力放在量化和批处理参数调优上。另外,投机解码会增加显存占用(小模型或 Medusa 头、验证器的 KV Cache),所以显存紧张的环境里要额外注意。

6. 落地选型与实战经验

原理讲得再多,最后都要落到"生产环境里到底该用哪个框架、怎么配置参数"这个问题上。作为一个踩过不少坑的人,我把自己的选型逻辑和调参经验整理如下。

6.1 主流推理框架对比

目前开源社区里最常用的几个推理框架,各有各的擅长场景:

框架核心优势适合场景注意事项
vLLMPagedAttention,吞吐高,社区活跃在线服务、高并发对某些算子的覆盖不如专用引擎细
TensorRT-LLMNVIDIA 官方优化,算子极致固定卡型、追求极致性能编译时间长,模型兼容性需要维护
SGLangRadixAttention,前缀复用强RAG、多轮对话等共享前缀场景相对年轻,生态还在追赶
llama.cpp / OllamaCPU 和消费级 GPU 友好本地部署、边缘设备吞吐上限低于服务端框架

vLLM 是绝大多数团队的第一选择,因为它的 PagedAttention 把显存效率做得很高,同时支持连续批处理和多种量化格式,部署成本最低。如果你的显存固定、卡型固定、业务量也稳定,可以考虑 TensorRT-LLM,它会在算子层面再做一轮针对性的极致优化,吞吐还能再上一个台阶。SGLang 在前缀复用上有独到优势,如果你的服务大量请求共享同一个系统提示词或长文档前缀,它的收益会非常直观。

6.2 关键参数怎么定

配置推理服务时,我一般按这个顺序来调参数:

  • gpu_memory_utilization:显存利用率上限,vLLM 默认 0.9。建议从 0.85 起步,留出一点余量给 CUDA context 和其他开销,稳定后再往上加。
  • max_num_seqs:最大并发序列数。这个值不是越大越好,它受限于 KV Cache 的可用显存。可以先设一个保守值,观察显存剩余和队列积压情况,再逐步上调。
  • max_model_len:模型支持的最大序列长度,注意包括输入输出总和。设得过大同样会推高 KV Cache 预留空间。
  • max_tokens:单请求输出上限,建议按业务真实场景设,不要给用户无限生成的自由空间。它直接影响极端情况下的服务稳定性。
  • enable_prefix_caching(vLLM 中的相关开关):如果你的请求有大量重复前缀,开启后能重用 KV Cache,显著降低首 token 延迟。

调参的总体原则是:先保证不 OOM,再追求吞吐。我见过太多人一上来就把显存利用率拉到 0.95,结果请求一多就爆显存,进程直接崩掉,这比慢一点可怕得多。

6.3 我实际踩过的坑

第一个坑:量化模型和框架版本不匹配。曾经有一版 AWQ 量化模型在某框架的早版本上推理结果偶尔出现乱码,排查了很久才发现是量化格式兼容性问题。现在我的流程是固定框架版本,并且每次升级框架都要回归跑一遍业务评测集,不能只看能不能加载。

第二个坑:长尾请求导致的延迟抖动。连续批处理下,一个特别长的请求会拖长同批次里所有请求的完成时间。后来我在网关层把请求按预期输出长度分桶,短请求走低延迟通道,长请求走高吞吐通道,整体 P99 延迟立刻好了很多。

第三个坑:预热不足。很多推理框架在第一次收到请求时才做算子选择和图优化,导致冷启动时首 token 延迟特别高。上线前一定要发一批预热请求,把 CUDA kernel、显存分配都触发一遍。

第四个坑:多副本部署时的负载均衡策略。如果把长请求和短请求混在同一个负载均衡队列里,会出现某个副本被长请求占满、其他副本空闲的情况。最好在负载均衡层根据请求特征做哈希或加权,避免这类"热副本"现象。

6.4 性能指标怎么衡量与调优顺序

衡量 LLM 推理性能,我建议至少关注四个指标:

  • TTFT(Time to First Token):首 token 延迟,直接影响用户第一感知。主要受 prefill 速度和调度效率影响。
  • TPOT(Time Per Output Token):平均每个输出 token 的生成耗时,决定输出"流畅度"。
  • 吞吐量(tokens/s):整个服务每秒生成的 token 总数。
  • 并发能力:显存允许的最大并发请求数。

调优顺序上,我的建议是先算显存账,确保 KV Cache 规划合理;然后调批处理参数,把吞吐拉起来;再考虑量化,把带宽瓶颈松一松;最后才是投机解码这类算法级优化。原因很简单:显存是地基,批处理是主干,量化是放大器,投机解码是锦上添花。顺序反了,容易在某个环节反复横跳,事倍功半。

最后分享一个我个人的小习惯:每次做推理优化,我都会先写一个几十行的压测脚本,固定住 prompt 模板、生成长度、并发数这些变量,把 TTFT、TPOT、吞吐三个数打出来留档。因为这些数字受模型版本、框架版本、硬件状态影响太大,没有基线数据,任何优化都说不清到底有没有效果。有了基线,每次改动带来的收益或回退都能一目了然,这才是推理优化能持续迭代下去的基础。

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

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

立即咨询