Skip to content

KV缓存与PagedAttention — 概念

传统 KV Cache 的问题

在标准 Transformer 推理中,每个请求的 KV 缓存需要预分配最大长度的连续显存:

问题:

  • 显存浪费:预分配最大长度,实际使用远小于此
  • 碎片化:不同请求释放后产生不连续的空闲区域
  • 无法共享:相同前缀的请求各自保存独立副本

PagedAttention 原理

PagedAttention 借鉴操作系统虚拟内存的分页机制:

核心概念

概念说明
Block(块)固定大小的 KV 缓存单元,默认 16 tokens
Block Table(块表)虚拟块到物理块的映射表
Block Pool(块池)所有可用物理块的集合
Reference Counting(引用计数)跟踪每个物理块的引用数,支持共享

块的生命周期

前缀缓存(Prefix Caching)

Prefix Caching 允许具有相同前缀的请求共享 KV 缓存块:

实现方式:

  1. 每个 block 计算内容的哈希值
  2. 新请求的 block 哈希与已有 block 匹配
  3. 匹配成功则复用已有物理块(引用计数 +1)
  4. 不匹配则从空闲池分配新块

哈希计算

block_hash = hash(block_content, parent_block_hash)

这是链式哈希:每个块的哈希依赖其内容和父块的哈希,确保相同序列位置上的相同内容产生相同哈希。链的种子 NONE_HASH 现默认确定性(#51875):密码学哈希(sha256/sha256_cbor)用固定种子 "vllm-none-hash",使不同进程算出的 block hash 一致——跨进程 prefix cache 复用(P2P offload、多实例共享)由此才能工作;非密码学哈希(xxhash)保留随机种子防预计算碰撞。P2P tier 握手时经 get_none_hash_seed() 通告对端。

内部 checkpoint 与有状态层的「前缀缓存」

Mamba/KDA 等有状态层没有逐 token 的 KV,传统上无法做前缀缓存。v0.26.1rc0 后通过「内部 prefill checkpoint」打通(#52789/#53614,TTFT 改善 9%~25%):chunked prefill 中在内部对齐位置(num_tokens-1)//hash_block_size*hash_block_size,而非仅 prompt 尾部)写状态快照(MambaSpec.prefill_checkpoint_alignmentget_mamba_prefill_checkpoint_position()),后续请求可从中间状态恢复而非重放整个前缀;并解除 not use_eagle 限制,使部分前缀命中与推测解码可同用(详见 topics/speculative-decoding/)。DCP 下亦然:resolve_dcp_kv_block_size 把 full-attention/MLA 的 block_size 乘 dcp_world_size(Mamba/SW 保持复制),Hybrid 协调器的 partial hash hit 不再要求 dcp_world_size==1(#50493/#53598,Kimi K3 打通 DCP 前缀命中)。

稀疏保留间隔也已从环境变量 VLLM_PREFIX_CACHE_RETENTION_INTERVAL 升格为 CacheConfig.prefix_cache_retention_interval 参数且默认 0(#52216):0 = 只保留语义 checkpoint(最近 replay 边界、共享前缀交汇点),正数 = 额外按该间隔保留周期性 checkpoint,None = 密集保留;仅作用于 sliding-window 与 Mamba group,OffloadingConnector 调度器同享该配置(#51886)。

KV-Cache 布局标准化(Layout Refactor 收官)

RFC #42082 的布局重构在本窗口收官(#51612/#51704/#51718):每个 attention backend 各自为政的 get_kv_cache_shape()/get_kv_cache_stride_order() 被统一布局描述符取代——新文件 vllm/v1/kv_cache_layout.py 定义 KVCacheLayout 枚举,逻辑形状恒为 [L, B, H, N, C](Layer/Block/Head/NumStates/Content),每个成员是一个 stride 置换,带 is_layer_compact/is_block_contiguous 等谓词。Backend 改经 supported_kv_cache_layouts()(按偏好排序)声明能力;布局在 engine core 里一次性解析(resolve_kv_cache_layout(),memory profiling 前调用,混合形状收窄到 block-compact、校验 VLLM_KV_CACHE_LAYOUT 与 connector 偏好),几何计算集中到 compute_layer_kv_cache_shape_bytes / create_kv_cache_viewstorch.as_strided 在扁平 int8 buffer 上切层视图)。需要 packing 的 backend(如 TurboQuant)经 customize_spec() 钩子自行发布 num_head_slots/state_content_bytes。这为混合 page size 分组(Qwen3.8-Flash-Next 的 _get_packed_kv_cache_groups 贪心分桶 + _approximate_gcd)、GLM-5Next 的 Mamba/MLA/Kpool tail 混排布局(_get_kv_cache_groups_glm5_next)铺平了道路。

KV 缓存卸载(KV Cache Offloading)

当 GPU 显存不足时,vLLM 支持将 KV 缓存卸载到其他存储层级:

分层策略

层级延迟容量适用场景
GPU HBM~ns有限活跃请求的 KV 缓存
CPU RAM~us较大被抢占的请求
SSD/Disk~ms很大长期不活跃的请求

文件系统分层(FileSystem Tiering)

基于纯 Python 的磁盘二级缓存层:

关键特性:

  • DualQueueThreadPool:分离读写队列,读操作优先处理
  • 原子写入:先写临时文件,再 rename 确保一致性
  • O_DIRECT I/O:绕过页缓存,减少内存拷贝
  • 哈希子目录:将 KV 块按哈希映射到子目录,避免单目录文件过多

多级卸载编排器(TieringOffloadingManager)

OffloadingConnector 通过 spec_name 选择两种模式:CPUOffloadingSpec(单 CPU 层)或 TieringOffloadingSpec(多级)。多级模式下 CPU 为主层级(gateway,唯一可直接访问 GPU),其下挂若干 secondary tier(磁盘 fs、对象存储 obj)。

核心原则:所有 GPU↔secondary 数据须经 CPU 层中转 —— store 时级联写入所有层,load 时先提升(promotion)到 CPU 再到 GPU。secondary 层用 submit_load / submit_store 提交异步任务,get_finished_jobs() 轮询完成;lookup 返回 LookupResult 枚举(HIT 立即可读、MISS 未命中、HIT_PENDING 存在但尚未可读、RETRY 后端暂无法判定需稍后重试)替代旧的 bool|None,调度器据此决定命中计数与是否延迟重试。

实际搬运在 worker 进程内的 OffloadingWorker(原 OffloadingHandlervllm/v1/kv_offload/base.py)完成,方向由 submit_store / submit_load 显式表达。CPU pinned memory 的注册(cudaHostRegister,对数十 GB 缓冲可能耗时数秒)在 worker 初始化时同步完成 —— mmap 区域经 pin_mmap_region()、其余 CPU 张量经 PyTorch pin_memory=True。(PR #45850 曾把该注册挪到后台守护线程 CPUTensorPinThread 异步执行以避免启动阻塞,但因引入竞态——传输在 pinning 完成前就开始——仅 5 天后即被 #46958 回退,故当前为同步实现。)

为避免存在性检查阻塞调度器,secondary tier 可组合 AsyncLookupManager:每步末把批量 keys 投递给后台线程,下一步初取回命中结果(True / False / None 未就绪),通过线程所有权划分而非显式锁保证线程安全。

P2P 层级(PD 分离 / 对称 P2P)

v0.26 新增 p2p secondary tier(PR #42285 + #48021,vllm/v1/kv_offload/tiering/p2p/),让 PD 分离的 decoder 直接经 RDMA 从 prefiller 的 CPU 缓存拉 KV 块。它把控制面与数据面解耦

  • 控制面用 ZMQ(control/ZmqTransportVLLM_P2P_SIDE_CHANNEL_HOST:PORT,默认 localhost:5710,端口按 DP rank 偏移使各 DP 副本各占一 socket),负责 peer 发现、握手与 lookup/fetch 协调,只搬运不透明的 msgpack 字典。
  • 数据面用 NIXL RDMA(data/NixlTransportbackends 默认 ["UCX"],可选 MOONCAKE/GDS_MT/LIBFABRIC,选择逻辑与主 NixlConnector 一致),持有本地 KV 块内存区、注册远程 peer 元数据后异步 write_blocks

每个 remote peer 持有一个双向 P2PSessionClientRole 请求块 + ServerRole 服务块共用一条 ControlConnection)。两种角色模式:

对称 P2P(PR #48021)新增第三个角色键 remote_kv_source,使任意实例都能探测 peer 恰好持有哪些块——lookup() 先返回 RETRY 并登记 key,步末批量发 LookupMsg,收到携带逐 key 命中布尔值的 LookupRespMsg 后只对命中块 submit_load;经典 PD(remote_prefiller)仍走立即 HIT。握手时强制 PYTHONHASHSEED 一致(保证块内容哈希跨实例对齐)并用 SHA-256 config_fingerprint 拒绝模型/dtype/block-size 不兼容的 peer。该层叠在 OffloadingWorker 之上而非取代它——GPU↔CPU 仍由 CPUOffloadingWorker/SingleDirectionOffloadingHandler 完成,P2P 作为 secondary tier 只能经 CPU 主层级中转;新增 ParentManager ABC(tiering/base.py)在每步 serve_external_requests() 期间把 tiering manager 的回调句柄传给 P2P server role,使其能反查主层级。

MooncakeStore 多组支持

MooncakeStore 连接器现在支持混合注意力模型的多组 KV 缓存:

  • MooncakeStoreCoordinator:计算每个 KV 缓存组的 load/store mask
  • 多 TokenDatabase:每组 KV cache 维护独立的 ChunkedTokenDatabase
  • LCM 块对齐:跨异构 KV 缓存组的块对齐(使用最小公倍数)
  • 指标子系统MooncakeStoreConnectorStats 跟踪每次操作的延迟、字节数、p90 延迟和错误率
  • 接收线程池:KV load 由 VLLM_MOONCAKE_LOAD_RECV_THREADS(默认 1)控制的多线程并行执行,多个 RDMA GET 同时在途以饱和带宽、降低多块 KV 载入的尾延迟;store 端则按请求的 _saved_offset 仅存新增 KV 范围,跳过已持久化的前缀
  • DCP rank 命名空间:lookup key prefix 必须枚举所有 rank 命名空间。普通 TP 下可用 KV-head 去重(tp_count = min(tp_size, num_kv_head))减少前缀数;但开启 DCP(Decode Context Parallelism)后,每个 TP 组被切成 DCP 子组、各组存不同 decode-context 切片,DCP 复用 TP worker(dcp_rank == tp_rank % dcp_size)且 KV-head 去重不再适用,必须显式枚举 (tp_rank, pcp_rank, dcp_rank, pp_rank) 全组合,否则会漏掉 DCP rank 1+ 上的块导致 cache miss(PR #46855)。
  • 混合 DCP 前缀命中(#53324):混合模型 + DCP 下 MooncakeStore 也能服务远端 prefix 命中,截断逻辑改为对每个 full-attention group(target + draft)分别截到最终 hit_length。Mamba "align" 边界状态按边界 sub-hash 精确持久化(#51358);截断时序修复为在请求创建时(on_new_request)即执行 _truncate_mamba_request_for_prefill(#53663)。

Encoder Cache 传输(EPD)

多模态的 EPD(encoder-prefill 分离)把 encoder 输出转移到代理侧,配套新增 encoder cache 传输器:ECMooncakeConnector(#41567,vllm/distributed/ec_transfer/ec_connector/mooncake/,约 3200 行,基于 Mooncake TransferEngine,含 producer 侧 reservation 协议);另有 P2P 的 CPU EC Connector(ZMQ 控制面 + NIXL 数据面,#47941)与统一的 build_connector_worker_meta 元数据构建入口(#49585)。

异步 KV load 移出关键路径

SchedulerOutput 新增 has_sync_kv_loads(#53333):仅当本步有请求带 num_external_computed_tokens > 0同步 load 时才在 forward 前立刻 start_load_kv;否则把 _pending_load_start 延迟到 forward 提交之后再启动异步 load,把 host 侧提交开销移出关键路径。

分布式 KV 传输连接器(KV Connectors)

v1 连接器框架(vllm/distributed/kv_transfer/kv_connector/v1/)通过统一的生命周期钩子(on_schedule_endon_new_requesttake_eventshas_pending_push_workreset_cache)把 KV 缓存的生产/消费与调度解耦,支撑 Prefill-Decode 分离(PD disaggregation)部署。

NIXL 连接器:Pull 与 Push 双模式

原单一的 NixlConnector(pull/READ)被拆分为两种专用模式:

  • NixlPullConnector:D 端从 P 端 READ 拉 KV(NixlConnector 保留为向后兼容别名)。
  • NixlPushConnector:P 端在 prefill 完成后直接 NIXL WRITE 推送到 D 端预分配显存,新增专门的 writer 线程、注册/配对队列与看门狗(D 端注册后超时未收到推送则失败请求)。

注意 kv_role='kv_both'(同节点兼任 producer+consumer)已进入弃用周期,应分别使用 kv_producer / kv_consumer。NIXL 连接器还按 region 区分 KV 布局:MLA region 采用 REPLICATE(整块、key-only),full-attn region 采用 SPLIT(按 TP head 切片),使混合 attention 架构的传输得以正确工作。

状态化 SSM 模型也支持 NIXL P/D 分离 —— ssm_conv_transfer_utils.py 把 Mamba1/Mamba2/GDN 的卷积状态按 TP rank 拆解出正确的 RDMA 子投影偏移(Mamba1 仅含 x,Mamba2 含 x/B/C,GDN 含 Q/K/V),使 D rank 能从 P 端读到自己那份 conv 状态。但混合 SSM 暂不支持跨层 KV 布局,且 NixlPushConnector 的双向传输(D→P 块重传)模式已默认关闭 —— push 的 WRITE 通道已能完成 D→P 通信,双向模式会与其 READ 通道冲突;该开关仍保留给 NixlPullConnector

自描述 KV 事件

开启 self_describing_kv_events 后,OffloadingConnector 会为每个被卸载的 chunk 发出自描述的 BlockStored / BlockRemoved 事件(含组成块哈希、token、父哈希、LoRA、KV cache group 与 spec 类型),供外部消费者按块粒度索引卸载层内容。chunk 模式下重叠前缀的 chunk 会重发相同哈希,消费者需引用计数去重。KV 事件结构同时从数组编码切换为 map 编码。

每请求选择性卸载

可通过请求级 kv_transfer_params.max_offload_tokens 限制单请求最多卸载的 token 数(如只缓存系统提示前缀)。卸载策略 OffloadPolicyBLOCK_LEVEL(仅新算块)与 REQUEST_LEVEL(含前缀命中块,供需完整上下文的 tier 使用)。请求显式 skip_reading_prefix_cache 时 CPU 侧也不再查询命中(#54998)。

卸载侧近期的三处正确性修复:SWA coverage 校验(#55712)——sliding-window 命中查找的右边界会向上取整而分配侧窗口终点在实际边界,可能差一个 chunk,首个连续命中串需放大窗口 cdiv(sliding_window - 1 + right_padding, tokens_per_chunk)混合 page size 单 cache group(#54756)——DMA region 注册改为遍历 kv_cache_tensorsblock_stride 每 block 字节数、区分 layer-outermost / block-outermost 布局,同一 group 内不同层可各有 page size(Qwen3.8-Flash-Next 类架构需要);KV-cache 布局标准化(见上文)后 TurboQuant 等打包后端经 customize_spec 发布自己的 packing。

KV 缓存量化

除默认的 FP16/BF16 与 FP8 KV cache 外,新增 INT4 per-token-head 量化KVQuantMode.INT4_PER_TOKEN_HEADv1/attention/ops/int4_per_token_head.py):每字节打包两个 4-bit 值,写入前做 Randomized Hadamard Transform 预旋转,读取时用统一的 Triton 注意力内核按两路 INT4 流做 split-dot —— 相比 FP16 翻倍有效缓存容量。打包布局(如 DeepSeek V4 的 DSv4 跨层打包)在卸载时会触发专门的 cross-layer 单张量初始化分支,避免被拆成每层独立张量。

Block 大小选择

block_size 的选择影响性能:

block_size优点缺点
小(8)更精细的显存管理,浪费更少更多的元数据开销
中(16)平衡选择(默认)
大(32)更少的元数据开销更多显存浪费

相关概念