基于 vLLM(vllm-project/vllm)与 SGLang(sgl-project/sglang)main 分支源码,2026-08-21。
一、底层 KV Cache 实现机制(基于 main 分支源码)
1. vLLM:PagedAttention + 哈希链前缀缓存
分层结构 :KVCacheManager (vllm/v1/core/kv_cache_manager.py) → KVCacheCoordinator (kv_cache_coordinator.py,按 KV 类型分组:全注意力/滑窗/Mamba) → 每组一个 SingleTypeKVCacheManager → 共享一个 BlockPool (vllm/v1/core/block_pool.py)。
核心数据结构(直接读源码验证):
BlockPool.blocks: 全量KVCacheBlock数组;free_block_queue: 空闲块双向链表(同时充当 LRU 驱逐队列------被缓存但空闲的块留在队尾作驱逐候选)。BlockHashToBlockMap:dict[BlockHashWithGroupId, KVCacheBlock | dict[int, KVCacheBlock]],hash→物理块索引。同 hash 多块时退化为 dict(源码注释明确说明不做去重,以保证 block table append-only)。- 块哈希是链式的 :第 i 块的 hash = f(父块 hash, 本块 16 个 token id, extra_keys),见
kv_cache_utils.py的hash_block_tokens/resolve_block_hashes。这保证前缀任何位置不同则后续全部 miss。 - 命中流程:请求创建时预计算全序列 block_hashes →
get_computed_blocks()逐 hash 查cached_block_hash_to_block→ 命中块 ref_cnt++(touch),只 prefill 未命中尾部;新满块经cache_full_blocks()注册进哈希索引。
物理层 :gpu_model_runner._allocate_kv_cache_tensors() 用 torch.zeros(int8) 分配一整块 buffer(按 block_stride 打包多层别名同一 backing),再 reshape 成各 attention backend 的 KV shape;kernel 通过 slot_mapping 散射写入、block_table 分页寻址(vllm/v1/attention/backends/flash_attn.py)。
调度与抢占 :V1 抢占只有 recompute 模式 (无 CPU swap)------Scheduler._preempt_request() 直接把 num_computed_tokens 清零、释放块、重新排队重算。长 prompt 被抢占 = 全部 prefill 重来,这是 agent 场景最贵的失败模式。
2. SGLang:RadixAttention 前缀树 + 两级内存池
Radix Tree (python/sglang/srt/mem_cache/radix_cache.py,已验证):
TreeNode:children: defaultdict、key: RadixKey(token 序列 +extra_keyLoRA 隔离 +cache_salt)、value(KV 物理页索引 tensor)、lock_ref引用计数、last_access_time、host_value(host 层副本)。match_prefix(): 从 root 下行匹配;RadixKey.match()用指数搜索+二分 找首个分叉 token(避免逐 token Python 循环);命中落在节点中间时_split_node切分;结果按page_size对齐截断。evict(): 只驱逐叶子(evictable_leaves堆),默认按last_access_timeLRU(策略工厂支持 lru/lfu/fifo/slru/priority);父节点变叶子后入堆继续驱逐;lock_ref > 0的节点(被 in-flight 请求引用)受保护。- 与 vLLM 的本质区别:vLLM 是「定长块 + 平铺哈希表」,SGLang 是「变长节点的前缀树」------树天然表达前缀包含关系,且支持 token 级 page_size=1 的细粒度复用。
物理池 (mem_cache/memory_pool.py + allocator/):
ReqToTokenPool: 请求→token 物理位置的页表;MHATokenToKVPool/MLATokenToKVPool等:真正的 KV tensor(NHD/page-major 布局;MLA 存压缩 latent)。PagedTokenToKVPoolAllocator(page_size>1 时):alloc/free 用 CUDA kernel 批量操作 free page 列表。
HiRadixCache 层次缓存 (hiradix_cache.py):在 GPU (L1) 之上叠 host 内存 (L2) 和外部存储 (L3),write_through/write_back 写策略 + prefetch;L3 后端工厂(storage/backend_factory.py)注册了 mooncake / hf3fs / nixl / aibrix / file / shm 等------即 KV 可以落到跨节点分布式存储。
二、问题一:agent 长时程任务下如何高吞吐
按「省显存 → 提复用 → 架构级扩展」三档,均为两引擎已落地的机制:
| 手段 | 原理 | vLLM 落地 | SGLang 落地 |
|---|---|---|---|
| Prefix caching | 复用共享前缀跳过重复 prefill | 默认开启,链式哈希 | Radix 树,勿 --disable-radix-cache |
| KV 量化 | fp8/nvfp4 直接减半/减 75% 占用 | --kv-cache-dtype fp8(Blackwell 可 nvfp4) |
同名参数,MLA 有独立 fp8 路径 |
| Chunked prefill | 长 prompt 切块与 decode 混排,削激活峰值 | 默认开,max_num_batched_tokens |
--chunked-prefill-size |
| SWA 稀疏缓存 | 滑窗层只留窗口内 KV,窗外即时释放 | hybrid cache manager 区分 full/SWA 组 | SWAChunkCache + free_swa_out_of_window_slots |
| KV offloading | 冷 KV 异步搬到 CPU/SSD/远端 | OffloadingConnector(CPU 单层 / Tiering 多级 FS/OBJ/P2P,lru/arc 策略) |
HiRadixCache(--hicache-size + L3 backend) |
| PD 分离 | prefill/decode 各自独立 GPU 池,RDMA 传 KV | MooncakeConnector / NixlConnector(Pull/Push) | --disaggregation-mode + mooncake/nixl 后端 |
| 准入控制 | watermark 预留 + max_num_seqs + 抢占兜底 | watermark、_preempt_request(recompute) |
retract_decode |
agent 场景特有的三个杠杆(往往比引擎调参收益更大):
- Prompt 结构设计 :把稳定的 system prompt、工具 schema 放在最前面,每轮变化的工具结果放尾部------前缀哈希/radix 匹配要求逐字节一致,前缀处任何一个时间戳都会让命中点后移。
- Session 粘性路由:同一会话路由到同一实例,最大化本地缓存命中(配合下方问题二)。
- Context 压缩:LLMLingua 类工具在 prefill 前裁剪 prompt;LMCache 的 CacheBlend 支持非前缀位置 KV 复用+局部重算。
组合原则:先提命中率(前缀缓存+路由+prompt 结构)省 prefill → 再量化省单位占用 → 再 offloading 省总量 → 最后 PD/L3 做横向扩展。业界参照:Mooncake(Kimi,arXiv:2407.00079,KVCache-centric 分离架构)、LMCache(arXiv:2510.09665)、NVIDIA Dynamo+NIXL。
三、问题二:KV Cache 是否只能存本机、必须粘性路由?
这个说法一半过时、一半仍是工程现实。
过时的部分------跨节点分布式 KV 已经是两个引擎的一等能力:
| 路径 | 代表实现 | 成熟度 |
|---|---|---|
| 远端 KV 池(L3) | SGLang HiCache + Mooncake Store/3FS/NIXL/AIBrix 后端;vLLM OffloadingConnector Tiering(FS/OBJ/P2P)、LMCache(CPU/SSD/Redis/Mooncake) | 生产可用,多实例共享同一份前缀 KV,无需粘性路由也能命中 |
| PD 间 KV 直传 | vLLM MooncakeConnector/NixlPushConnector(RDMA 直写对端 GPU 显存);SGLang disaggregation/(bootstrap handshake + mooncake/nixl conn) |
大规模生产验证(Mooncake 论文即 Kimi 生产架构) |
| KV 事件发布 | vLLM kv_events.py(BlockStored/BlockRemoved,ZMQ pub-sub),供外部 gateway 建全局 KV 索引 |
较新,生态建设中 |
另外纠正一个隐含前提:即便单机内,TP 多卡时 KV 本来就是按 head 切分存在多卡上的------"单节点"本身已是分布式存储。
仍然成立的部分 ------粘性路由依然是主流默认,原因是经济性而非技术不可行:
- 跨节点取 KV 的传输成本(即使 RDMA,GB 级 KV 也要毫秒~十毫秒级)vs 就地重算 prefill 的成本,只有命中率足够高时传输才划算;
- 本地 radix/prefix 缓存命中是零成本、零延迟的;
- 所以生产栈(NVIDIA Dynamo、vLLM Production Stack、SGLang Router)的标准做法是 cache-aware routing:先按前缀哈希做亲和性调度把命中率做满,分布式 KV 作为溢出层处理"亲和节点已被驱逐/重启"的长尾。
选型建议:
- 单集群、会话可路由 → 粘性/cache-aware 路由 + 本地缓存即可,不必上分布式 KV;
- 多实例需要共享大 system prompt/RAG 语料、或实例弹性伸缩导致亲和失效频繁 → 上 LMCache 或 SGLang HiCache L3(Mooncake 后端);
- 已有 RDMA 网络、追求极致吞吐 → PD 分离 + NIXL/Mooncake 直传。
来源 :vLLM 源码 vllm/v1/core/{block_pool,kv_cache_manager,kv_cache_utils}.py、vllm/distributed/kv_transfer/kv_connector/v1/、docs/features/{mooncake_connector_usage.md,kv_offloading_usage.md};SGLang 源码 python/sglang/srt/mem_cache/{radix_cache,hiradix_cache,memory_pool}.py、python/sglang/srt/disaggregation/、docs/docs/advanced_features/{hicache_design,pd_disaggregation}.mdx;业界资料 Mooncake arXiv:2407.00079、LMCache arXiv:2510.09665、kvcache-ai/Mooncake、LMCache、NVIDIA Dynamo 文档。