
vLLM Automatic Prefix CachingKV Cache 前缀复用机制的原理、配置与实战【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllmAutomatic Prefix CachingAPC是 vLLM 中用于消除重复 prompt 计算的核心优化引擎会缓存已处理请求的 KV cache 块当新请求与已有请求共享相同前缀时直接复用这些块跳过共享部分的 prefill 计算。本文基于仓库中的功能文档docs/features/automatic_prefix_caching.md与设计文档docs/design/prefix_caching.md展开并深入到vllm/v1/core的 KV cache 管理器源码讲清 APC 的开启方式、适用负载、哈希机制、数据结构、分配与逐出流程以及它的收益边界。APC 解决什么问题在 LLM 推理中KV cache 的 prefill 计算开销与 prompt 长度成正比。现实中大量请求天然带有共享前缀长文档查询用户对同一份长文档如软件手册、年报反复提出不同问题。若没有 APC每次请求都要把长文档重新 prefill 一遍启用 APC 后vLLM 只需处理该长文档一次后续所有请求通过复用其 KV cache 避免重算从而获得更高吞吐和更低延迟。多轮对话用户在同一会话中多轮聊天每一轮的 prompt 都包含完整的对话历史。APC 让 vLLM 把历史轮次的处理结果复用到后续所有轮次同样显著降低延迟、提升吞吐。这两个场景在功能文档中被明确列为 APC 收益最大的典型负载。反之当请求之间几乎没有公共前缀或时间主要花在解码decode阶段时APC 的收益会非常有限——这一点在“收益边界”一节展开。开启 APC 与核心配置参数离线推理enable_prefix_caching在 vLLM 引擎中设置enable_prefix_cachingTrue即可开启 APC。仓库提供了完整的演示脚本 automatic_prefix_caching_offline.py其核心逻辑如下from vllm import LLM, SamplingParams # 使用一段长 Markdown 表格作为共享 prompt 前缀 llm LLM(modellmsys/longchat-13b-16k, enable_prefix_cachingTrue) sampling_params SamplingParams(temperature0, max_tokens100) # 第一次查询完整 prefill LONG_PROMPT get_generation_time( llm, sampling_params, LONG_PROMPT Question: what is the age of John Doe? ..., ) # 第二次查询共享 LONG_PROMPT 前缀KV cache 直接命中耗时明显更短 get_generation_time( llm, sampling_params, LONG_PROMPT Question: what is the age of Zack Blue? ..., )该脚本用一张约 30 行的随机 Markdown 表格充当共享前缀先后提出两个仅问题不同的请求并打印两次的生成耗时——第二次由于跳过了LONG_PROMPT的重复 prefill耗时会显著低于第一次。直接运行即可复现python examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py从源码看enable_prefix_caching是CacheConfig的字段见 cache.pyenable_prefix_caching: bool True Whether to enable prefix caching.在当前仓库中其默认值为True即 vLLM 默认就开启前缀缓存如需显式控制可在离线LLM(...)参数或vllm serve的缓存相关参数中传入。在线服务哈希算法--prefix-caching-hash-algoAPC 以“块哈希”作为缓存键因此哈希算法的选择直接影响缓存的确定性、跨实例复用能力与安全性。设计文档指出早期版本的哈希键不保证无碰撞自 v0.11 起默认算法改为sha256消除了碰撞风险。对vllm serve可通过--prefix-caching-hash-algo控制四个取值及取舍如下该参数在 arg_utils.py 中注册默认值定义于 cache.py算法序列化方式特点与适用场景sha256默认Pythonpickle最安全的选择避免潜在哈希碰撞但哈希值可能在不同 Python / vLLM 版本间不可复现sha256_cbor规范化 CBORcbor2可复现、跨语言兼容的哈希推荐用于跨环境确定性缓存xxhashpickle xxHash(128-bit)更快的非密码学哈希需安装可选的xxhash包xxhash_cbor规范化 CBOR xxHash可复现哈希 xxHash 速度同样需要xxhash包需要特别强调文档中的安全警告非密码学强度的哈希算法xxhash系列理论上会提高哈希碰撞的风险可能导致未定义行为甚至在多租户环境下泄露私有信息。即便碰撞概率极低在开启前也应权衡自身的安全风险承受能力与性能收益。从源码实现看这个安全考量是有具体机制支撑的非密码学算法使用每进程随机种子可通过PYTHONHASHSEED覆盖目的就是防止攻击者离线预计算碰撞块见 kv_cache_utils.py 中resolve_none_hash_seed与init_none_hash的实现——当检测到使用xxhash系列且未设置PYTHONHASHSEED时还会显式警告“块哈希在进程间不可复现”。多租户场景cache_salt缓存隔离在共享环境中缓存键碰撞或延迟差异都可能带来隐私风险。vLLM 支持通过可选的cache_salt做按请求隔离把 salt 注入第一个块的哈希只有携带相同 salt 的请求才能复用已缓存的 KV 块。这既阻止了“通过观察延迟差异推断缓存内容”的时序侧信道攻击又不牺牲性能。请求示例设计文档给出的 JSON{ messages: [ {role: system, content: You are a helpful assistant.}, {role: user, content: Here is a document with details about the world series: ...}, {role: user, content: Who won the world series in 2020?} ], cache_salt: your-cache-salt }这样配置后缓存共享被限制在“显式约定了相同 salt”的用户或请求之间实现了信任组内的复用、信任组外的隔离。底层原理哈希式块标识vLLM 选择了基于哈希的前缀缓存方案。每个 KV cache 块由其块内 token加上块之前所有前缀 token共同哈希。设计文档中的示意如下Block 1 Block 2 Block 3 [A gentle breeze stirred] [the leaves as children] [laughed in the distance] Block 1: |--- block tokens ----| Block 2: |------- prefix ------| |--- block tokens ---| Block 3: |------------------ prefix --------------------| |--- block tokens ----|第 1 块可以由 token A gentle breeze stirred 唯一标识第 3 块则需同时包含块内 token laughed in the distance 与前缀 token A gentle breeze stirred the leaves as children。因此块的哈希键形如hash(tuple[components])组件包括父块哈希Parent hash前一个块的哈希值使哈希链天然编码了前缀块内 tokenBlock tokens完整 token 元组。显式包含精确 token 是为了降低哈希碰撞概率附加哈希Extra hashes让块唯一所需的其他值如 LoRA ID、多模态输入哈希、多租户隔离用的 cache salt 等。两个重要约束设计文档 Note 1/2只缓存写满的整块以及默认哈希算法自 v0.11 起为sha256。对应到源码块的元数据由 KVCacheBlock 承载dataclass(slotsTrue) class KVCacheBlock: KV-cache block metadata. block_id: int # 块 ID范围 0 ~ num_gpu_blocks - 1 ref_cnt: int 0 # 引用计数 _block_hash: BlockHashWithGroupId | None None # 块写满并缓存后才赋值 _block_hash_num_tokens: int | None None # 哈希覆盖的 token 数 prev_free_block: KVCacheBlock | None None # 双向链表指针 next_free_block: KVCacheBlock | None None ...注意当前实现比设计文档中的简化版本更进一步_block_hash_num_tokens记录哈希覆盖的 token 数支持“块内部分边界”的命中配合prefix_match_unit配置命中边界可以比物理块更细并且BlockHashWithGroupId把块哈希与 KV cache group ID 绑定以支持多组混合 KV cache如 Mamba 全注意力混合模型。多模态输入如何参与哈希多模态请求中图片在 tokenize 后被一串占位 token 替换prefill 阶段再替换为图像嵌入。挑战在于占位 token 序列本身无法区分不同图片。vLLM 的解法是把前端图像处理器生成的图像哈希编码进块哈希。设计文档给出了具体例子一条包含文本 Whats in this image? 加图片的消息经 chat template 与 tokenize 后成为Prompt: s[INST]Whats in this image?\n[IMG][/INST] Tokenized prompt: [1, 3, 7493, 1681, 1294, 1593, 3937, 9551, 10, 4] Prompt with placeholders (P): [1, 3, 7493, 1681, 1294, 1593, 3937, 9551, P, P, ..., P, 4]假设 block size 为 16、共 41 个占位 token则各块哈希为Block 0 Parent hash: None Token IDs: 1, 3, 7493, 1681, 1294, 1593, 3937, 9551, p, ..., p Extra hash: image hash Block 1 Parent hash: Block 0 hash Token IDs: p, ..., p Extra hash: image hash Block 2 Parent hash: Block 1 hash Token IDs: p, ..., p Extra hash: image hash Block 3 Parent hash: Block 2 hash Token IDs: p, ..., p, 4 Extra hash: image hash可以看到同一张图无论落在哪些块所有覆盖占位区的块都携带相同的图像 extra hash从而把“同一前缀 同一图片”的请求精确地归入同一缓存链。KV Cache 管理器组件与关键操作APC 在 v1 中由 KV cache 管理器实现。初始化后包含四个组件Block PoolKVCacheBlock列表初始化时一次性分配全部块避免后续 Python 对象创建开销Free Block Queue空闲块队列仅保存头尾指针以便操作Cache Blocks从哈希键到块 ID 的映射缓存索引Request Blocks从请求 ID 到已分配块 ID 的映射。对应源码位于 kv_cache_manager.pyKVCacheManagerL118 起与 block_pool.py。其中空闲队列由 FreeKVCacheBlockQueue 实现——它把双向链表指针直接内嵌在KVCacheBlock中而非使用deque获得两个好处队列中部元素的移除是 O(1)“touch”命中缓存的块时需要把它从队列中摘出且无需引入元素包装器对象。块分配Allocation新请求的调度流程KVCacheManager.get_computed_blocks→allocate_slots见 get_computed_blocks 与 allocate_slots调度器调用kv_cache_manager.get_computed_blocks()对请求的 prompt token 哈希并查缓存得到已计算块序列。源码中有一个关键细节——max_cache_hit_length request.num_tokens - 1即使 prompt 全部命中缓存也必须重算最后一个 token 以取得 logits因此命中长度上限为prompt_length - 1调度器调用kv_cache_manager.allocate_slots()依次完成计算所需新块数块不足则返回 None“触摸touch”命中块引用计数 1若该块不被其他请求使用则从空闲队列移除防止其在同批次中被逐出从空闲队列头部弹出新块若弹出的块是已缓存块则同时将其逐出从此其他请求不能再复用若某块已写满 token立即加入 Cache Blocks使同一批次的后续请求也能复用它。运行中请求的流程类似但跳过查找直接调用allocate_slots()从空闲队列头部分配新块必要时逐出缓存块把新 token 追加到已有块和新块的 slot 中块一旦写满即加入 Cache Blocks。源码中的allocate_slotsdocstring 还展示了块布局的划分 comp 已命中、 new_comp 本地新命中、 ext_comp 外部 connector 命中、 new 待计算、 lookahead 投机解码预留说明 APC 与 KV connectorP/D 分离、跨实例缓存和投机解码都已打通。重复块Duplicated blocks设计文档用一个例子说明了 v1 与 v0 的行为差异。设 block size 为 4Request 1 的 prompt 为 ABCDEF、解码长度 3Prompt: [A, B, C, D, E, F] Output: [G, H, I] Time 0: Tokens: [A, B, C, D, E, F, G] Block Table: [0 (ABCD), 1 (EFG)] Cache Blocks: 0 Time 1: Tokens: [A, B, C, D, E, F, G, H] Block Table: [0 (ABCD), 1 (EFGH)] Cache Blocks: 0, 1 Time 2: Tokens: [A, B, C, D, E, F, G, H, I] Block Table: [0 (ABCD), 1 (EFGH), 2 (I)] Cache Blocks: 0, 1此时块 0 和块 1 已被缓存。若以贪心采样重发完全相同的请求Request 2它会得到全新块 3Time 0: Tokens: [A, B, C, D, E, F, G] Block Table: [0 (ABCD), 3 (EFG)] Cache Blocks: 0, 1 Time 1: Tokens: [A, B, C, D, E, F, G, H] Block Table: [0 (ABCD), 3 (EFGH)] Cache Blocks: 0, 1, 3块 3 与块 1 内容冗余同一哈希键被缓存了两次。v0 会在发现重复时释放块 3 并让 Request 2 改用块 1而 v1 的块表是**只追加append-only**的不允许把块表从[0, 3]改成[0, 1]因此会暂时保留重复块直到请求结束释放时消除。这是“正确性优先、简化调度”的权衡。释放Free请求结束时其所有块若引用计数归零即被释放。源码入口是 KVCacheManager.free。设计文档指出被释放的块按逆序追加到空闲队列尾部请求的最后一个块哈希了最多 token最不可能被其他请求复用所以应当最先被逐出。逐出Eviction, LRU当空闲队列头部块最近最少使用的块仍是已缓存块时必须将其逐出以防被复用。逐出三步从空闲队列头部弹出该 LRU 块从 Cache Blocks 中移除该块 ID清除块哈希源码中对应KVCacheBlock.reset_hash()kv_cache_utils.py。端到端示例缓存命中与逐出全景设计文档用一个 block size 为 4、总共 10 个块的场景完整演示了 APC 生命周期。下面截取其中两个关键时间点原图全部位于 docs/assets/design/prefix_caching/ 目录Time 1缓存为空新请求到达。分配 4 个块其中 3 个已写满并进入 Cache Blocks第 4 个块含 3/4 个 token。Time 3Request 1 带着 14 个 prompt token 到达前 10 个 token 与 Request 0 相同。只有前 2 个块8 个 token命中缓存——因为第 3 个块只匹配到 4 个 token 中的 2 个不满足“整块命中”的要求。这正是“只缓存完整块”约束带来的块对齐损失后续时间点Time 4/5/6继续展示请求结束时块按逆序入队、命中块被 touch 后从空闲队列移除、以及分配顺序7 - 8 - 9 - 4 - 3 - 6 - 5中哪些块命中、哪些被逐出。完整图示与说明请参考 docs/design/prefix_caching.md 的 Example 一节example-time-4/5/6/7.png。收益边界与注意事项功能文档对 APC 的局限给出了明确结论值得原样保留APC 总体上不会降低 vLLM 性能APC 只缩短 prefill 阶段耗时不缩短 decode 阶段耗时。因此当 vLLM 的时间主要花在生成长答案上答案很长时APC 带来的收益很小前缀必须真实存在新请求与任何已有请求都不共享前缀时计算无法被复用APC 同样无收益。结合源码还可以补充两条实践提示请求可能主动跳过缓存查找。get_computed_blocks在enable_caching为 False 或请求标记skip_reading_prefix_cache时直接返回空kv_cache_manager.py。注释说明了典型情形请求需要 prompt logprobs或调用 all-pooling 的 pooling 模型——这些场景下复用缓存的 KV 可能改变结果语义因此不做命中缓存命中是块对齐的。命中长度上限为prompt_length - 1且按块边界对齐max_cache_hit_length request.num_tokens - 1所以即使前缀逐 token 相同最后不足一块、或因命中重算整块的部分仍需计算。若前缀恰好跨在块边界中间如示例 Time 3未对齐的部分同样不能命中。小结要点说明依据开启方式enable_prefix_cachingTrue当前CacheConfig默认即为 Truecache.py、演示脚本哈希算法--prefix-caching-hash-algosha256默认/sha256_cbor/xxhash/xxhash_cborarg_utils.py、cache.py缓存隔离请求级cache_salt注入首块哈希实现信任组内复用设计文档块标识父哈希 块内 token 附加哈希LoRA ID、图像哈希、saltkv_cache_utils.py数据结构Block Pool Free Block Queue内嵌双向链表 Cache Blocks Request Blockskv_cache_manager.py、block_pool.py逐出策略LRU请求释放时块按逆序入队长哈希块最先被逐出kv_cache_utils.py收益边界仅优化 prefill前缀不共享或 decode 主导时无收益功能文档APC 属于“近乎白拿”的优化——它不改变模型输出且默认开启。真正需要用户决策的是哈希算法选择跨环境复用选sha256_cbor追求速度且能接受碰撞风险选xxhash系以及多租户下的cache_salt隔离策略。若要继续深入建议按顺序阅读 docs/design/prefix_caching.md 与 kv_cache_manager.py、kv_cache_utils.py 两个核心实现文件。【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考