昇腾 910B 上 DeepSeek-V4-Flash PD 分离:KV Cache 原理、容量估算与参数调优实战

昇腾 910B 上 DeepSeek-V4-Flash PD 分离:KV Cache 原理、容量估算与参数调优实战 姊妹篇说明本文是《DeepSeek-V4-Flash × 昇腾 PD 分离 × Mooncake 外部 KV 池乱码排障实录》的续篇。上一篇解决外部共享 KV 池能不能用结论对 hybrid V4-Flash 未验证、实测乱码本篇解决**“不用外部池时怎么把本地 KV Cache 用到极致”**。一句话结论DeepSeek-V4-Flash 在昇腾 PD 分离下真正的性能瓶颈往往不在容量不够而在max-num-batched-tokens太小导致 P 节点并发上不去此外启动时看到的“一个请求要 11GB”这类提示只是按 max-model-len 算的理论预留预算vLLM 启动估算偏保守不代表真实占用别被它吓到。本文给出从原理到容量估算再到参数调优的完整方法论。1. 引言从缓存丢失快说起典型诉求“P 节点并发一上来本地 KV Cache 淘汰就特别快缓存一丢就要重新 prefill 长上下文特别花时间。”这句话里其实藏着三个不同的问题需要分开解决淘汰快→ KV 池容量 / 并发参数问题本文第 3、4 节重算慢→ 需要 CPU Offload 承接淘汰的 KV本文第 5 节看着显存不够→ 启动时的 11GB 提示多为理论预留误报需用启动日志反算真实每 token KV本文第 3.3 节混淆这三者就会陷入疯狂调参但没效果的死循环。2. P/D 节点 KV Cache 原理对比2.1 先纠正一个词传的不是序列是KV 张量序列tokenstoken ID 列表如[1, 234, 567]KV Cacheattention 计算产生的Key / Value 张量P 节点做的是拿 prompt tokens → 前向计算 → 每步产生对应的 K、V 张量 → 把这些 KV 张量传给 D 节点。传的是 KV 张量不是 token 序列。D 节点收到后不需要重算 attention直接拿来做 decode。2.2 数据流全景┌─────────────────────────────────────────────────────────┐ │ P 节点Prefill— 工厂 │ │ │ │ Prompt: [A,B,C,D,E] │ │ ↓ Prefill 计算 │ │ KV Cache: {K_A,V_A}, {K_B,V_B}, ..., {K_E,V_E} │ │ ↓ MooncakeConnector P2P 传输传的是 KV 张量 │ │ 同时本地保留prefix caching可被 LRU 淘汰 / CPU Offload│ └──────────────────────────┬──────────────────────────────┘ ↓ KV 张量传输NPU Direct ┌─────────────────────────────────────────────────────────┐ │ D 节点Decode— 施工现场 │ │ │ │ 接收 KV: {K_A,V_A}, ..., {K_E,V_E} │ │ ↓ 加载到 D 的 KV Cache │ │ KV Cache: [A][B][C][D][E] ← 来自 P │ │ 生成 F → KV Cache: [A][B][C][D][E][F] ← F 是自己的 │ │ 生成 G → KV Cache: [A][B][C][D][E][F][G] ← G 是自己的 │ │ ... 只增不减直到请求结束才整体释放 │ └─────────────────────────────────────────────────────────┘2.3 P 节点 vs D 节点本质相同角色不同维度P 节点工厂D 节点工地角色生产者算 KV消费者接收 KV 生产者生成新 KVKV 内容多个请求的前缀 KV可复用当前活跃请求完整 KVprompt 已生成能复用吗✅ 能相同前缀命中❌ 不能每请求独立增长模式不增长前缀长度固定只增不减每生成一个 token 就涨释放时机LRU 慢慢淘汰请求结束才释放并发数低如 4~8 prefill高如 48~128 decodeCPU OffloadOffloadingConnector前缀换页RecomputeCPUOffloadConnector抢占保护2.4 误区澄清P 的 KV 真的比 D 小吗常见误解“D 节点不存历史信息、生成完就销毁所以 D 可以复用的空间更多压力更小。”恰恰相反。正确理解P 的 KV 小是因为每个前缀短 可以 LRU 淘汰 并发低4~8D 的 KV 大是因为每个请求只增不减 并发高48~128 不能共享算一笔账prompt 10K、生成 2K 的业务P 节点并发 4D 节点并发 48单请求 KV~10K tokens前缀可复用~11K tokensprompt 已生成只增不减KV 总 tokens~50K唯一前缀可淘汰~528K活跃不可淘汰显存压力小大 10 倍类比P 像图书馆存很多薄书/prefix可借给多人复用不用的放仓库D 像48 个画家同时作画每人一张画布只加不减画完才收走。画室的压力远大于图书馆。所以D 节点才是 KV Cache 压力的主战场P 节点的压力来自前缀数量多而非单个前缀大。3. KV Cache 容量估算方法论3.1 每 token KV 字节数一切估算的基石所有容量计算都依赖一个必须自己实测的量每 token KV 字节数。公式单请求 KV ≈ max_model_len × bytes_per_token这个值定不准后面所有参数都是空中楼阁。3.2 实测方法从启动日志反算启动 P 节点后抓日志里这两行# GPU KV cache size: YYYYY tokens # Available KV cache memory: Z GiB计算bytes_per_token(Z *1024^3)/ Y3.3 启动时一个请求要 11GB是理论预留不是真实占用启动 P 节点时你可能见过类似提示“一个完整请求需要 11GB 显存显存不够请调低 max-model-len”先别慌 —— 这大概率只是 vLLM 按max-model-len算出来的理论预留最大值而不是运行时真实占用。原因在于vLLM 启动时会做一次profile_run再按max-model-len × 每 token KV 大小预留单请求最坏情况的 KV 空间这个启动估算偏保守它可能没有充分计入 DeepSeek-V4-Flash 的MLA 压缩 / 混合注意力带来的 KV 缩减且默认按请求一定跑满max-model-len的最坏情况留预算DeepSeek-V4-Flash 采用压缩注意力每 token 的 KV 远小于传统 GQA 模型社区实测约 6~8 bytes/token 量级所以真实占用通常远低于启动提示看个例子max-model-len215000时 vLLM 按它预留于是提示约 11GB但业务请求实际只有 10K tokens 时真实 KV 只占其中极小一部分按压缩后的每 token KV 算大约只有几百 MB 量级。怎么拿到真实数字唯一可信的方法是上一节的启动日志反算bytes_per_tokenAvailable KV cache memory / GPU KV cache size排查动作不挂任何 KV 连接器裸跑 baseline → 用启动日志反算bytes_per_token与社区参考值V4-Flash 压缩后约6~8 bytes/token量级对照量级一致→ hybrid 压缩 KV 工作正常之前显存不够只是max-model-len设太大导致的预留误报显著偏大数倍甚至一个数量级→ 排查是否误加了--disable-hybrid-kv-cache-manager禁掉它会让压缩失效、KV 变大或版本 / 连接器存在异常不要拿启动提示当真实占用—— 一切以启动日志反算为准小结把max-model-len从拍脑袋的 215000 降到业务真实 P99如 32768这类显存不够的误报会自然消失同时把大量 HBM 还给并发。3.4 910B3 容量测算实例硬件910B3单卡 64GB单机 8 卡 512 GB HBM按gpu-memory-utilization0.85、KV cache 约占预算 60% 估算整机可用 KV 总量 ≈ 512 GB × 0.85 × 0.6 ≈ 261 GB若 P 节点是DP2 × TP48 卡分 2 个 DP 组每 DP 组 KV 容量 ≈ 130 GB⚠️ 关键max-num-seqs和max-num-batched-tokens都是每 DP 组的限制。总并发 每 DP 组 × DP size。按 V4-Flash 压缩后每 token KV 约6~8 bytes量级估算以实测bytes_per_token为准每 DP 组 130 GB 可容纳千万级 tokens的前缀 —— 容量极其充裕。所以缓存丢失快通常不是总容量不够而是并发参数 / 淘汰策略问题。4. 核心参数调优4.1 gpu-memory-utilization为什么 0.91 是悬崖边跳舞直觉上显存利用率越高越不浪费但生产环境不建议 ≥0.90原因风险说明激活显存波动profile_run只测单请求峰值突发长请求激活可能超预估 → 直接 OOMAscend Graph 开销图捕获会额外占显存且锁住不释放0.91 可能启动正常、跑着跑着崩启动预留误报 / 激活波动vLLM 按max-model-len保守预留叠加突发长请求激活超预估0.91 没余量兜底D 节点逐步增长decode 每步 KV 只增不减顶满会导致生成到一半被抢占建议值场景建议值生产环境推荐0.85压测极限性能0.90调试观察0.80绝对不要≥0.95核心认知有了 CPU Offload 后HBM 角色从主力仓库变成热数据缓存。让换页机制工作比硬塞更有效率——频繁抢占/换页的开销会吃掉硬挤出来的收益。4.2 max-num-batched-tokens从 8192 到 32768最关键的调优问题现象max-num-seqs32设了但观察到“只有 1 个在处理其余排队”。根因两个独立的天花板总并发能力 max-num-seqs ×>Chunked Prefill 机制当 prompt max-num-batched-tokens时vLLM 会自动切块200K tokens promptmax-num-batched-tokens32768 → Chunk 1: tokens[0~32767] → 算 KV → 追加 → Chunk 2: tokens[32768~65535] → 算 KV → 追加能看到 Chunk1 的 KV → ... 共约 7 个 Chunk → 全部完成后完整 KV 传给 D 节点关键点处理结果数值上等价于一次性处理后面 chunk 的 attention 能看到前面所有 KV不是每算完一个 chunk 就传给 DD 要等所有 chunk 算完代价调度开销让总 prefill 时间略增但避免了 OOM调大的三个副作用激活显存暴增最致命token 数翻倍 → 激活显存近似翻倍 → 可能 OOMTTFT 反而变差调度器会凑满 batch请求要等整个 chunk 算完D 节点 ITL 变差若 D 也设大单步延迟增加你的 D 用小值 144没问题甜点值建议910B3TP4平均 prompt 长度建议值每步能塞下≤ 4K16384~4 个8K16384~24576~2-3 个16K常见32768~2 个32K49152~65536~2 个原则让max-num-batched-tokens ≥ 2 × 平均 prompt单步至少能同时 prefill 2 个请求。安全线910B3 单卡 64GB 在 TP4 下32768 是甜点49152 是上限65536 是危险区。4.3 max-num-seqs每 DP 组的并发上限官方定义每个 DP 组允许处理的最大请求数。总并发能力 max-num-seqs ×>4.4 max-num-partial-prefills长短请求的公平性默认值是 1这会导致队头阻塞默认 max-num-partial-prefills1 请求A [200K] ──chunk1──► ──chunk2──► ... 占满 100 步 请求B [300 token] 到达 → 本可 1 步完成 → 但必须等 A 全部跑完 请求B 的 TTFT 变得和 200K 请求一样长 解决方案配套三个参数--max-num-partial-prefills2\--max-long-partial-prefills1\--long-prefill-token-threshold8192效果同时最多 2 个请求在做 partial prefill但长 prompt 只能占 1 个名额超过 8K tokens 算长 prompt短请求可以插队到长请求之间TTFT 大幅降低长 prompt 吞吐基本不受影响官方原文“Settingmax-long-partial-prefillsless thanmax-num-partial-prefillswill allow shorter prompts to jump the queue in front of longer prompts.”什么时候调压测发现长 prompt 场景下短请求 TTFT 异常高时才调。它是公平性优化开关不是必改项。4.5 block-size 与其他--block-sizeV4-Flash 官方推荐32对应VLLM_PREFIX_CACHE_RETENTION_INTERVAL4096即 32×1284096。若用 128 则 retention 需设为 16384--max-model-len降到业务真实 P99如 32768别拍脑袋填 215000--enforce-eager调试期开稳定后可视情况关--no-disable-hybrid-kv-cache-manager必须保留。V4-Flash 依赖它做压缩 / 混合注意力的 KV 管理误加--disable-hybrid-kv-cache-manager会让压缩失效、KV 占用变大4.6 参数对照总表参数原值建议值原因--max-num-batched-tokens819232768解决只有1个在跑--gpu-memory-utilization0.910.85防 OOM / 激活显存波动缓冲--max-model-len215000业务 P99如 32768省显存消除 11GB 误报--max-num-seqs3232起步每 DP 组并发上限--max-num-partial-prefills1默认2按需解决长短请求队头阻塞--block-size12832V4 官方推荐RETENTION_INTERVAL163844096配 block32128 倍关系5. CPU OffloadHBM 之外的第二层外部共享池不可靠时用本机 CPU 内存做 HBM 的下一层。两个连接器职责完全不同别混淆。5.1 OffloadingConnectorP 节点前缀换页作用P 节点 HBM 满了 → 不活跃前缀 KV 换页到 CPU → 下次同前缀请求进来从 CPU 通过 PCIe 搬回 NPU避免重算 prefix。{kv_connector:OffloadingConnector,kv_role:kv_both,kv_connector_extra_config:{cpu_bytes_to_use:214748364800,blocks_per_chunk:8,spec_name:NPUOffloadingSpec,spec_module_path:vllm_ascend.distributed.kv_transfer.kv_pool.kv_offload.native.npu}}5.2 RecomputeCPUOffloadConnectorD 节点抢占保护作用D 节点 HBM 满触发 RecomputeScheduler 抢占时把被抢占请求的 KV 暂存 CPU恢复时拷回避免回流 P 节点重算 prefill。--additional-config{scheduler_config:{recompute_scheduler_enable:true}}\--kv-transfer-config{ kv_connector: MultiConnector, kv_role: kv_consumer, engine_id: 1, kv_connector_extra_config: { connectors: [ {kv_connector: MooncakeConnectorV1, kv_role: kv_consumer, kv_port: 30100, kv_connector_extra_config: {prefill: {dp_size: 2, tp_size: 4}, decode: {dp_size: 2, tp_size: 4}}}, {kv_connector: RecomputeCPUOffloadConnector, kv_role: kv_consumer, kv_connector_extra_config: {cpu_bytes_to_use_per_rank: 26843545600}} ] } }铁律recompute_scheduler_enable只在 D 节点开P 节点或 PD 混部开会启动失败RecomputeCPUOffloadConnector的kv_role必须是kv_consumer它不是让 D 的 KV 普遍变大只在抢占瞬间起作用5.3 两者区别与协同维度OffloadingConnectorPRecomputeCPUOffloadConnectorD挂载节点P 节点D 节点触发时机HBM 满了LRU 淘汰不活跃前缀HBM 满了抢占正在 decode 的请求保护对象历史前缀 KV可复用被抢占请求的 KV避免回流 P是否跨请求共享是前缀 hash 命中否仅该请求自身是否解决新请求命中✅ 是❌ 否kv_rolekv_bothkv_consumer配套开关无recompute_scheduler_enable:true两者互补不冲突P 用 Offloading 管前缀换页D 用 Recompute 管抢占保护。5.4 200GB 配置建议换算200 GB 200 × 1024^3 214,748,364,800 bytesP 节点cpu_bytes_to_use: 214748364800注意确认是全局还是 per-rank保守可先除以卡数D 节点cpu_bytes_to_use_per_rank: 2684354560025 GB/卡8 卡共 200GBDocker 必须加--shm-size256g否则大容量固定内存分配失败主机 RAM 预留200GB offload 系统 vLLM 基础建议总 RAM ≥ 512GB预期收益诚实评估场景无 Offload200GB OffloadP 前缀未命中 CPU 有重算秒~十秒级H2D 搬回100K≈300MBPCIe 64GB/s≈5msD 抢占恢复回流 P 重算极慢CPU 暂存恢复秒级新请求、CPU 也无重算重算无改善⚠️ Offload不会让新请求凭空变快。它把HBM 淘汰→重算替换成HBM 淘汰→CPU 命中→H2D 搬回。在长上下文 高并发 多轮对话场景收益巨大短请求 低复用场景收益有限。6. 调优行动清单可勾选6.1 第一步基线测量最重要pip show vllm vllm-ascendgit rev-parse HEAD锁定真实版本不挂任何 KV 连接器裸跑 baseline抓启动日志GPU KV cache sizeAvailable KV cache memory计算bytes_per_token (Z * 1024^3) / Y判定与社区参考值V4-Flash 压缩后约6~8 bytes/token量级对照量级一致即正常显著偏大则排查是否误禁用 hybrid KV manager / 连接器异常6.2 第二步参数调优一次只改一个变量max-num-batched-tokens: 8192 →32768gpu-memory-utilization: 0.91 →0.85max-model-len: 215000 →业务真实 P99压测观察vllm:num_requests_running应从 1 涨到 2~4观察vllm:num_preemptions应为 0对比 TTFT 与吞吐6.3 第三步CPU Offload基线稳定后P 节点挂OffloadingConnector200GBD 节点挂RecomputeCPUOffloadConnectorrecompute_scheduler_enableDocker--shm-size256g过 token 级闸门验证正确性6.4 踩坑预警信号含义动作P 节点 OOMbatch tokens 太大退回 16384 / 24576TTFT 反而变长batch 太大导致排队适当降低num_preemptions 0KV 真的不够降并发 / 加 Offloadkv_cache_usage_perc持续 0.9KV 池见底加 Offload 或降max-model-len外部池 external0 就乱hybrid 未验证路径回退本地方案见排障篇7. 结语调优的本质不是把参数拉满而是理解每个参数背后的物理约束在相互冲突的目标间找平衡点gpu-memory-utilization容量 vs 稳定性 → 选 0.85max-num-batched-tokens吞吐 vs 激活显存/延迟 → 选 32768 甜点max-num-partial-prefills长 prompt 吞吐 vs 短请求延迟 → 让短的插队CPU OffloadHBM 容量 vs PCIe 带宽 → 用 200GB 换不重算最重要的那条建议别信任何估算包括本文的数字用你自己机器的启动日志反算bytes_per_token用你真实 prompt 分布跑 benchmark。数据出来了参数自然就定了。下一篇如果有会是《PD 分离 外部共享 KV 池的验证之路》——等升级到 vLLM-Ascend v0.23.0 nightly-main 并用 token 级闸门跑通后再写。