在存储分离的大模型推理系统中,Prefill 节点负责处理提示词并生成 KV Cache,Decode 节点通过共享存储复用已有前缀状态,再继续执行后续计算。表面上看,这一过程类似于“Prefill 写入对象、Decode 读取对象”;实际实现还需要解决三个更具体的问题:
- vLLM 中的 KV Cache 分散在分页 GPU 内存中,如何形成适合跨节点保存的稳定对象;
- Decode 节点如何联合查询本地 GPU Cache、LMCache L1 和远端 L2,确定当前请求真正可复用的连续前缀;
- 从远端取回的对象如何恢复到 Decode 节点新分配的 paged KV blocks 中。
这些问题对应三套不同的数据组织方式:
- vLLM page/block:面向 GPU KV 内存分配和 attention 访问;
- LMCache token chunk:面向内容寻址、命中查询和对象复用;
- 远端存储对象:面向网络传输、压缩、量化和后端管理。
KV Cache 在三套组织方式之间流动时,会经历分页地址解析、数据 gather、对象化、序列化、网络传输、反序列化和 scatter 写回。KV Connector 位于推理调度器和外部缓存系统之间,负责把“哪些 token 可以复用”的调度语义,转换成“查询哪些对象、恢复到哪些 GPU slots”的数据传输行为。
本文以 vLLM V1、LMCache Cache Engine 和 LMCache MP 分层存储架构为主线,完整分析两条典型路径:
- Prefill 节点如何将 paged KV 转换为远端可复用对象;
- Decode 节点如何扫描多层级 KV Cache,并按需恢复本地缺失的连续前缀。
本文讨论普通的因果前缀缓存,即 prefix caching。CacheBlend 等支持非连续片段复用的机制具有额外的状态校正过程,不属于本文的主流程。
一、理解 KV Cache 传输所需的四种数据形态
存储分离系统中的 KV Cache 会依次表现为 GPU page、内容寻址 chunk、内存对象和远端 payload。四种形态解决的问题不同,彼此之间通过明确的转换接口衔接。
1.1 vLLM page:GPU 物理内存管理视角
vLLM 为 attention layers 预分配 KV Cache tensors,并把可用空间划分为固定大小的 blocks。请求的逻辑 token 序列保持连续,但 KV 数据在 GPU 中可以落在离散的物理 blocks 上。
例如:
token 0–15 → physical block 37
token 16–31 → physical block 12
token 32–47 → physical block 81
token 48–63 → physical block 45
vLLM 通过两类结构描述这一关系:
block_table描述请求使用了哪些逻辑或物理 blocks;slot_mapping描述每个参与本次计算的 token 对应哪个 KV slot。
Prefill 节点与 Decode 节点拥有各自独立的 KV block allocator,因此相同 token 范围在两个节点上通常对应不同的物理 block IDs。
vLLM page 主要服务以下运行时需求:
- 快速分配和释放 GPU KV 空间;
- 支持请求随 decode 过程持续增长;
- 支持 prefix block 共享和引用计数;
- 支持 continuous batching;
- 允许 attention kernel 直接通过 block table 访问分页 KV。
LMCache 的 VLLMPagedMemGPUConnectorV2 会读取各层 KV tensor 指针、block size、KV layout 和 slot_mapping[start:end],再据此执行分页数据的提取或写回。相关实现位于:
lmcache/v1/gpu_connector/gpu_connectors.py
关键调用和数据包括:
slot_mapping[start:end]
kv_cache_pointers
block_size
multi_layer_kv_transfer(...)
这些实现表明,LMCache 通过当前 vLLM 实例的 slot mapping 访问 KV 数据,远端对象无需继承 Prefill 节点的物理 block ID。
1.2 LMCache chunk:内容寻址视角
GPU block ID 的有效范围局限于某个 vLLM 实例和某段运行时间。外部缓存需要稳定的对象名称,以支持跨进程、跨节点和跨请求复用。
LMCache 的 ChunkedTokenDatabase 将 token 序列切分成固定大小的 chunks,并为每个 chunk 生成 CacheEngineKey。当前默认配置通常采用 256-token chunk,实际大小可以通过配置调整。
概念代码如下:
prefix_hash = INITIAL_HASHfor start in range(0, len(tokens), chunk_size):end = min(start + chunk_size, len(tokens))token_chunk = tokens[start:end]prefix_hash = hash(prefix_hash, token_chunk)key = CacheEngineKey(model_name=model_name,world_size=world_size,worker_id=worker_id,chunk_hash=prefix_hash,kv_dtype=kv_dtype,request_config=request_config,)yield start, end, key
这里使用链式 prefix hash:
后一个 chunk 的 key 同时依赖此前的前缀 hash。这样能够保证:相同的局部 token 片段出现在不同前缀后时,会形成不同的普通 prefix-cache key。
一个有效的 chunk key需要区分可能影响 KV 内容或布局的因素,例如:
- 模型标识;
- tensor parallel world size;
- worker 或 rank 标识;
- KV dtype;
- token prefix hash;
- request-specific cache config;
- layerwise 模式中的 layer ID;
- 与缓存兼容性相关的其他配置。
对应实现位于:
lmcache/v1/token_database.py
关键函数包括:
process_tokens(...)
_prefix_hash(...)
Token Database 的输出不是 KV payload,而是:
(start_token, end_token, CacheEngineKey)
这组信息定义了外部缓存对象的逻辑边界。
1.3 MemoryObj:数据布局转换视角
LMCache 使用 MemoryObj 承载从 paged KV 中提取出的逻辑对象。MemoryObj 同时保存:
- 实际数据缓冲区;
- shape;
- dtype;
- memory format;
- 有效长度;
- 引用和生命周期状态。
对于普通多头 attention,LMCache 常见的中间格式为 KV_2LTD,可以概括为:
[K/V, layer, token, local hidden dimension]
vLLM 端的数据沿物理 page 组织,MemoryObj 沿逻辑 token range 组织。GPU Connector 在两种布局之间执行 gather 和 scatter。
假设当前 worker 负责:
- (L) 个 attention layers;
- 每层 (H_{kv}) 个本地 KV heads;
- head dimension 为 (D);
- chunk 包含 (C) 个 tokens;
- KV dtype 的元素大小为 (S) bytes。
一个包含 K 和 V 的未压缩 chunk,其逻辑数据量约为:
其中系数 2 对应 K 和 V。
在 layerwise 模式下,一个对象只覆盖一层:
实际对象还可能包含对齐空间、量化参数、序列化 header 和校验数据。
相关实现位于:
lmcache/v1/cache_engine.py
lmcache/v1/gpu_connector/gpu_connectors.py
关键接口包括:
metadata.get_shapes(num_tokens)
metadata.get_dtypes()
gpu_connector.batched_from_gpu(...)
gpu_connector.batched_to_gpu(...)
1.4 L2 payload:网络和存储后端视角
MemoryObj 进入 L2 adapter 前,可以经过 serde 转换。网络上的实际 payload 可能采用以下形式:
原始 BF16 / FP16 KV
FP8 量化数据 + scale
LZ4 / Zstd 压缩流
CacheGen 风格编码对象
加密字节流
LMCache 的 serde 接口包括:
class Serializer:def serialize(self, src, dst, key) -> int:...def estimate_serialized_size(self, layout_desc) -> int:...class Deserializer:def deserialize(self, src, dst, key) -> None:...
Serializer 将 KV-shaped source 写入目标 buffer,并返回实际有效字节数;Deserializer 根据对象 metadata,把序列化数据恢复为 KV-shaped destination。
L2 adapter 再把以下内容映射到具体后端:
ObjectKey
+ layout metadata
+ serialized payload
远端对象可以表现为:
- Redis 或 Valkey 中的 key-value;
- 远端注册内存中的对象;
- Mooncake 或 NIXL 管理的数据对象;
- POSIX 文件;
- NVMe slab;
- S3 或兼容对象存储中的 object。
因此,存储分离系统中的“传输对象”由两部分共同定义:
- Token Database 和 Cache Engine 确定对象代表哪一段 KV;
- serde 和 L2 adapter 确定对象在网络及后端中的物理表示。
二、组件分工:谁决定命中,谁搬数据,谁管理远端对象
完整系统中包含多层 Connector 和 Manager。它们分别处理请求语义、GPU 布局、对象索引和后端协议。
2.1 KV Connector:协调调度与传输
vLLM 的 KVConnectorBase_V1 将 Connector 划分为 scheduler-side 和 worker-side。
Scheduler-side 关注:
- 本地 KV 命中之后,外部缓存还能提供多少 token;
- 请求需要分配多少新的 KV blocks;
- 如何构造本轮 worker 所需的 connector metadata;
- 异步加载完成后,何时把请求重新放回可运行队列。
Worker-side 关注:
- 注册各层 KV cache tensors;
- 在模型执行前启动加载;
- 在具体 attention layer 前等待 KV 就绪;
- 在 attention layer 结束后启动保存;
- 在 GPU page 回收前等待异步保存完成。
核心接口包括:
class KVConnectorBase_V1:def get_num_new_matched_tokens(self,request,num_computed_tokens,):...def update_state_after_alloc(self,request,blocks,num_external_tokens,):...def build_connector_meta(self,scheduler_output,):...def register_kv_caches(self,kv_caches,):...def start_load_kv(self,forward_context,**kwargs,):...def wait_for_layer_load(self,layer_name,):...def save_kv_layer(self,layer_name,kv_layer,attn_metadata,**kwargs,):...def wait_for_save(self):...
相关实现位于:
vllm/distributed/kv_transfer/kv_connector/v1/base.py
vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py
KV Connector 主要完成四项工作:
- 命中语义转换:把外部 chunk 命中转换成 vLLM 可使用的 token 数;
- 地址关联:把 token range 关联到本轮请求的 block table 和 slot mapping;
- 生命周期管理:确保传输完成前,源或目标 pages 保持有效;
- 执行同步:在模型或某层 attention 使用 KV 前建立完成条件。
2.2 GPU Connector:执行布局转换
GPU Connector 处理:
vLLM paged layout⇅
LMCache contiguous MemoryObj layout
它接收:
- KV tensor pointers;
- slot mapping;
- block size;
- token 起止位置;
- KV layout;
- transfer direction;
- chunk 内需要跳过的 prefix tokens。
对应实现通常调用:
multi_layer_kv_transfer(...)
传输方向可以是:
D2H:GPU paged KV → CPU MemoryObj
D2D:GPU paged KV → GPU MemoryObj
H2D:MemoryObj → GPU paged KV
具体方向取决于 L1 内存位置、后端能力和部署配置。
2.3 Token Database:定义外部对象边界
Token Database 负责:
- 按
chunk_size切分 token; - 处理 store/retrieve mask;
- 生成链式 prefix hash;
- 构造
CacheEngineKey; - 返回每个对象对应的 token range。
它决定外部存储以什么粒度建立索引。
2.4 Storage Manager:组织缓存层级
Storage Manager 向 Cache Engine 提供批量对象操作:
batched_allocate(...)
batched_put(...)
batched_contains(...)
batched_get(...)
它把 chunk keys 和 MemoryObj 映射到 L1 或 L2 location,并负责对象的引用、锁定和释放。
2.5 StoreController、PrefetchController 与 L2 Adapter
StoreController 处理写入方向:
L1 新对象
→ Store Event
→ Serialize
→ L2 Adapter
→ Remote Backend
PrefetchController 处理读取方向:
L1 Miss
→ L2 Lookup
→ L2 Load
→ Deserialize
→ 回填 L1
L2 adapter 负责具体后端操作,例如:
submit_store_task(key, data)
submit_lookup_and_lock_task(keys)
submit_load_task(keys, layout_desc)
这一分层使 vLLM 调度逻辑能够保持统一,同时允许存储后端采用 Redis、NIXL、Mooncake、文件系统或对象存储。
三、Prefill 写入路径:如何形成一个可共享的远端 KV 对象
Prefill 写入可以理解为三个连续阶段:
- vLLM 在 GPU 中生成分页 KV;
- LMCache 根据 token 前缀形成内容寻址对象;
- 对象进入 L1,并异步扩散到远端 L2。
3.1 从模型计算结果到 paged KV
Prefill 请求进入 scheduler 后,vLLM 完成:
- prompt token 处理;
- 本地 prefix cache 查询;
- KV block 分配;
- block table 和 slot mapping 构造;
- model worker 调度。
模型逐层计算 Q、K、V,并把 K/V 写入对应 layer 的 paged KV tensor。
概念过程如下:
for layer_id, layer in enumerate(model.layers):query, key, value = layer.project_qkv(hidden_states)paged_kv_cache[layer_id].write(key=key,value=value,slot_mapping=slot_mapping,)hidden_states = layer.attention(query=query,kv_cache=paged_kv_cache[layer_id],block_table=block_table,)
完成 Prefill 后,请求的 KV 内容分布在多层和多个离散 blocks 中:
KV Connector 的 register_kv_caches() 向外部系统暴露这些 KV tensors。NIXL 等 Connector 可以在这里完成内存注册;LMCache GPU Connector 则保存 tensor pointers 和布局信息,用于后续 gather/scatter。
代码证据:
vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py
lmcache/v1/gpu_connector/gpu_connectors.py
关键接口:
register_kv_caches(...)
VLLMPagedMemGPUConnectorV2(...)
3.2 从 token 前缀到存储对象
LMCache 保存对象时,首先通过 Token Database 确定对象边界。
chunk_infos = token_database.process_tokens(tokens=tokens,hashes=hashes,offsets=offsets,mask=store_mask,request_configs=request_configs,
)
假设:
prompt length = 900 tokens
chunk size = 256 tokens
Token Database 可以形成:
chunk 0:token 0–255
chunk 1:token 256–511
chunk 2:token 512–767
chunk 3:token 768–899
是否保存不足一个完整 chunk 的尾部,由 save_unfull_chunk 等配置决定。关闭该行为时,保存范围会截断到完整 chunk 边界。
Store mask 可以跳过已经存在或无需重复保存的前缀。例如,外部缓存已经包含 chunk 0 和 chunk 1,本次只需要处理 chunk 2 与 chunk 3。
已存在:chunk 0、chunk 1
新增: chunk 2、chunk 3
代码证据:
lmcache/v1/token_database.py
lmcache/v1/cache_engine.py
关键逻辑:
process_tokens(...)
save_unfull_chunk
mask
3.3 从 paged KV 到连续 MemoryObj
Cache Engine 根据每个 chunk 的 token 数计算 shape 和 dtype,然后分配 MemoryObj:
for start, end, key in chunk_infos:num_tokens = end - startkv_shapes = metadata.get_shapes(num_tokens)kv_dtypes = metadata.get_dtypes()memory_obj = storage_manager.allocate(shapes=kv_shapes,dtypes=kv_dtypes,fmt=kv_format,)
完成分配后,GPU Connector 执行批量 gather:
gpu_connector.batched_from_gpu(memory_objs=memory_objs,starts=starts,ends=ends,kvcaches=kv_caches,slot_mapping=slot_mapping,
)
随后 Cache Engine 才把对象交给存储层:
storage_manager.batched_put(keys=keys,memory_objs=memory_objs,location=store_location,
)
调用关系可以归纳为:
batched_from_gpu() 使用:
slot_mapping[start:end]
从多个 layer tensors 和离散 physical blocks 中 gather KV 数据,并按逻辑 token 顺序写入 MemoryObj。
最终对象形成过程如下:
远端对象所保存的是 KV 内容、chunk key 和布局 metadata。Prefill block IDs 在完成 gather 后不再承担对象寻址作用。
代码证据:
lmcache/v1/cache_engine.py
lmcache/v1/gpu_connector/gpu_connectors.py
关键调用:
batched_from_gpu(...)
multi_layer_kv_transfer(...)
storage_manager.batched_put(...)
3.4 L1:对象进入分层存储系统的入口
在 LMCache MP 架构中,vLLM worker 向独立 LMCache Server 发出 STORE 请求。Server 在 L1 中预留目标空间,完成 GPU 到 L1 的数据复制,再将对象状态更新为可读。
L1 通常承担:
- 靠近 GPU 的低延迟对象缓存;
- GPU copy 的目标和来源;
- 新对象推入 L2 前的暂存;
- L2 对象预取回本地后的落点;
- 对象读写锁和生命周期管理。
在该流程中:
控制面:STORE 请求ObjectKeytoken rangelayout metadatacompletion event数据面:实际 KV tensorGPU transferMemoryObj buffer
控制 RPC 描述对象和状态,GPU Transfer Module 负责大体积 KV 数据搬运。
架构证据:
LMCache MP architecture
L1Manager.reserve_write()
L1Manager.finish_write()
GPUTransferModule
3.5 L2:异步扩散、序列化和远端保存
L1 写入完成后,StoreController 接收对象事件,并向配置的 L2 adapters 提交异步写任务。
不同 adapters 可以采用独立 serde。例如:
L2-0 本地 NVMe:原始 BF16 对象L2-1 远端共享存储:FP8 或压缩对象
序列化对象通常包括:
Object Header
├── format version
├── codec ID
├── original dtype
├── tensor shape
├── token count
├── layer information
├── serialized length
└── checksumCodec Metadata
├── scale / zero point
├── codebook
└── chunk indexPayload
└── serialized bytes
LMCache 提供 Serializer/Deserializer 接口,具体 header 和 payload 格式由 serde 实现决定。
若原始对象大小为 (B),序列化 payload 大小为 (B_s),则对象压缩比为:
网络实际发送量还包括:
远端后端最终保存的对象形态取决于 adapter:
Key-value 后端
Key:serialized CacheEngineKeyValue:serialized KV payload
文件后端
base_path/
└── model_id/└── worker_id/└── chunk_hash.object
远端内存后端
Object Index:CacheEngineKey → remote address / lengthRegistered Memory:[object 0][object 1][object 2]...
以 RESP backend 为例,一个 LMCache chunk 对应 Redis 或 Valkey 中的 key-value;Mooncake、NIXL 等后端则可以通过远端内存和专用数据传输路径保存对象。
实现与架构证据:
LMCache MP StoreController
LMCache serde API
LMCache L2 Adapter API
RESP backend
关键接口:
submit_store_task(...)
Serializer.serialize(...)
Serializer.estimate_serialized_size(...)
3.6 异步保存与 GPU page 生命周期
StoreController 向 L2 的异步写入可以持续更长时间,但源 paged KV 只需要保持到相关 GPU gather 或 DMA 完成。Connector 必须准确管理这一边界。
wait_for_save() 用于防止 vLLM 在源数据尚未复制完成时覆盖对应 pages。L1 到 L2 的后续异步扩散则由 LMCache 自己管理对象生命周期。
代码证据:
vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py
关键接口:
wait_for_save()
3.7 Layerwise 保存:把模型执行与 KV 写出重叠
在 layerwise 模式中,一个 token chunk 的不同层独立形成对象:
(chunk key, layer 0)
(chunk key, layer 1)
...
(chunk key, layer N)
这样可以在模型继续计算后续层时,异步保存前面层的 KV。
Layerwise 模式改变对象在 layer 维度上的切分方式:
普通模式:一个对象 = 一个 token chunk × 当前 worker 的多个 layersLayerwise:一个对象 = 一个 token chunk × 一个 layer
Token chunk 的内容寻址关系仍然保持不变。
代码证据:
vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py
lmcache/v1/cache_engine.py
关键接口:
save_kv_layer(...)
四、Decode 读取路径:如何扫描多层缓存并恢复缺失前缀
Decode 端需要先回答两个问题:
- 当前 Decode GPU 已经拥有多少连续 KV;
- LMCache 的 L1 和 L2 还能把该前缀扩展到哪里。
确定两个边界后,系统才会分配目标 pages,并恢复两者之间的缺失区间。
4.1 本地 GPU prefix 是查询起点
请求进入 Decode scheduler 后,vLLM 先查询当前实例中的本地 prefix blocks。
关键调用关系为:
new_computed_blocks, local_hit_tokens, ... = (kv_cache_manager.get_computed_blocks_for_connector(request)
)num_external_tokens, load_kv_async = (connector.get_num_new_matched_tokens(request,block_aligned_local_hit_tokens,)
)
查询顺序如下:
num_computed_tokens 表示当前 Decode GPU 已拥有的 KV 前缀。Connector 返回外部缓存在此基础上新增可提供的 token 数。
代码证据:
vllm/v1/core/sched/scheduler.py
vllm/distributed/kv_transfer/kv_connector/v1/base.py
关键调用:
get_computed_blocks_for_connector(...)
get_num_new_matched_tokens(...)
4.2 Chunk 扫描发生在 key 和索引层
LMCache Connector 将请求 token IDs 交给 lookup client:
num_external_hit_tokens = lookup_client.lookup(token_ids,lookup_id=request_id,request_configs=request_configs,
)
Lookup 使用与 Prefill 保存时一致的 Token Database,重新生成 chunk keys:
chunk_infos = token_database.process_tokens(tokens=token_ids,request_configs=request_configs,
)keys = [chunk.key for chunk in chunk_infos]
这里的扫描操作处理:
CacheEngineKey;- hit/miss;
- 对象 location;
- pin 或 lock 状态;
- 连续命中终点。
KV payload 在后续 prefetch 和 retrieve 阶段才进入数据传输路径。
代码证据:
lmcache/integration/vllm/vllm_v1_adapter.py
lmcache/v1/token_database.py
lmcache/v1/cache_engine.py
关键调用:
lookup_client.lookup(...)
token_database.process_tokens(...)
storage_manager.batched_contains(...)
4.3 多层级查询:从 L1 延伸到多个 L2
普通 Cache Engine lookup 会调用:
hit_chunks = storage_manager.batched_contains(keys=keys,search_range=retrieve_locations,pin=pin,
)
在 MP 架构中,查询进一步分解为:
例如,一个部署可以包含:
L0:Decode GPU paged KV
L1:本地 pinned CPU DRAM
L2-0:本地 NVMe
L2-1:远端共享内存
L2-2:对象存储
一个具体的查询结果可能为:
key 0:GPU 本地已命中
key 1:L1 hit
key 2:L1 miss,L2-0 hit
key 3:L1 miss,L2-0 miss,L2-1 hit
key 4:所有层 miss
有效前缀可以扩展到 key 3。命中的远端对象被 PrefetchController 加载回 L1,供后续 RETRIEVE 使用。
架构证据:
LMCache MP PrefetchController
LMCache L2 lookup flow
StorageManager.batched_contains(...)
4.4 连续前缀约束决定有效命中范围
普通自回归 prefix caching 只使用从 token 0 开始连续存在的 KV 对象。
假设索引状态为:
chunk 0:hit
chunk 1:hit
chunk 2:miss
chunk 3:hit
chunk 4:hit
有效前缀截止到 chunk 1:
vLLM 的 Connector 契约要求返回当前实际可用的最大 prompt prefix。LMCache lookup 根据连续 hit_chunks 更新命中终点,retrieve 在首个缺失对象处终止恢复。
Layerwise 模式还需要确认同一 token chunk 的各层对象均存在。任意必需 layer 缺失,该 chunk 都无法形成完整的 attention KV 状态。
代码证据:
vllm/distributed/kv_transfer/kv_connector/v1/base.py
lmcache/v1/cache_engine.py
关键行为:
hit_chunks
break
layer keys
4.5 本地命中与外部命中共同确定加载区间
命中查询结束后,系统得到两个边界:
local_hit_end:Decode GPU 已有 KV 的连续终点external_hit_end:LMCache 可提供 KV 的连续终点
三段 token 区域如下:
新增外部命中量为:
num_new_external_tokens = (num_external_hit_tokens- num_computed_tokens
)
vLLM 随后为需要进入 Decode GPU 的区间分配 blocks,并通过 update_state_after_alloc() 和 connector metadata 把目标 slot 信息传给 worker。
代码证据:
vllm/v1/core/sched/scheduler.py
vllm/distributed/kv_transfer/kv_connector/v1/base.py
lmcache/integration/vllm/vllm_v1_adapter.py
4.6 Chunk-aligned mask 把按需加载落实到对象集合
Worker 侧启动加载时,LMCache Adapter 根据本地命中和外部命中构造 retrieve mask。
token_mask = torch.ones(len(tokens),dtype=torch.bool,
)masked_token_count = (vllm_cached_tokens// lmcache_chunk_size* lmcache_chunk_size
)token_mask[:masked_token_count] = Falselmcache_engine.retrieve(tokens=tokens[:lmcache_cached_tokens],mask=token_mask[:lmcache_cached_tokens],...
)
其含义为:
[0, chunk-aligned local prefix)已完整存在于 Decode GPU,对应 chunks 不读取[chunk-aligned local prefix, external hit end)生成 keys 并加载完整 chunk objects[external hit end, prompt end)LMCache 不提供,由模型计算
Token Database 对普通 retrieve mask 的要求可以表示为:
FFFF FFFF TTTT TTTT
其中 False 形成完整 chunk 对齐的前缀,True 形成连续的待加载后缀。
代码证据:
lmcache/integration/vllm/vllm_v1_adapter.py
lmcache/v1/token_database.py
关键变量:
vllm_cached_tokens
lmcache_cached_tokens
masked_token_count
token_mask
4.7 从远端对象到 MemoryObj
Cache Engine 对 selected keys 执行批量读取。对象通常先按 location 分组:
chunks_by_location = {"LocalCPU": [(key_1, 256, 512),],"RemoteL2": [(key_2, 512, 768),(key_3, 768, 1024),],
}
然后分别调用:
memory_objs = storage_manager.batched_get(keys=keys,location=location,
)
在 MP 模式中,LOOKUP 已经把命中的 L2 objects 预取到 L1。RETRIEVE 从 L1 取得对应 MemoryObj,并在完成 GPU 写入后释放 read lock。
配置 serde 时,读取路径包含反序列化:
概念代码如下:
serialized_obj = await adapter.load(key)kv_obj = storage_manager.allocate(shapes=layout.shapes,dtypes=layout.dtypes,fmt=layout.kv_format,
)deserializer.deserialize(src=serialized_obj,dst=kv_obj,key=key,
)
代码与接口证据:
lmcache/v1/cache_engine.py
LMCache serde API
LMCache L2 Adapter API
关键调用:
storage_manager.batched_get(...)
Deserializer.deserialize(...)
4.8 从 MemoryObj scatter 到 Decode paged KV
KV-shaped MemoryObj 恢复后,GPU Connector 根据 Decode 节点当前的 slot mapping 把数据写入新分配的 pages。
gpu_connector.batched_to_gpu(memory_objs=memory_objs,starts=starts,ends=ends,kvcaches=decode_kv_caches,slot_mapping=decode_slot_mapping,skip_prefix_n_tokens=skip_prefix,
)
Prefill 与 Decode 的地址转换可概括为:
稳定关系来自:
CacheEngineKey
+ token range
+ Decode slot_mapping
代码证据:
lmcache/v1/cache_engine.py
lmcache/v1/gpu_connector/gpu_connectors.py
关键调用:
batched_to_gpu(...)
multi_layer_kv_transfer(...)
slot_mapping[start:end]
五、Page 与 Chunk 边界不一致时,系统如何处理重叠
vLLM page size 与 LMCache chunk size通常不同。常见配置中:
vLLM block size = 16 tokens
LMCache chunk size = 256 tokens
一个完整 LMCache chunk 对应:
个 vLLM blocks。
当本地命中终点落在 chunk 中间时,远端读取和 GPU 写入会呈现不同的数据量。
假设:
Decode 本地已有 KV = token 0–287,共 288 tokens
LMCache 连续命中 = token 0–511,共 512 tokens
5.1 调度视角:缺失 224 tokens
Decode 真正缺失:
token 288–511
数量为:
5.2 存储视角:读取完整 256-token chunk
LMCache chunks 为:
chunk 0:token 0–255
chunk 1:token 256–511
Adapter 计算完整本地 chunk 前缀:
因此:
chunk 0:跳过
chunk 1:读取
远端对象读取量为 256 tokens。
5.3 GPU 写入视角:跳过重叠的 32 tokens
chunk 1 中:
token 256–287:Decode 已有,32 tokens
token 288–511:Decode 缺失,224 tokens
GPU Connector 计算:
skip_prefix_n_tokens = min(chunk_length,local_cached_tokens - chunk_start,
)
代入:
chunk_length = 256
local_cached_tokens = 288
chunk_start = 256
得到:
因此,GPU 实际写入 224 tokens。
三种统计口径如下:
| 统计层次 | token 数 |
|---|---|
| 请求实际缺失 | 224 |
| 远端对象读取 | 256 |
| GPU 新写入 | 224 |
该行为体现了三种并存的粒度:
- 调度器按 token 前缀计算缺失范围;
- 外部存储按完整 chunk object 读取;
- GPU Connector 在 chunk 内按 token offset 跳过重叠数据。
代码证据:
lmcache/integration/vllm/vllm_v1_adapter.py
lmcache/v1/gpu_connector/gpu_connectors.py
关键变量:
masked_token_count
skip_prefix_n_tokens
六、Layerwise Decode:让 KV 加载与模型执行形成流水
普通加载模式需要先恢复所有相关 layers 的 KV,再开始模型 forward。Layerwise 模式把对象沿 layer 维度拆开,使前面 layers 可以提前就绪。
LMCache Adapter 通过:
retrieve_layer(...)
建立 layerwise retriever。vLLM 在每个 attention layer 使用 KV 前调用:
wait_for_layer_load(layer_name)
该方法推进加载 generator,等待当前层对应的 KV 已写入 paged KV。
Layerwise lookup 同时需要验证一个 token chunk 的必需 layer objects 均可用。任何一层缺失都会使该 chunk 无法形成完整的 KV 状态,因此连续 prefix 会截止在前一个完整 chunk。
实际流水效果取决于:
- L2 backend 是否支持并发 load;
- CPU/GPU copy stream 配置;
- serde 解码并行度;
- layer object 大小;
- CUDA Graph 模式;
- 预取深度。
代码层面可以确认的是,vLLM 和 LMCache 已提供逐层保存、加载和等待 hook。
代码证据:
vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py
lmcache/integration/vllm/vllm_v1_adapter.py
lmcache/v1/cache_engine.py
关键接口:
retrieve_layer(...)
wait_for_layer_load(...)
save_kv_layer(...)
七、存储与读取的对称性:格式可逆,请求范围按需选择
存储路径和加载路径在数据表示上形成一组反向转换:
使用无损 serde 时,加载结果应恢复与源对象等价的 KV 内容;使用有损量化或专用 KV 编码时,数值误差由 codec 定义。
请求范围由当前 Decode 请求动态决定。
假设 Prefill 保存:
chunk 0
chunk 1
chunk 2
chunk 3
后续请求只匹配到 chunk 2,同时 Decode GPU 已经拥有 chunk 0:
对应行为可以归纳为:
| 层次 | 行为 |
|---|---|
| 请求级 | 根据当前 prompt、本地命中和外部命中选择加载范围 |
| Chunk 级 | 被选中的普通对象通常完整读取 |
| GPU 写入级 | 可通过 skip_prefix_n_tokens 跳过 chunk 内重叠部分 |
| 数据格式级 | Serializer 与 Deserializer 构成正反向转换 |
| 物理地址级 | Prefill 和 Decode 分别使用自己的 slot mapping |
因此,Prefill 写入的对象集合可以被多个后续请求部分复用;每次 Decode 加载的对象集合由该请求当时的缓存状态决定。
八、KV Connector 与存储数据面的边界
KV Connector、GPU Connector、serde 和 L2 adapter 分别位于不同边界。
8.1 KV Connector 处理请求和调度语义
本地命中多少 token
外部命中多少 token
哪些 token 需要分配 GPU blocks
何时启动加载
何时等待某层
何时允许回收 pages
8.2 GPU Connector 处理内存布局
vLLM paged KV⇅
LMCache contiguous MemoryObj
其核心依据为:
KV tensor pointers
slot mapping
block size
token range
layout
transfer direction
8.3 Serde 处理对象内部表示
KV-shaped MemoryObj⇅
quantized / compressed / encrypted payload
8.4 L2 Adapter 处理网络和后端协议
ObjectKey + Payload⇅
Redis / RDMA / NIXL / File / Object Store
完整职责链如下:
这套边界也决定智能网卡卸载的合理位置:
- KV Connector 继续负责命中语义、token range、slot mapping 和生命周期;
- 智能网卡可以承接 serde、网络传输、checksum、远端对象读取和 DMA;
- GPU Connector 负责最终的 paged KV scatter,或者与支持 GPU direct access 的网卡数据面协作完成写入。
九、代码证据索引
下表汇总本文主要结论及其实现依据。
| 结论 | 代码或接口依据 |
|---|---|
| Decode 先查询本地 GPU prefix,再查询外部 Connector | vllm/v1/core/sched/scheduler.py 中 get_computed_blocks_for_connector() 与 get_num_new_matched_tokens() 的调用顺序 |
| Connector 返回本地命中之后的外部新增命中 | KVConnectorBase_V1.get_num_new_matched_tokens() 接口定义 |
| LMCache 根据 token 序列生成 chunk keys | lmcache/v1/token_database.py 中 process_tokens() 与 _prefix_hash() |
| Chunk key 使用链式 prefix hash | _prefix_hash() 将上一 chunk hash 纳入下一 chunk key |
Prefill 保存前先把 paged KV gather 成 MemoryObj |
LMCacheEngine.store() 中 batched_from_gpu() 先于 batched_put() |
| Gather/scatter 使用当前实例的 slot mapping | VLLMPagedMemGPUConnectorV2 与 multi_layer_kv_transfer() |
| L1 写入完成后由 StoreController 异步推送 L2 | LMCache MP STORE flow 与 StoreController |
| L1 miss 由 PrefetchController 查询 L2 | LMCache MP LOOKUP / PREFETCH flow |
| 外部命中按 chunk keys 扫描 | storage_manager.batched_contains(keys, ...) |
| 普通 prefix 只扩展到第一个 miss 前 | lookup/retrieve 中的连续 hit_chunks 与失败终止逻辑 |
| Decode 只读取本地命中与外部命中之间的 chunks | Adapter 根据 vllm_cached_tokens 和 lmcache_cached_tokens 构造 mask |
| Mask 以完整 chunk 前缀对齐 | masked_token_count = floor(vllm_cached_tokens / chunk_size) × chunk_size |
| 首个重叠 chunk 可以完整读取、部分写入 | GPU Connector 的 skip_prefix_n_tokens |
| 远端对象经过 serde 转换 | Serializer.serialize()、Deserializer.deserialize() |
| Layerwise 模式支持逐层保存和加载 | save_kv_layer()、retrieve_layer()、wait_for_layer_load() |
| Prefill 与 Decode 可使用不同 block IDs | 两侧分别通过自身 slot_mapping 执行 gather/scatter |
十、总结:存储分离 KV Cache 的典型行为
Prefill 路径可以概括为:
Decode 路径可以概括为:
整个系统同时存在四种关键粒度:
GPU 内存按 page/block 管理;
外部索引按 token chunk 查询;
网络和后端按 object 传输;
GPU 恢复按 slot mapping 写入。
Prefill 保存过程将瞬时的 GPU physical pages 转换为稳定的内容寻址对象。Decode 查询过程先利用本地 GPU prefix,再沿 L1 和 L2 扩展外部连续命中。实际加载集合由本地命中终点和外部命中终点共同确定。
KV Connector 贯穿这条路径,负责调度语义、metadata 和生命周期;GPU Connector 完成 paged/contiguous 布局转换;Token Database 定义 chunk 和对象 key;serde 决定网络数据表示;L2 adapter 将对象映射到具体远端后端。
最终形成的典型行为是:
Prefill 将 paged KV 整理成内容寻址 chunks,并通过 L1/L2 写入远端;Decode 先扫描本地 page cache,再扫描外部 chunk index,只恢复当前请求连续命中范围内、本地尚未具备的对象,并按当前 Decode 实例的 slot mapping 写回 paged KV。