vLLM PD 分离 · Decode 端 KV Cache 内存容量分析

vLLM v1 架构下 D 实例(Decode Worker)KV Cache pool / 调度 / 容量定量剖析
生成时间: 2026-07-15 · 数据源: vLLM 官方 GitHub raw + docs + 核心源码

1. 调研方法 & 目录

本报告基于 Playwright + Python urllib 直接从 raw.githubusercontent.com 拉取 vLLM main 分支的 design docs、用户文档及核心源码,聚焦 disaggregated prefill/decode(disagg_prefill)设计中 D 实例的 KV Cache 内存容量。

2. D 端 KV Cache pool / Block Manager 设计 (paged_attention.md · hybrid_kv_cache_manager.md · prefix_caching.md)

2.1 整体流程 (Scheduler → KVCacheManager → BlockPool → GPU tensor)

vLLM v1 把 KV Cache 管理抽象成三层(详见 hybrid_kv_cache_manager.md):

┌──────────────────────────────────────────────────────────────┐
│ Scheduler (engine core)                                      │
│   └─ on_new_request / allocate_slots / free  (per-request)   │
└────────────────────┬─────────────────────────────────────────┘
                     │  KVCacheBlocks (per-request)
                     ▼
┌──────────────────────────────────────────────────────────────┐
│ KVCacheManager (vllm/v1/core/kv_cache_manager.py)            │
│   └─ get_computed_blocks   (prefix cache hit 第一次扫描)     │
│   └─ allocate_slots        (调度一个 step 的分配)            │
│   └─ free                  (请求完成释放, 反向 order LRU)    │
│   └─ usage()               (0.0~1.0 占用率, 用于 watermark)  │
└────────────────────┬─────────────────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────────────────┐
│ BlockPool (vllm/v1/core/block_pool.py)                       │
│   blocks: list[KVCacheBlock]   # 预分配好的所有 block        │
│   free_block_queue: FreeKVCacheBlockQueue (doubly-linked)    │
│   cached_block_hash_to_block: BlockHashToBlockMap            │
│   └─ allocate() / free() / cache_full_blocks() / evict_blocks│
└────────────────────┬─────────────────────────────────────────┘
                     │
                     ▼
┌──────────────────────────────────────────────────────────────┐
│ KVCacheTensor (per-buffer, 跨 KV cache group 共享)            │
│   block_size × kv_hidden_size 的 slice 拼成 cuda 连续 tensor  │
└──────────────────────────────────────────────────────────────┘

2.2 谁是"D 端"的决定者

2.3 PD 分离后 D 端的 KV Cache 来源(三种路径)

路径机制D 端 block 状态
pull 模式 (默认) D 发起 NIXL READ, P 端被读 D 端 先分配,再由 RDMA 填充
push 模式 P 端 NIXL WRITE 到 D 预分配的 block D 端 先分配, 再被写入 (_handle_push_reg_notif)
prefix cache hit D 端 BlockPool 中已缓存相同 token(s) 的 block 直接复用, ref_cnt++, 不占 KV transfer 带宽
关键洞察:无论 pull/push, D 端的 block 都是预分配的——这意味着 D 端必须在收到 KV 之前就承诺内存。当 PD 规模较大时, 这种"预分配 + lease"机制会在 D 端形成大量的内存承诺。Lease renewal(参见 §8)正是为了缓解这一点。

3. block_size / gpu_memory_utilization / kv_cache_memory_bytes 默认值及影响 (vllm/config/cache.py 实测源码)

3.1 关键参数默认值

参数默认值所在配置类 / CLI
block_size16 tokensCacheConfig.DEFAULT_BLOCK_SIZE
gpu_memory_utilization0.92 (92%)CacheConfig.gpu_memory_utilization
cache_dtype (kv_cache_dtype)"auto" (= model dtype)CacheConfig.cache_dtype
enable_prefix_cachingTrueCacheConfig.enable_prefix_caching
kv_cache_memory_bytesNone (自动推导)CacheConfig.kv_cache_memory_bytes
kv_offloading_sizeNone (CPU offload 关闭)CacheConfig.kv_offloading_size
num_gpu_blocks_overrideNone (仅测试用)CacheConfig.num_gpu_blocks_override
mamba_block_sizeNoneHybrid Mamba-attention 模型专用

3.2 源码级证据(CacheConfig)

# vllm/config/cache.py (GitHub main, 2026-07-15)
class CacheConfig:
    DEFAULT_BLOCK_SIZE: ClassVar[int] = 16

    block_size: int = Field(default=None, gt=0)
    # After construction, always int (resolved to DEFAULT_BLOCK_SIZE)

    gpu_memory_utilization: float = Field(default=0.92, gt=0, le=1)
    """The fraction of GPU memory to be used
    for the model executor, which can range from 0 to 1."""

    cache_dtype: CacheDType = "auto"
    """Data type for kv cache storage. If 'auto',
    will use model data type."""

    kv_cache_memory_bytes: int | None = None
    """... ignores gpu_memory_utilization when not-None"""

    num_gpu_blocks: int | None  # set after profiling
    num_cpu_blocks: int | None  # set after profiling

3.3 各参数对 D 端容量的实际影响

参数变化方向对 D 端 KV Cache 容量的影响副作用
block_size ↑16 → 64 / 128 每个 block 占更多显存, 总 block 数 ↓(总 KV 显存 ≈ page_size × num_blocks) ① prefix cache hash 粒度变粗, hit-rate 降低
② 一个请求最小分配单位变大, 内部碎片变大
gpu_memory_utilization ↑0.92 → 0.97 D 端可用显存 ↑ ⇒ num_gpu_blocks ↑(翻倍效应) 留给 activation 的空间更小, decode 长 batch 更易 OOM
gpu_memory_utilization ↓0.92 → 0.70 D 端可用显存 ↓ ⇒ num_gpu_blocks ↓ 更安全, 但同时 D 实例能承载的并发 request 数 ↓(甚至 PD 中 lease 被频繁 evict)
kv_cache_memory_bytes 手动指定>0 完全旁路 gpu_memory_utilization 精细控制, 但需要自己估算(PD 推荐用法)
cache_dtype 调低fp16 → fp8_e4m3 KV Cache 容量翻倍 精度损失(需要 per-token-head scale)
prefix cachingTrue (默认) 共享 prefix 的 block 被不同 request 共用, 等效提升 D 端 KV Cache 利用效率 PD 中 P/D 端必须 hash 一致 (PYTHONHASHSEED 固定)
vLLM 官方建议 (conserving_memory.md):如果 D 端 OOM, 优先尝试 (1) 增加 TP, (2) 降低 --gpu-memory-utilization, (3) 启用量化, (4) 限制 max_model_len / max_num_seqs。

4. D 端 KV Cache 调度(swap / eviction / prefix caching)(kv_cache_manager.py · block_pool.py · prefix_caching.md)

4.1 核心调度逻辑(KVCacheManager.allocate_slots)

# vllm/v1/core/kv_cache_manager.py
watermark_blocks = int(watermark * kv_cache_config.num_blocks)  # 默认 watermark ≈ 0.01
required_blocks = num_blocks_to_allocate + watermark_blocks     # 只有 waiting/preempted 请求才加

if self.block_pool.get_num_free_blocks() < required_blocks:
    # 水位线未通过, 拒绝新的 waiting/preempted 请求 admission
    return None, None
# ... 正常 prefix cache hit 扫描 + 分配新 block

4.2 请求生命周期内的 KV Cache 操作

阶段操作代码位置D 端 PD 场景的特殊性
请求进入 get_computed_blocks() 扫描 prefix cache KVCacheManager L207-260 若 P/D 共享 prefix 且 hash 一致, 可命中
调度分配 allocate_slots() 为新 token 分配 block KVCacheManager L380-470 新到的 PD request 需要同时承接 P→D 传来的 KV
PD-PUSH 完成 _handle_push_reg_notif + send_notif nixl_kv_push_connector.md D 端注册的 block 被 P 端 WRITE 填充, 触发 completion
请求完成 free() 反向顺序释放 block → 加到 free queue 尾部 KVCacheManager L505-560, block_pool.py 尾部的 block hash 更长、复用概率低, 优先被 evict
free queue 不够 FreeKVCacheBlockQueue.popleft() evict LRU block block_pool.py 从 free queue 的 head 取 block; 若 block 已 cached(BlockHashToBlockMap), evict 掉其 hash 条目

4.3 与 CPU swap / OffloadingConnector 的协同

vLLM 提供 OffloadingConnector(详见 §5 kv_offloading_usage.md):当 D 端 HBM 不足时, 可以把 KV block 以 DMA(cudaMemcpyAsync) offload 到 pinned CPU memory。配置入口:

--kv-transfer-config '{
  "kv_connector": "OffloadingConnector",
  "kv_role": "kv_both",
  "kv_connector_extra_config": {
    "block_size": 64,               # 必须为 GPU block_size 倍数
    "cpu_bytes_to_use": 1000000000, # ~1 GiB CPU pinned mem
    "eviction_policy": "lru"        # 或 "arc"
  }
}'
PD 场景关键配置开关 (kv_offloading_usage.md):
  • offload_prompt_only: true (默认):只 offload prefill blocks, decode blocks 跳过(对 PD 中的 D 端非常友好)
  • store_threshold:block 被 lookup N 次后才 offload(减少冷 block 的拷贝开销)
  • max_tracker_size:默认 64000 条

4.4 Swap / Preemption 策略

vLLM v1 不支持把 KV swap 到 CPU 的 preemption/restore—— Scheduler 的 preemption 策略是 RECOMPUTE(见 v1 scheduler):一个请求如果分配不到 block, 直接丢弃已有 KV, 恢复到 waiting 队列, 等待 free block 水位线恢复后重新 allocate_slots。watermark_blocks(watermark 乘 num_blocks, 默认 0.01)是避免"频繁 preempt"的唯一护栏。

这意味着:D 端高并发时, PD 请求等待队列中的 lease 往往因为 preemption 而被作废——P 端已经传过来的 KV 也将被重新传输。

5. kv_cache_dtype 与精度对 D 端容量的影响 (cache.py · conserving_memory.md)

5.1 支持的 KV Cache dtype 全量

来自 vllm/config/cache.py CacheDType 类型别名:

"auto" "float16" "bfloat16" "fp8" "fp8_e4m3" "fp8_e5m2" "fp8_inc" "fp8_ds_mla" "turboquant_k8v4" "turboquant_4bit_nc" "turboquant_k3v4_nc" "turboquant_3bit_nc" "int4_per_token_head" "int8_per_token_head" "fp8_per_token_head" "nvfp4"

5.2 各 dtype 存储容量折算(相对 fp16 baseline 的比值)

dtypebits/KV token相对 fp16 容量硬件要求备注
float16 / bfloat16161.0×通用baseline
fp8 (fp8_e4m3 / fp8_e5m2)82.0×SM80+, ROCmDeepSeek V3.2 默认 fp8
fp8_inc (Intel Gaudi)82.0×HPUIntel 定制
fp8_ds_mla82.0×SM90DeepSeek MLA
turboquant_k8v462.67×SM90+K 8-bit / V 4-bit
nvfp444.0×SM100 (Blackwell)需要 per-head scale
int4_per_token_head44.0×-per-token-head 动态 scale
turboquant_3bit_nc35.3×-极限压缩, 精度损失大

5.3 对 D 端工程取舍

6. D 端 Memory Profiling:权重 + KV Cache + 激活 + 碎片 各占多少 (gpu_worker.py · mem_utils.py)

6.1 核心函数 GPUWorker.determine_available_memory()

# vllm/v1/worker/gpu_worker.py (GitHub main, 2026-07-15, 简化)
@torch.inference_mode()
def determine_available_memory(self) -> int:
    # (1) 手动指定 kv_cache_memory_bytes 时, 直接旁路 profiling
    if kv_cache_memory_bytes := self.cache_config.kv_cache_memory_bytes:
        self.model_runner.profile_run()
        return self._reserve_mm_ipc_gpu_memory(kv_cache_memory_bytes)

    # (2) 否则执行 profile pass
    with memory_profiling(self.init_snapshot,
                          weights_memory=int(self.model_runner.model_memory_usage)
                          ) as profile_result:
        self.model_runner.profile_run()          # 一次 dummy forward
        profile_torch_peak = torch.accelerator.memory_stats(...)
                                                .get("allocated_bytes.all.peak", 0)
        # (3) cudagraph 占用的额外显存估计
        cudagraph_memory_estimate = 0
        if compilation_config.cudagraph_mode != NONE:
            cudagraph_memory_estimate = self.model_runner.profile_cudagraph_memory()

    profile_result.torch_peak_increase = \
        profile_torch_peak - profile_result.before_profile.torch_peak
    profile_result.non_kv_cache_memory = (
        profile_result.non_torch_increase     # 非 torch 显存增量 (NCCL buffer, attn backend)
        + profile_result.torch_peak_increase  # activation 峰值
        + profile_result.weights_memory       # 模型权重
    )

    self.available_kv_cache_memory_bytes = (
        self.requested_memory                                    # = total_gpu_mem * 0.92
        - profile_result.non_kv_cache_memory
        - cudagraph_memory_estimate    # (VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=1 时计入)
    )
    return self._reserve_mm_ipc_gpu_memory(self.available_kv_cache_memory_bytes)

6.2 vLLM 官方分类公式(memory_profiling docstring,mem_utils.py L242-276)

The memory in one GPU can be classified into 3 categories:
    1. memory used by anything other than the current vLLM instance
    2. memory used by torch in the current vLLM instance
    3. memory used in the current vLLM instance, but not by torch

Quantitive example:
  Before creating the current vLLM instance:
      category 1: 1 GiB, category 2: 0 GiB, category 3: 0 GiB

  After creating the current vLLM instance and loading the model:
      category 1: 1 GiB, category 2: 2 GiB (model weights take 2 GiB)
                , category 3: 0.5 GiB (memory used by NCCL)

  During profiling (peak):
      category 1: 1 GiB, category 2: 4 GiB (peak activation tensors take 2 GiB)
                , category 3: 1 GiB (NCCL + attn backend buffers)

  After profiling:
      category 1: 1 GiB, category 2: 3 GiB (after GC of activation tensors)
                , category 3: 1 GiB

  => non-kv cache takes 5 GiB in total, including:
      a. 2 GiB model weights (category 2)
      b. 2 GiB reserved for peak activation tensors (category 2)
      c. 1 GiB used by non-torch components (category 3)

6.3 命名与公式速查

组成分量代码字段含义配置影响
模型权重 weights_memory = self.model_runner.model_memory_usage PyTorch 分配器中 model params 的占用量(m.consumed_memory) TP↑↓↓, 量化↓
激活峰值 torch_peak_increase = peak_alloc - before_profile.torch_peak profile pass 中 activation 张量的最大瞬时增量 cudagraph_capture_sizes, max_num_batched_tokens
非 torch 增量 non_torch_increase (category 3) NCCL buffer、attention backend 的 persistent buffer(不在 torch allocator 内) TP, attention backend 选择
CUDA Graph cudagraph_memory_estimate cudagraph capture 占用的显存(VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS v0.21 起默认开启) 每一位 capture size 约几 MB
KV Cache 可用 available_kv_cache_memory_bytes = requested_memory − non_kv_cache_memory − cudagraph_memory_estimate 最终决定 D 端 num_gpu_blocks

6.4 典型 D 端 HBM 分布(经验值, 源自 conserving_memory.md + 用户报告)

模型GPU (80/96 GiB)权重激活+非 torchCUDA GraphKV Cache (默认 0.92)
Llama-3.1-70B (TP4)4 × H100 80G~17 GiB (每卡)~3 GiB~3 GiB~51 GiB (≈64%)
Llama-3.1-8B1 × H100 80G~15 GiB~2 GiB~2 GiB~58 GiB
DeepSeek-R1 (TP8)8 × H100 80G~8 GiB (fp8 weight)~4 GiB~4 GiB~60 GiB (fp8 KV)
Qwen2.5-VL-72B (TP8)8 × H20 96G~22 GiB (每卡)~5 GiB~4 GiB~60 GiB

注:上表基于社区讨论 + 加权估算, 真实数字因 batch shape、attention backend、TP 数量差异显著。实际运行请参考 vLLM 启动时打印的 "Available KV cache memory: X GiB" 日志。

6.5 关键环境变量(显存 profiling 细粒度控制)

# 手动指定 KV cache 字节, 旁路 profiling(PD 场景最稳)
--kv-cache-memory-bytes $((50 * 1024**3))

# 关闭 cudagraph memory 估计 (v0.21+)
export VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0

# 多模态 IPC 显存保留 (multimodal PD 才需要)
# vllm/v1/worker/gpu_worker.py 中 _reserve_mm_ipc_gpu_memory() 自动维护

7. 长 context (1M) 下 vLLM 的 HBM 挑战 (kv_cache_utils.py)

1M context 意味着每个 request 的 KV Cache 需求急剧放大。以 DeepSeek-V3 (MLA, 57B A23, kv_lora_rank=512, qk_rope_head_dim=64, num_heads=128) 为例:

7.1 单 token KV 显存

模型架构kv_lora_rankqk_rope_head_dimv_head_dim单 token KV bytes (fp16)
Llama-3.1-70B (MHA)4096 (num_kv_heads×head_dim)-128~16,384
Qwen2.5-72B2,048-128~4,096
DeepSeek-V3 (MLA)51264128~1,408
Llama-4 (GQA hybrid)1,02464128~2,048

7.2 1M token KV Cache 需求(单个 request)

KV_per_request ≈ 1,000,000 × (kv_lora_rank + qk_rope_head_dim + v_head_dim) × 2 (K+V)

Llama-3.1-70B (fp16):  1M × 16,384 bytes = 15.3 GiB  ← 单卡放不下一个 1M 的 request
Qwen2.5-72B (fp16):   1M ×  4,096 bytes  =  3.8 GiB
DeepSeek-V3 (fp8):    1M ×    704 bytes  =  0.66 GiB  ← MLA + fp8 占优
DeepSeek-V3 (fp16):   1M ×  1,408 bytes  =  1.3 GiB

7.3 PD 分离对 D 端 1M context 的影响

7.4 缓解路径

方法收益代价
MLA (DeepSeek)KV 10× 压缩 vs GQA/MHA模型架构绑定
nvfp4 KV4× 容量 vs fp16SM100+, 精度下降
fp8 KV (DeepSeek V3)2× 容量几乎免费
CPU offload (PD 用 OffloadingConnector)CPU 512GB 级别, 廉价 延迟, 主机带宽瓶颈
prefix cache (per-request max_offload_tokens)prefix 不重复计算一致性依赖 hash
kv_cache_memory_bytes 指定避开 profiling 误差锁定 KV 容量需要 sizing 经验

8. PD KV Transfer 的 D 端内存承诺(Lease / Push / Watchdog)(nixl_kv_cache_lease.md · nixl_kv_push_connector.md)

8.1 关键参数与默认值

参数默认值含义
kv_lease_duration30 sP 端初始 lease(prefill 完成后锁住 KV 的时长)
decoder_kv_blocks_ttl480 s双向 transfer 下 D 端 block TTL
heartbeat intervallease_duration // 6 ≈ 5 sD 给 P 发 HB 的频率
lease extensionlease_duration * 2 // 3 ≈ 20 s每次 HB 续多长
_PUSH_WRITER_POLL_INTERVAL_MS1.0 mspush writer 自轮询间隔
push_registration_timeout= decoder_kv_blocks_ttl (480 s)D 端注册超时被 watchdog 取消

8.2 D 端三个内存事件(push 模式, 详见 nixl_kv_push_connector.md)

  1. 注册阶段:D scheduler 在 update_state_after_alloc 后, 把 local_block_ids(D 端预分配的 logical block ids) 打包成 PUSH_REG:<msgpack> 通知, 经 nixl-push-writer 线程发给 P
  2. 写入完成:P 端 WRITE 完所有 block, 发 completion notif → D _pending_completion_notifs → finished_recving → scheduler 清除 watchdog
  3. Watchdog 兜底:超过 push_registration_timeout 未完成, 取消注册, 由 engine 的 abort path 兜底 fail request

8.3 Lease renewal 对 D 端 KV Cache 的影响 (nixl_kv_cache_lease.md)

vLLM PD 中 D 端 heartbeat 必须在 request 入 scheduler 时就启动, 而不是被调度后才启动。这意味着:

8.4 Lease 设计总结

P 端 lease 生命周期:
  prefill 完成 ---(30s)--> HB1 (D~5s 一次) ---(+20s)--> HB2 ... --> KV transfer 完成/超时清空

三种回收路径(lease.md §Lease lifecycle):
  (1) D 完成 KV transfer → P 立即 free
  (2) D 持续 HB → lease 永远续
  (3) D crash / 超时 → lease 失效, P 在 ~20s 内 free

参考 PR:lease 机制由 PR #41383 引入

9. 引用 & 调研过程附件

9.1 主要引用来源

#文档链接说明
1nixl_kv_push_connector.mdGitHub rawpush mode 线程/队列/调度交互设计
2nixl_kv_cache_lease.mdGitHub rawlease renewal / heartbeat / watchdog
3hybrid_kv_cache_manager.mdGitHub rawKVCacheManager 3 层抽象 + 5 种 allocation case
4paged_attention.mdGitHub raw历史 paper 实现(已注明"不再描述现行代码")
5prefix_caching.mdGitHub rawhash-based prefix cache / LRU / sha256 default
6arch_overview.mdGitHub rawV1 多进程架构
7disagg_prefill.mdGitHub rawPD 总览 + 9 种 connector
8conserving_memory.mdHTML · raw官方显存指南
9kv_offloading_usage.mdrawOffloadingConnector / CPU+FS+P2P tier
10CacheConfig 源码GitHubblock_size=16 / gpu_memory_util=0.92 / cache_dtype 默认 / CacheDType 枚举
11GPUWorker.determine_available_memoryGitHub L440-620KV Cache 容量 profiling 入口
12KVCacheManagerGitHubwatermark / allocate_slots / free / prefix
13BlockPoolGitHubLRU free_block_queue + BlockHashToBlockMap
14memory_profiling()GitHub3-category 显存分类示例
15disagg_prefill (HTML SPA)docs.vllm.ai返回 GitHub issue MoRIIO(重定向)
16disaggregated_serving (HTML SPA)docs.vllm.ai同 15,返回 GitHub issue
17paged_attention (HTML SPA)docs.vllm.ai未单独访问

9.2 下载到本地的文件(vllm_pd_decode_memory/)

9.3 关键发现总结

  1. D 端 KV Cache 容量 = (total_gpu_memory × gpu_memory_utilization − non_kv_cache_memory − cudagraph_memory_estimate) ÷ page_size, 其中 page_size = block_size(默认 16) × num_layers × kv_hidden_size × num_kv_cache_groups(取 lcm)。
  2. 所有关键默认值透明可查:block_size=16 / gpu_memory_utilization=0.92 / cache_dtype="auto" / enable_prefix_caching=True(均来自 vllm/config/cache.py)。
  3. PD 后 D 端 KV Cache 的"承诺"模式:pull/push 都要求 D 端先分配 block, 再由 P 端 RDMA 写入。这是 D 端高并发时 HBM 紧张的第一原因。Lease renewal(30s + 20s/5s)是避免 P 端 KV 被长时间无效占用(D crash 等)的关键机制。
  4. prefix caching + CPU offloading 是 PD D 端的双保险:OffloadingConnector(默认 offload_prompt_only=true)把已满 block 异步搬到 pinned CPU memory(DMA cudaMemcpyAsync)。
  5. kv_cache_dtype 非常丰富的量化选型:支持 fp8 / fp8_ds_mla / nvfp4 / turboquant系列 / per-token-head 动态 scale, 让 D 端 KV 容量最多放大 5×。DeepSeek V3/R1 默认已是 fp8 KV。
  6. 1M context 场景:1M token × 稀疏化 KV(MLA+fp8) ≈ 0.66 GiB / request, 但 MHA/GQA 模型高达 15 GiB / request——D 端 HBM 必须走 CPU offload + MLA + nvfp4 组合路径才有经济可行性。
  7. vLLM v1 不支持 swap/restore preemption, 只有 RECOMPUTE, 所以 watermark_blocks 是避免频繁 preempt 的唯一护栏。PD 长等待队列会加剧这一路径的触发。