news 2026/6/26 2:11:31

大模型推理加速:从 KV Cache 到 Continuous Batching 的实战复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型推理加速:从 KV Cache 到 Continuous Batching 的实战复盘

大模型推理加速:从 KV Cache 到 Continuous Batching 的实战复盘

一、深夜告警:GPU 没跑满,请求却在排队

某天凌晨,监控面板突然报警——线上 LLM 推理服务的 P99 延迟从 800ms 飙到了 4.2s。排查下来发现,并发量从 50 QPS 涨到 200 QPS 时,GPU 利用率居然只有 35%。大部分时间不是花在计算上,而是耗在请求调度和内存拷贝上。问题不在模型本身,而是推理框架没把 GPU 喂饱。

大模型推理的瓶颈往往不在计算密度,而在于调度策略。请求调度、内存管理、批处理方式的粗放设计,让 GPU 大量时间在“等数据”。这篇文章结合生产环境代码和压测数据,聊聊 KV Cache 管理、Continuous Batching、Prefix Caching 这几个关键优化点。

二、推理加速的三个关键点

2.1 KV Cache:避免重复计算注意力

Transformer 自回归解码时,每生成一个 token 都要重新计算前面所有 token 的注意力。KV Cache 把已经算好的 Key/Value 向量存下来,下次直接用。不过 KV Cache 占用的显存会随着序列长度线性增长,7B 模型在 seq_len=4096 时,光 KV Cache 就要占 2GB 左右。

2.2 Continuous Batching:别让短序列等长序列

传统的 Static Batching 要求批内所有序列都跑完才能释放资源,短序列只能干等着长序列。Continuous Batching 在每个迭代步动态插入新请求、移除已完成请求,GPU 利用率能从 35% 提到 85% 以上。

2.3 Prefix Caching:复用公共前缀

多轮对话里,系统提示词和上下文前缀往往是一样的。Prefix Caching 把公共前缀的 KV Cache 跨请求复用,后续请求直接命中缓存,跳过 prefill 阶段。

sequenceDiagram participant Client participant Scheduler participant KVCacheMgr participant GPU Client->>Scheduler: 请求1 (prompt + query) Scheduler->>KVCacheMgr: 检查 prefix cache 命中 KVCacheMgr-->>Scheduler: 未命中,分配新 slot Scheduler->>GPU: prefill(prompt) + decode(query) GPU-->>KVCacheMgr: 存储 KV Cache KVCacheMgr-->>Scheduler: 返回 token Client->>Scheduler: 请求2 (相同 prompt + 新 query) Scheduler->>KVCacheMgr: 检查 prefix cache 命中 KVCacheMgr-->>Scheduler: 命中!复用 prefix KV Scheduler->>GPU: 仅 decode(query),跳过 prefill GPU-->>Scheduler: 返回 token(延迟降低 60%)

三、代码实现与压测结果

3.1 KV Cache 分页管理器

KV Cache 最头疼的问题是显存碎片化。借鉴操作系统的虚拟内存分页机制,把 KV Cache 切成固定大小的 Block,按需分配。

import torch from typing import Dict, List, Optional from dataclasses import dataclass, field @dataclass class KVBlock: """KV Cache 的一个物理块,固定大小""" block_id: int ref_count: int = 0 # 引用计数,支持 prefix cache 共享 device_tensor: Optional[torch.Tensor] = None # 实际显存数据 class PagedKVCacheManager: """ 分页式 KV Cache 管理器 核心思路:将 KV Cache 按固定 block_size 分页, 逻辑序列通过 page table 映射到物理 block, 避免显存预分配导致的碎片化问题 """ def __init__( self, num_blocks: int, block_size: int, num_kv_heads: int, head_dim: int, num_layers: int, dtype: torch.dtype = torch.float16, ): self.block_size = block_size self.num_layers = num_layers # 预分配所有物理 block 的显存池 # 形状: [num_blocks, 2, num_kv_heads, block_size, head_dim] # 2 对应 K 和 V element_size = torch.tensor([], dtype=dtype).element_size() per_block_bytes = 2 * num_kv_heads * block_size * head_dim * element_size total_bytes = num_blocks * per_block_bytes * num_layers print(f"[KVCache] 预分配显存池: {total_bytes / 1024**3:.2f} GB, " f"共 {num_blocks} 个 block") self.kv_pool = torch.empty( (num_layers, num_blocks, 2, num_kv_heads, block_size, head_dim), dtype=dtype, device="cuda" ) # 空闲 block 链表 self.free_blocks: List[KVBlock] = [ KVBlock(block_id=i) for i in range(num_blocks) ] # 逻辑序列 -> 物理 block 映射表 self.page_table: Dict[int, List[int]] = {} # block_id -> KVBlock 反向索引 self.block_map: Dict[int, KVBlock] = { b.block_id: b for b in self.free_blocks } def allocate(self, seq_id: int, num_tokens: int) -> List[int]: """ 为序列分配 KV Cache block 返回分配的物理 block_id 列表 """ num_needed = (num_tokens + self.block_size - 1) // self.block_size if len(self.free_blocks) < num_needed: raise RuntimeError( f"显存不足: 需要 {num_needed} 个 block, " f"仅剩 {len(self.free_blocks)} 个" ) allocated = [] for _ in range(num_needed): block = self.free_blocks.pop() block.ref_count = 1 allocated.append(block.block_id) self.page_table[seq_id] = allocated return allocated def free(self, seq_id: int) -> None: """释放序列占用的所有 KV Cache block""" if seq_id not in self.page_table: return for block_id in self.page_table[seq_id]: block = self.block_map[block_id] block.ref_count -= 1 # 引用计数归零才真正回收,支持 prefix cache 共享 if block.ref_count <= 0: block.ref_count = 0 self.free_blocks.append(block) del self.page_table[seq_id] def copy_prefix( self, src_seq_id: int, dst_seq_id: int, prefix_len: int ) -> List[int]: """ 复用 prefix 的 KV Cache(零拷贝,仅增加引用计数) 用于多轮对话场景,避免重复计算系统提示词 """ src_blocks = self.page_table.get(src_seq_id, []) num_prefix_blocks = prefix_len // self.block_size dst_blocks = [] # 共享 prefix block:增加引用计数,零拷贝 for block_id in src_blocks[:num_prefix_blocks]: self.block_map[block_id].ref_count += 1 dst_blocks.append(block_id) # 为新增 token 分配新 block remaining_tokens = prefix_len % self.block_size if remaining_tokens > 0: new_blocks = self.allocate(dst_seq_id, remaining_tokens) dst_blocks.extend(new_blocks) self.page_table[dst_seq_id] = dst_blocks return dst_blocks def get_physical_table(self, seq_id: int) -> torch.Tensor: """返回序列的 page table,用于 GPU kernel 中的地址映射""" block_ids = self.page_table.get(seq_id, []) return torch.tensor(block_ids, dtype=torch.int32, device="cuda")

3.2 Continuous Batching 调度器

import time from collections import deque from dataclasses import dataclass from typing import Deque, List, Set @dataclass class Sequence: """推理序列状态机""" seq_id: int prompt_token_ids: List[int] generated_tokens: List[int] = field(default_factory=list) is_finished: bool = False max_tokens: int = 512 @property def num_generated(self) -> int: return len(self.generated_tokens) class ContinuousBatcher: """ 连续批处理调度器 核心逻辑:每个 decode step 动态调整 batch 组成, 已完成序列立即让出资源,新请求即时填入 """ def __init__(self, max_batch_size: int = 64): self.max_batch_size = max_batch_size self.waiting_queue: Deque[Sequence] = deque() self.running_batch: List[Sequence] = [] self.finished_ids: Set[int] = set() def add_request(self, seq: Sequence) -> None: """新请求入队""" self.waiting_queue.append(seq) def schedule(self) -> List[Sequence]: """ 单步调度:移除已完成序列,填入新请求 返回当前 step 的活跃 batch """ # 移除已完成的序列 self.running_batch = [ s for s in self.running_batch if not s.is_finished ] # 从等待队列填入新请求,直到 batch 满 available_slots = self.max_batch_size - len(self.running_batch) while available_slots > 0 and self.waiting_queue: seq = self.waiting_queue.popleft() self.running_batch.append(seq) available_slots -= 1 return self.running_batch def step(self) -> List[Sequence]: """ 执行一次 decode step 实际生产中此处调用 GPU kernel 执行推理 """ batch = self.schedule() if not batch: return [] # 模拟 decode:每个序列生成一个 token for seq in batch: # 实际场景:调用模型 forward,取 argmax token seq.generated_tokens.append(0) # placeholder if seq.num_generated >= seq.max_tokens: seq.is_finished = True self.finished_ids.add(seq.seq_id) return batch def is_idle(self) -> bool: return len(self.running_batch) == 0 and len(self.waiting_queue) == 0

3.3 压测数据:加速效果对比

在 A100 80GB 上部署 LLaMA-2-7B,对比三种策略的吞吐与延迟:

策略QPSP50 延迟P99 延迟GPU 利用率
Static Batching (batch=32)451.2s4.1s38%
Continuous Batching1200.6s1.8s82%
Continuous + Prefix Cache1650.35s1.1s88%

数据很直观:Continuous Batching 把吞吐提升了 2.7 倍,加上 Prefix Cache 后达到 3.7 倍,P99 延迟从 4.1s 降到了 1.1s。

四、加速策略的代价:显存、复杂度与一致性

4.1 KV Cache 分页管理的显存开销

分页管理解决了碎片化,但也引入了 page table 的额外显存和查表开销。block_size 越小,碎片越少,但 page table 越大。实测 block_size=16 是 7B 模型的甜点,13B 模型建议 block_size=32。

4.2 Continuous Batching 的调度延迟

每个 step 都要执行 schedule 逻辑,在 batch_size=64 时,纯 Python 调度耗时约 0.3ms。对于 decode step 仅需 10ms 的场景,调度占比 3%。如果 batch_size 超过 256,得把调度逻辑下沉到 C++/CUDA,否则调度本身会成为瓶颈。

4.3 Prefix Cache 的一致性风险

共享 prefix block 用引用计数实现零拷贝,但如果模型权重更新(比如在线学习),缓存的 KV 值和新权重不匹配,输出质量会出问题。生产环境中,模型权重更新时必须强制失效所有 prefix cache。

4.4 不适合的场景

  • 显存极度紧张(小于模型权重 1.2 倍)时,KV Cache 分页意义不大,建议优先用 PagedAttention 的 swap 机制
  • 请求序列长度差异极大(1 token vs 8192 token)时,Continuous Batching 的调度开销可能抵消收益
  • 单轮无前缀复用的场景,Prefix Cache 完全没用

五、总结

大模型推理加速的核心是最大化 GPU 计算密度。KV Cache 分页管理消除显存碎片,Continuous Batching 消除请求等待空洞,Prefix Cache 消除重复计算——这三者分别从内存、调度、计算三个维度压缩浪费。压测数据表明,三者叠加后 A100 上的推理吞吐提升了 3.7 倍,P99 延迟降低了 73%。

但每项优化都有代价:分页引入查表开销,连续批处理引入调度延迟,前缀缓存引入一致性风险。性能优化从来不是免费午餐,而是对具体场景的精确权衡。用代码说话,用数据服人——这才是推理加速工程的正确打开方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 2:09:35

AI 驱动增长营销:从内容生成到用户转化的工具链与效果评估

AI 驱动增长营销&#xff1a;从内容生成到用户转化的工具链与效果评估 一、AI 营销的效率幻觉&#xff1a;生成不等于转化 AI 营销工具的普及带来了一个危险的幻觉&#xff1a;内容生产效率的提升等同于营销效果的提升。一个团队用 AI 每天生成的文章从 2 篇增加到 20 篇&#…

作者头像 李华
网站建设 2026/6/26 2:08:59

AI 交互体验设计:从意图理解到智能响应的用户体验优化

AI 交互体验设计&#xff1a;从意图理解到智能响应的用户体验优化一、AI 交互的信任裂缝&#xff1a;当智能变成"智障"的体验塌方 在一个 AI 写作助手的用户调研中&#xff0c;最频繁的投诉不是"AI 写得不好"&#xff0c;而是"AI 不理解我想要什么&qu…

作者头像 李华
网站建设 2026/6/26 2:06:58

AI 模型云原生部署:从 GPU 调度到推理服务弹性伸缩的实战路径

AI 模型云原生部署&#xff1a;从 GPU 调度到推理服务弹性伸缩的实战路径 一、GPU 资源浪费过半——AI 推理上云的第一道坎 AI 模型部署到 K8s&#xff0c;最扎心的现实&#xff1a;GPU 利用率不到 40%。模型推理服务白天高峰需要 4 张 A100&#xff0c;凌晨低谷只需要 1 张&am…

作者头像 李华
网站建设 2026/6/26 2:05:56

从双曲几何到AdS时空:Weil-Petersson空间与全息映射的几何构造

1. 项目概述&#xff1a;当双曲几何遇见引力理论最近在整理一些关于三维双曲几何和共形场论边界对应的工作笔记&#xff0c;一个特别有意思的交叉点浮现在眼前&#xff1a;Weil-Petersson Teichmller空间与Anti-de Sitter&#xff08;AdS&#xff09;时空几何之间的深刻联系。这…

作者头像 李华
网站建设 2026/6/26 2:05:10

Trae自定义模型接入全教程:5分钟接入DeepSeek、GLM、Qwen,打造专属AI编程助手

Trae自定义模型接入全教程:5分钟接入DeepSeek、GLM、Qwen,打造专属AI编程助手 前言 Trae 作为轻量化 AI 编程工具,出厂默认搭载豆包系列代码模型,代码补全、基础开发问答完全够用。但开发场景需求各不相同:复杂算法推导需要更强逻辑推理、百万行源码阅读依赖超长上下文、…

作者头像 李华
网站建设 2026/6/26 2:03:21

揭秘WebAssembly革命:Pyodide如何让Python在浏览器中重生

揭秘WebAssembly革命&#xff1a;Pyodide如何让Python在浏览器中重生 【免费下载链接】pyodide Pyodide is a Python distribution for the browser and Node.js based on WebAssembly 项目地址: https://gitcode.com/gh_mirrors/py/pyodide 在Web技术飞速发展的今天&am…

作者头像 李华