大模型推理加速实战:从 KV Cache 到连续批处理的全链路优化
大模型推理加速实战:从 KV Cache 到连续批处理的全链路优化

一、Token 延迟与吞吐困境:大模型推理的性能瓶颈全景
大模型推理的性能瓶颈,远不止"GPU 算力不够"这么简单。在生产环境中部署 LLM 服务时,核心矛盾集中在两个维度:首 Token 延迟(TTFT) 和 生成吞吐量(Tokens/s)。一个 70B 参数模型在单卡 A100 上做自回归解码,如果不做任何优化,每秒只能产出约 15-20 个 Token,而并发 10 路请求时,延迟会线性膨胀到不可接受的程度。
根本原因在于自回归解码的固有特征:每一步生成都需要将前面所有 Token 的 Key/Value 向量重新参与注意力计算。这意味着序列长度每增加 1,计算量就多一轮完整的 Attention,而 GPU 的计算单元在 Decode 阶段大量时间处于访存瓶颈(Memory-Bound),算力利用率常常不到 10%。
更严峻的是,当多路请求并发时,不同请求的序列长度差异巨大。如果采用朴素批处理,短序列必须等待长序列完成才能释放资源,导致 GPU 利用率进一步坍塌。这就是大模型推理中经典的"长度填充浪费"问题。
二、KV Cache 复用与连续批调度:推理引擎的核心加速机制
要突破上述瓶颈,推理引擎层面有两项关键优化:KV Cache 和 Continuous Batching。二者的协同工作构成了现代 LLM 推理引擎的性能基石。
sequenceDiagram
participant Client as 客户端请求
participant Scheduler as 连续批调度器
participant KVPool as KV Cache 池
participant GPU as GPU 计算单元
Client->>Scheduler: 请求A (prefill, seq_len=128)
Client->>Scheduler: 请求B (decode, seq_len=512)
Scheduler->>Scheduler: 组装微批次 [A_prefill, B_decode]
Scheduler->>KVPool: 分配 A 的 KV 槽位
Scheduler->>GPU: 提交 Attention 计算
GPU-->>KVPool: 写入 A 的 KV 向量
GPU-->>Scheduler: 返回 A 的首个 Token, B 的下一个 Token
Scheduler->>Scheduler: 检测 B 已完成 → 立即释放 B 的 KV 槽位
Client->>Scheduler: 请求C (prefill, seq_len=64)
Scheduler->>Scheduler: 新微批次 [A_decode, C_prefill] (复用 B 释放的槽位)
Scheduler->>GPU: 提交计算
2.1 KV Cache:用空间换时间的核心策略
KV Cache 的原理并不复杂:在自回归解码的每一步,只需计算当前 Token 的 Query,与缓存中所有历史 Key/Value 做 Attention 即可。这避免了重复计算前面所有层的 KV 投影,将每步的计算复杂度从 O(n*d) 降到 O(d)(d 为隐藏维度),但代价是显存占用随序列长度线性增长。
以 LLaMA-2-70B 为例,FP16 精度下每个 Token 的 KV Cache 占用约 2.5MB(70 层 x 2 x 8192 维度 x 2 字节)。一个 2048 长度的序列就需要约 5GB 显存仅用于 KV Cache,这还不包括模型权重本身。因此,KV Cache 的显存管理直接决定了最大并发数。
2.2 Continuous Batching:消灭填充浪费
传统的 Static Batching 要求同一批次内所有请求同时开始、同时结束,短请求被迫填充到最长请求的长度。Continuous Batching(也称 In-Flight Batching)打破了这一限制:调度器在每个解码步动态决定哪些请求参与计算,已完成的请求立即被移出批次,新请求随时插入。
这种"迭代级调度"使得 GPU 在每个步都能保持满载,吞吐量提升可达 2-4 倍。vLLM 的实测数据显示,在并发 64 路请求的场景下,Continuous Batching 相比 Static Batching 的吞吐提升约 3.2 倍。
三、生产级推理引擎配置与性能调优代码
以下代码展示了基于 vLLM 的生产级推理服务配置,重点关注 KV Cache 管理和批处理参数调优:
from vllm import LLM, SamplingParams
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.async_llm_engine import AsyncLLMEngine
import asyncio
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class InferenceConfig:
"""推理引擎核心配置——每个参数都直接影响吞吐与延迟的平衡点"""
model: str = "meta-llama/Llama-2-70b-chat-hf"
# GPU 利用率阈值:0.95 意味着预留 5% 显存给临时张量,
# 过高会导致 OOM,过低则浪费算力
gpu_memory_utilization: float = 0.95
# 最大序列长度:必须与业务最长输入对齐,
# 过大会浪费 KV Cache 预分配空间
max_model_len: int = 4096
# 最大并发批次数:受限于 KV Cache 显存池大小
max_num_seqs: int = 256
# PagedAttention 块大小:4 是 vLLM 默认值,
# 增大可减少页表开销但增加碎片
block_size: int = 4
# 交换空间:当 KV Cache 超出 GPU 显存时,
# 溢出到 CPU 内存,避免直接 OOM
swap_space: int = 16 # GB
async def create_engine(config: InferenceConfig) -> AsyncLLMEngine:
"""构建异步推理引擎,关键在于显存预算的精确分配"""
engine_args = AsyncEngineArgs(
model=config.model,
gpu_memory_utilization=config.gpu_memory_utilization,
max_model_len=config.max_model_len,
max_num_seqs=config.max_num_seqs,
block_size=config.block_size,
swap_space=config.swap_space,
# 启用前缀缓存:对共享 system prompt 的多轮对话场景,
# 可复用公共前缀的 KV Cache,节省 30-50% 重复计算
enable_prefix_caching=True,
# 分块预填充:将长序列的 prefill 拆分为多个小块,
# 与 decode 请求混合调度,避免长 prefill 阻塞短请求
enable_chunked_prefill=True,
)
return AsyncLLMEngine.from_engine_args(engine_args)
async def batch_inference(
engine: AsyncLLMEngine,
prompts: List[str],
max_tokens: int = 512,
temperature: float = 0.7,
) -> List[str]:
"""批量推理入口——通过异步流式处理实现高吞吐"""
sampling_params = SamplingParams(
max_tokens=max_tokens,
temperature=temperature,
# 重复惩罚:防止生成退化,但过高会损害多样性
repetition_penalty=1.05,
)
results = []
request_ids = []
# 异步提交所有请求,引擎内部自动做 Continuous Batching
for i, prompt in enumerate(prompts):
request_id = f"req-{i}"
request_ids.append(request_id)
engine.generate(prompt, sampling_params, request_id)
# 流式收集结果
finished_set = set()
while len(finished_set) < len(prompts):
# 非阻塞轮询引擎输出
result = await engine.generate(None, None, None)
if result and result.finished:
finished_set.add(result.request_id)
results.append(result.outputs[0].text)
return results
# 基准测试:量化验证配置参数对吞吐的影响
def benchmark_throughput(config: InferenceConfig, num_requests: int = 100):
"""
吞吐基准测试——用数据说话,不靠猜测调参。
固定输入长度 1024 Token,输出 256 Token,
测量不同并发度下的 Tokens/s 和 TTFT P99。
"""
llm = LLM(
model=config.model,
gpu_memory_utilization=config.gpu_memory_utilization,
max_model_len=config.max_model_len,
max_num_seqs=config.max_num_seqs,
)
# 生成固定长度的测试 prompt
test_prompts = ["Benchmark test prompt. " * 64] * num_requests
sampling_params = SamplingParams(max_tokens=256, temperature=0.0)
import time
start = time.perf_counter()
outputs = llm.generate(test_prompts, sampling_params)
elapsed = time.perf_counter() - start
total_tokens = sum(len(o.outputs[0].token_ids) for o in outputs)
throughput = total_tokens / elapsed
print(f"Throughput: {throughput:.1f} tokens/s, "
f"Elapsed: {elapsed:.2f}s, "
f"Total tokens: {total_tokens}")
四、显存墙与延迟抖动:推理加速的架构权衡
任何优化都不是免费的。KV Cache 和 Continuous Batching 在提升吞吐的同时,引入了以下必须正视的 Trade-off:
显存与并发的不可能三角:模型参数、KV Cache、激活值三者竞争同一块 GPU 显存。增大 gpu_memory_utilization 可以容纳更多并发请求,但一旦出现突发长序列,KV Cache 溢出触发 Swap 到 CPU 内存,延迟会从毫秒级飙升到秒级。生产环境中,P99 延迟比平均吞吐更重要,因此必须预留足够的显存余量。
PagedAttention 的碎片开销:vLLM 采用类似操作系统虚拟内存的 PagedAttention 机制管理 KV Cache,解决了显存碎片问题,但引入了间接寻址开销。在极短序列(< 32 Token)场景下,页表管理的开销占比反而超过计算本身,此时 PagedAttention 的收益为负。
Continuous Batching 的调度延迟:每个解码步都需要调度器重新评估批次组成,这个决策过程本身消耗约 0.1-0.3ms。在极低延迟要求(TTFT < 50ms)的场景下,这个开销不可忽略。对于实时对话类应用,可能需要限制最大批次大小来换取更稳定的延迟。
前缀缓存的失效风险:Prefix Caching 依赖请求间共享相同的前缀(如 System Prompt)。一旦 System Prompt 发生变化,缓存全部失效。在多租户场景下,不同租户的 Prompt 差异大,前缀缓存命中率可能低于 20%,此时开启该功能反而增加管理开销。
五、总结
大模型推理加速的核心思路可以归纳为三个层次:减少重复计算(KV Cache)、消除空闲等待(Continuous Batching)、精细化资源管理(PagedAttention + 前缀缓存)。三者缺一不可,且必须根据实际负载特征进行参数调优。
落地路线建议:
- 基线测量先行:部署前先用基准测试工具测量裸模型的 TTFT 和 Tokens/s,建立性能基线。
- 显存预算规划:根据模型大小和目标并发数,反推 KV Cache 可用显存,设置合理的
gpu_memory_utilization(建议 0.90-0.95)。 - 渐进式调参:从默认配置开始,逐步调整
max_num_seqs、block_size、swap_space,每次只改一个参数,用数据验证效果。 - 监控 P99 延迟:生产环境中,P99 延迟比平均吞吐更能反映真实用户体验,务必建立延迟分位数的实时监控。
- 负载特征匹配:短序列为主的服务侧重降低调度开销,长序列为主的服务侧重 KV Cache 显存管理,混合负载则需要分块预填充。
更多推荐




所有评论(0)