news 2026/8/31 22:59:39

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
昇腾 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 长上下文,特别花时间。”

这句话里其实藏着三个不同的问题,需要分开解决:

  1. 淘汰快→ KV 池容量 / 并发参数问题(本文第 3、4 节)
  2. 重算慢→ 需要 CPU Offload 承接淘汰的 KV(本文第 5 节)
  3. 看着显存不够→ 启动时的 11GB 提示多为理论预留误报,需用启动日志反算真实每 token KV(本文第 3.3 节)

混淆这三者,就会陷入"疯狂调参但没效果"的死循环。


2. P/D 节点 KV Cache 原理对比

2.1 先纠正一个词:传的不是"序列",是"KV 张量"

  • 序列(tokens):token ID 列表,如[1, 234, 567]
  • KV Cache:attention 计算产生的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)+ 生产者(生成新 KV)
KV 内容多个请求的前缀 KV(可复用)当前活跃请求完整 KV(prompt + 已生成)
能复用吗✅ 能(相同前缀命中)❌ 不能(每请求独立)
增长模式不增长(前缀长度固定)只增不减(每生成一个 token 就涨)
释放时机LRU 慢慢淘汰请求结束才释放
并发数低(如 4~8 prefill)高(如 48~128 decode)
CPU OffloadOffloadingConnector(前缀换页)RecomputeCPUOffloadConnector(抢占保护)

2.4 误区澄清:P 的 KV 真的比 D 小吗?

常见误解:

“D 节点不存历史信息、生成完就销毁,所以 D 可以复用的空间更多,压力更小。”

恰恰相反。正确理解:

  • P 的 KV 小,是因为:每个前缀短 + 可以 LRU 淘汰 + 并发低(4~8)
  • D 的 KV 大,是因为:每个请求只增不减 + 并发高(48~128)+ 不能共享

算一笔账(prompt 10K、生成 2K 的业务):

P 节点(并发 4)D 节点(并发 48)
单请求 KV~10K tokens(前缀,可复用)~11K tokens(prompt + 已生成,只增不减)
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)/ Y

3.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-len=215000时 vLLM 按它预留,于是提示"约 11GB";但业务请求实际只有 10K tokens 时,真实 KV 只占其中极小一部分,按压缩后的每 token KV 算大约只有几百 MB 量级。

怎么拿到真实数字?唯一可信的方法是上一节的启动日志反算

bytes_per_token=Available KV cache memory / GPU KV cache size

排查动作

  1. 不挂任何 KV 连接器裸跑 baseline → 用启动日志反算bytes_per_token
  2. 与社区参考值(V4-Flash 压缩后约6~8 bytes/token量级)对照:
    • 量级一致→ hybrid 压缩 KV 工作正常,之前"显存不够"只是max-model-len设太大导致的预留误报
    • 显著偏大(数倍甚至一个数量级)→ 排查是否误加了--disable-hybrid-kv-cache-manager(禁掉它会让压缩失效、KV 变大),或版本 / 连接器存在异常
  3. 不要拿启动提示当真实占用—— 一切以启动日志反算为准

小结:把max-model-len从拍脑袋的 215000 降到业务真实 P99(如 32768),这类"显存不够"的误报会自然消失,同时把大量 HBM 还给并发。

3.4 910B3 容量测算实例

硬件:910B3,单卡 64GB,单机 8 卡 =512 GB HBM

gpu-memory-utilization=0.85、KV cache 约占预算 60% 估算:

整机可用 KV 总量 ≈ 512 GB × 0.85 × 0.6 ≈ 261 GB

若 P 节点是DP2 × TP4(8 卡分 2 个 DP 组):

每 DP 组 KV 容量 ≈ 130 GB

⚠️ 关键:max-num-seqsmax-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只测单请求峰值,突发长请求激活可能超预估 → 直接 OOM
Ascend 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-seqs=32设了,但观察到“只有 1 个在处理,其余排队”

根因:两个独立的天花板
总并发能力 = max-num-seqs ×>Chunked Prefill 机制

当 prompt >max-num-batched-tokens时,vLLM 会自动切块:

200K tokens prompt,max-num-batched-tokens=32768 → Chunk 1: tokens[0~32767] → 算 KV → 追加 → Chunk 2: tokens[32768~65535] → 算 KV → 追加(能看到 Chunk1 的 KV) → ... 共约 7 个 Chunk → 全部完成后,完整 KV 传给 D 节点

关键点

  • 处理结果数值上等价于一次性处理(后面 chunk 的 attention 能看到前面所有 KV)
  • 不是每算完一个 chunk 就传给 D,D 要等所有 chunk 算完
  • 代价:调度开销让总 prefill 时间略增,但避免了 OOM
调大的三个副作用
  1. 激活显存暴增(最致命):token 数翻倍 → 激活显存近似翻倍 → 可能 OOM
  2. TTFT 反而变差:调度器会凑满 batch,请求要等整个 chunk 算完
  3. D 节点 ITL 变差:若 D 也设大,单步延迟增加(你的 D 用小值 144,没问题)
甜点值建议(910B3,TP4)
平均 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-prefills=1: 请求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-size:V4-Flash 官方推荐32(对应VLLM_PREFIX_CACHE_RETENTION_INTERVAL=4096,即 32×128=4096)。若用 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(配 block32)128 倍关系

5. CPU Offload:HBM 之外的第二层

外部共享池不可靠时,用本机 CPU 内存做 HBM 的下一层。两个连接器职责完全不同,别混淆。

5.1 OffloadingConnector(P 节点:前缀换页)

作用: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 RecomputeCPUOffloadConnector(D 节点:抢占保护)

作用: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 混部开会启动失败
  • RecomputeCPUOffloadConnectorkv_role必须是kv_consumer
  • 它不是"让 D 的 KV 普遍变大",只在抢占瞬间起作用

5.3 两者区别与协同

维度OffloadingConnector(P)RecomputeCPUOffloadConnector(D)
挂载节点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 bytes
  • P 节点cpu_bytes_to_use: 214748364800(注意确认是全局还是 per-rank,保守可先除以卡数)
  • D 节点cpu_bytes_to_use_per_rank: 26843545600(25 GB/卡,8 卡共 200GB)
  • Docker 必须加--shm-size=256g(否则大容量固定内存分配失败)
  • 主机 RAM 预留:200GB offload + 系统 + vLLM 基础,建议总 RAM ≥ 512GB

预期收益(诚实评估):

场景无 Offload200GB Offload
P 前缀未命中 + CPU 有重算(秒~十秒级)H2D 搬回(100K≈300MB,PCIe 64GB/s≈5ms)
D 抢占恢复回流 P 重算(极慢)CPU 暂存恢复(秒级)
新请求、CPU 也无重算重算(无改善)

⚠️ Offload不会让新请求凭空变快。它把"HBM 淘汰→重算"替换成"HBM 淘汰→CPU 命中→H2D 搬回"。在长上下文 + 高并发 + 多轮对话场景收益巨大;短请求 + 低复用场景收益有限。


6. 调优行动清单(可勾选)

6.1 第一步:基线测量(最重要)

  • pip show vllm vllm-ascend+git rev-parse HEAD锁定真实版本
  • 不挂任何 KV 连接器裸跑 baseline
  • 抓启动日志:GPU KV cache size+Available 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 →32768
  • gpu-memory-utilization: 0.91 →0.85
  • max-model-len: 215000 →业务真实 P99
  • 压测观察vllm:num_requests_running(应从 1 涨到 2~4)
  • 观察vllm:num_preemptions(应为 0)
  • 对比 TTFT 与吞吐

6.3 第三步:CPU Offload(基线稳定后)

  • P 节点挂OffloadingConnector(200GB)
  • D 节点挂RecomputeCPUOffloadConnector+recompute_scheduler_enable
  • Docker--shm-size=256g
  • 过 token 级闸门验证正确性

6.4 踩坑预警

信号含义动作
P 节点 OOMbatch tokens 太大退回 16384 / 24576
TTFT 反而变长batch 太大导致排队适当降低
num_preemptions > 0KV 真的不够降并发 / 加 Offload
kv_cache_usage_perc持续 >0.9KV 池见底加 Offload 或降max-model-len
外部池 external>0 就乱hybrid 未验证路径回退本地方案(见排障篇)

7. 结语

调优的本质不是"把参数拉满",而是理解每个参数背后的物理约束,在相互冲突的目标间找平衡点

  • gpu-memory-utilization:容量 vs 稳定性 → 选 0.85
  • max-num-batched-tokens:吞吐 vs 激活显存/延迟 → 选 32768 甜点
  • max-num-partial-prefills:长 prompt 吞吐 vs 短请求延迟 → 让短的插队
  • CPU Offload:HBM 容量 vs PCIe 带宽 → 用 200GB 换"不重算"

最重要的那条建议:别信任何估算(包括本文的数字),用你自己机器的启动日志反算bytes_per_token,用你真实 prompt 分布跑 benchmark。数据出来了,参数自然就定了。

下一篇(如果有)会是《PD 分离 + 外部共享 KV 池的验证之路》——等升级到 vLLM-Ascend v0.23.0 nightly-main 并用 token 级闸门跑通后再写。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 22:58:05

STM32MP157设备树编译实战:用Developer Package从零生成dtb

拿到一块STM32MP157D-DK1,烧了官方镜像,系统能跑,但真到给板子加外设时,设备树这关绕不开。我最初的想法是偷懒,直接在运行中的系统里改/boot下的dtb,结果发现开发板用的SD卡根本没有传统的/boot目录&#…

作者头像 李华
网站建设 2026/8/31 22:47:57

STM32C092 FDCAN重映射问题:为何初始化成功却无波形

1. 一版板子回来,FDCAN 却完全“失声”:问题现场还原STM32C092FCP6 这颗料我用了有一阵子了,一直以为 FDCAN 的重映射问题只会在别人的帖子里出现。结果这块板子贴片回来,第一次上电就给我上了一课:代码初始化全走绿&a…

作者头像 李华
网站建设 2026/8/31 22:46:50

STM32H7 SPI3 MISO引脚复用错误:PC11收0xFF的根因与修复

这问题我太熟了。上个月用 NUCLEO-H7S3L8 调试一块外置 ADC,CubeMX 里把 SPI3 的 MISO 指定到 PC11,代码生成、接线、上电,结果 HAL_SPI_TransmitReceive 读回来的缓冲区永远是一排 0xFF。示波器戳上去,PC11 引脚上明明有波形&…

作者头像 李华
网站建设 2026/8/31 22:44:32

Vue3 setup返回值精讲:对象、函数、render与script setup对比

Vue3 进入组合式 API 时代之后,setup函数就成了每个组件里绕不开的核心入口。很多刚接触 Vue3 的同学最大的困惑不是setup怎么定义,而是setup的返回值到底能写什么、不能写什么。返回值写错了,模板拿不到数据、事件触发不了、父组件通信失败&…

作者头像 李华
网站建设 2026/8/31 22:41:58

嵌入式笔试C语言高频代码解析:从宏定义到状态机

嵌入式笔试的 C 语言题,翻来覆去就是那几类:宏定义、关键字语义、指针、内存对齐、位操作、链表、状态机、通信协议。很多同学不是不会写代码,而是没把“面试官真正要考的点”吃透。明明一段代码看着眼熟,一追问就卡住&#xff0c…

作者头像 李华
网站建设 2026/8/31 22:41:03

基于Python的服饰推荐系统:从算法到Web落地全解析

简介:这是一套面向本科毕业设计、高校课程设计及初级项目开发者的服饰推荐系统完整实现方案,基于Python构建,解决个性化穿搭推荐场景下的商品匹配与展示问题。资源包含2000个文件,主体为1863张服饰图像数据(JPG&#x…

作者头像 李华

关于博客

这是一个专注于编程技术分享的极简博客,旨在为开发者提供高质量的技术文章和教程。

订阅更新

输入您的邮箱,获取最新文章更新。

© 2025 极简编程博客. 保留所有权利.