# PR #39396 完整报告

- 仓库：`sgl-project/sglang`
- 标题：[docs] DeepSeek-V4: MI355X PD disaggregation recipes for all three strategies
- 合并时间：2026-09-14 17:11
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/39396

---

# DeepSeek-V4 MI355X PD 三策略配方与 Playground 支持（PR #39396）

## 执行摘要

本 PR 为 DeepSeek-V4 cookbook 补上 MI355X 的 prefill/decode 分离部署配方，并按低延迟、均衡、高吞吐三档策略分别给出角色级 sizing；为让配方可组合，文档站共享引擎 `_playground.jsx` 新增 `roleOverrides`、`forceOff`、`notes`、`resolveRouter` 等扩展点，顺带修掉 DP-Attention 关闭后遗留 `--enable-dp-attention-local-control-broadcast`、以及投机解码 sizing 提示被抑制两个问题。改动全部落在文档与 Playground 配置层（4 个文件，+544/-40），不触碰任何运行时、内核或模型代码，因此不存在服务侧回归；风险集中在共享生成器的连带影响与配方尚未端到端验证两点。

## 功能与动机

PR body 给出的动机很直接：

> The DeepSeek-V4 cookbook had no prefill/decode disaggregation recipe for MI355X, so the MORI-IO transport, the per-role sizing it needs, and the cache tiers that pair with it were not reachable from the Playground.

也就是说，MI355X 用户此前既拿不到 ROCm 专属的 MORI-IO 传输配置，也无法从 Playground 组合出可复制的 PD 命令。作者同时强调这次不是加一个开关：

> Disaggregation is not one operating point, so each serving strategy gets its own role sizing rather than a single "PD on" toggle.

三档策略在并行度（TP-only 对 TP8/DP8）、`--max-running-requests`（8 / 96 / 256）、decode graph ladder（1–8 / 1–96 / 1–32）以及卸载层级（无 / HiCache / UMBP）上都不同，因此必须按角色、按策略表达。

## 实现拆解

### 1. 共享生成器：把业务约束编码进 Playground（`docs/src/snippets/_playground.jsx`）

| 扩展点 | 位置 | 作用与动机 |
|---|---|---|
| `resolveRouter(fc, sel)` | 模块级 | 允许平台用 `routerOverrides: [{when, port?, command}]` 覆写 PD 路由器，首个命中项生效，未提供字段回落默认，避免某平台改写默认 router 影响其他 cell |
| `HICACHE_HEADS` | 模块级常量 | HiCache 与 UMBP 互斥，`umbp` 轴需要整族剥离 HiCache 参数，故集中共享 flag 头 |
| `forceOff: [{when, stripEnv, reason}]` | attention 轴 `apply()` / `render()` | 命中时强制清空 DP flag 家族并按白名单剥 env；UI 同步置灰并显示 `reason`，避免展示一个 apply 会丢弃的取值 |
| `roleOverrides` + role 级 `when` | pdDisagg 轴 `apply()` | 同一 role 在不同 operating point 下整体替换 flags/env，不必在 Mode 下拉里堆重复项；`when` 让 ROCm 专属 sizing 不泄漏到 CUDA |
| `notes: [{when, mode, text}]` | pdDisagg 轴 `render()` | 在卡片内提示依赖跨轴事实的前置条件 |

受影响的下游逻辑：`apply()` 中 DP 分支被改写后，任何携带 `--enable-dp-attention-local-control-broadcast` 的 cell（不限本 PR 新增配方）渲染结果都会变化。

### 2. 配方声明：三档 PD 角色（`docs/src/snippets/configs/deepseek-ai/deepseek-v4.jsx`）

- `attention.dpAttn.forceOff`：低延迟与均衡的 PD role 强制 TP-only，并剥离 `SGLANG_DP_SHARED_EXPERT_LOCAL`、`SGLANG_DP_USE_GATHERV`、`SGLANG_DP_USE_REDUCE_SCATTER`。理由写在 `reason` 里——`--max-running-requests` 是 server 级并向下整除 `attn_dp_size`，打开 DP 会让 per-rank 批量低于已捕获的 graph，而命令里两个 flag 都没变。
- `pdDisagg.modes`：prefill/decode 两个 role 带 `when: {hw: ["mi355x"], strategy: ["low-latency"]}`，各自携带 MORI per-rank dispatch 预算（16384 / 128）；prefill 走 `--disable-cuda-graph`，decode 捕获 1–8 的 ladder。
- `pdDisagg.roleOverrides`：balanced 重新描述为 ceiling 96、chunked-prefill 16384、ladder 到 96；high-throughput 继承 TP8/DP8、ceiling 256、mem-fraction 0.92、ladder 截到 32。
- `transferBackends.mori`：补 transport 级 env（`SGLANG_MORI_COMBINE_DTYPE`、`MORI_IO_SQ_BACKOFF_TIMEOUT_US`、`MORI_IO_QP_MAX_SEND_WR`）并限定 `envWhen.hw`；`ibDevices` 拆成 mlx5 与 `rdma3,rdma0,...,rdma5` 两族互相 hide。
- `speculative`：`mtp-314` 从 `pro-official` 的 hide 列表移除（0813 保留 MTP head，且 DSpark 无法在 PD 下运行），`mtp-112` 仍隐藏。
- `hicache.roleOverrides`：balanced 的 prefill 配 ratio 2.5 / `page_first` / write-through / best-effort 预取，条件比 modes 更严（额外锁定 variant、quant、nodes）。

### 3. 部署面板：AMD RDMA 容器参数（`docs/src/snippets/_deployment.jsx`）

抽出共享常量 `AMD_RDMA_DOCKER_FLAGS`（`--device /dev/infiniband`、`--cap-add IPC_LOCK`、memlock/stack/nofile 上限），挂到 MI300X/MI325X/MI350X/MI355X 四张卡的 `multiNodeDockerFlags`，让多节点 PD 的 Docker 命令自带 RDMA 直通。

### 4. Cookbook 正文（`docs/cookbook/autoregressive/DeepSeek/DeepSeek-V4.mdx`）

新增 §3.8 讲清三档 operating point、角色差异、卸载层级如何区分均衡与高吞吐，以及 ceiling / graph ladder 的算术；§3.9 说明 UMBP 是 HiCache 的兄弟而非其后端；同时改写 MTP/DSpark 段落，指出 PD 下 DSpark 不可用、应回落 MTP 3-1-4。

### 5. 测试与验证配套

无单元测试（作者标注 N/A，理由是未触碰运行时）。验证方式为 `docs/scripts/check_cookbook_configs.mjs` 加浏览器逐策略 × 角色渲染；所有新 cell 保持 `verified: false`，等待 cookbook 的 "Submit verified cell" 流程确认。

### `docs/src/snippets/_playground.jsx`

文档站 Playground 的共享命令生成引擎，本 PR 的主要逻辑改动集中在此：新增 roleOverrides / forceOff / notes / resolveRouter 四组扩展点，并修复 DP flag 家族与 local-control-broadcast 的连带剥离。它被所有 cookbook 复用，改动会外溢到其他模型的 cell。

### `docs/src/snippets/_playground.jsx` — 角色解析与 DP-Attention 强制关闭

```jsx
// 约束匹配器：所有带 when 的配置（routerOverrides / modes / roleOverrides /
// notes / forceOff）都复用它 —— key 之间是 AND，单个 key 的取值数组内是 OR。
const matchConstraint = (base, constraint) => {
  if (!constraint || typeof constraint !== "object") return false;
  const entries = Object.entries(constraint);
  if (entries.length === 0) return false;
  return entries.every(([k, vs]) =>
    Array.isArray(vs) && vs.includes(base[k]));
};

// PD 路由器是所有 cell 共享的，所以想换路由策略的平台声明
// routerOverrides: [{when, port?, command}]，而不是把默认值改掉；
// 首个命中项生效，override 未提供的字段回落到默认 router。
const resolveRouter = (fc, sel) => {
  if (!fc || !fc.router) return null;
  const hit = (fc.routerOverrides || []).find(
    (r) => r && matchConstraint(sel, r.when));
  return hit ? { ...fc.router, ...hit } : fc.router;
};

// Attention 轴 apply()：forceOff 表示取值由选择本身决定，用户显式选择与
// base cell 都不再有发言权 —— 用于 PD role 与其派生 cell 拓扑不同的场合。
const dpForced = (knobEntry("dpAttn").forceOff || []).find(
  (r) => r && h.matchConstraint(factsNow(), r.when));
if (dpForced) {
  flags = h.stripFlagsByFirstToken(flags, [
    ...h.DP_HEADS, "--enable-dp-attention",
    "--enable-dp-attention-local-control-broadcast",
  ]);
  // 只服务于 DP 的 env 必须随 flag 一起撤掉，否则会留在一个已经没有 DP 的命令里。
  const stripEnv = dpForced.stripEnv || [];
  if (stripEnv.length) {
    env = env.filter((e) => !stripEnv.includes(e.split("=")[0]));
  }
} else if (value.dpAttn !== null && value.dpAttn !== undefined
    && !blocked("dpAttn", value.dpAttn)) {
  const dpHead = h.flagSpelling(flags, h.DP_HEADS, "--dp-size");
  // local-control-broadcast 只在 DP attention 打开时有意义，必须随它下架；
  // apply 每次从 base cell 重新播种，因此重新调整 DP 大小（而非关闭）时会恢复。
  const hadLocalBroadcast =
    h.hasFlag(flags, "--enable-dp-attention-local-control-broadcast");
  flags = h.stripFlagsByFirstToken(flags, [
    ...h.DP_HEADS, "--enable-dp-attention",
    "--enable-dp-attention-local-control-broadcast",
  ]);
  if (typeof value.dpAttn === "number" && value.dpAttn > 0) {
    flags = h.insertAfter(flags, h.ANCHOR_NEAR_TP, [
      `${dpHead} ${value.dpAttn}`,
      "--enable-dp-attention",
      ...(hadLocalBroadcast
        ? ["--enable-dp-attention-local-control-broadcast"] : []),
    ]);
  }
}

// pdDisagg 轴 apply()：role 自带的 when 让它只对一部分选择生效；
// roleOverrides 把同一条 role 在更窄的选择下重新描述，于是低延迟与高吞吐
// 两种 operating point 不必在 Mode 下拉里各占一项。首个命中项整体替换该
// role 的 flags/env，此时 role 自己的 when 门控被跳过。
const roleOverride = (fc.roleOverrides || []).find((r) =>
  r && r.mode === mode && r.when && h.matchConstraint(sel, r.when));
const roleSpec = roleOverride || modeMeta;
const modeGate = roleOverride ? null : (modeMeta && modeMeta.when);
const modeOk = !modeGate || Object.keys(modeGate).every(
  (k) => (modeGate[k] || []).includes(sel[k]));
if (modeOk && roleSpec && roleSpec.flags && roleSpec.flags.length) {
  // 先剥同头 flag，再追加 role 自己的版本 —— 让 role 的取值压过 base cell。
  flags = h.stripFlagsByFirstToken(
    flags, roleSpec.flags.map((f) => f.split(/[\s=]/)[0]));
  adds.push(...roleSpec.flags);
}

// render() 里把 forceOff 的结果镜像到 UI：展示命令真正会取到的值并置灰，
// 而不是留一个看似可点、apply 却会丢弃的选项。
const forced = (knob.forceOff || []).find(
  (r) => r && h.matchConstraint(base, r.when));
renderSelect(forced ? false : knobDisplay(knob), entriesFor(knob),
  (nv) => setKnob(knob.id, nv), base, labelFor(knob),
  { hideValues: hideNullFor(knob),
    disabled: kc.disabled || !!forced,
    disabledReason: forced ? forced.reason : kc.disableReason });

```

### `docs/src/snippets/configs/deepseek-ai/deepseek-v4.jsx`

DeepSeek-V4 的 Playground 配方定义文件，本 PR 的核心交付物：MI355X 低延迟 / 均衡 / 高吞吐三档 PD role 的 sizing、MORI transport env、rdmaN NIC 列表、HiCache 与 UMBP 的层级划分以及 DP-Attention 的 forceOff 约束都在这里声明。

### `docs/src/snippets/configs/deepseek-ai/deepseek-v4.jsx` — 三档 PD 配方与 DP 强约束

```jsx
// Attention 卡：低延迟与均衡两个 PD role 只跑 TP-only。--max-running-requests
// 是 server 级、按 attn_dp_size 向下整除的，只有 dp_size == 1 时它才同时等于
// per-rank batch；读者若自行打开 DP，per-rank 批量会掉到已捕获 graph 之下，
// 而命令里两个 flag 都没变。所以这里直接 forceOff（高吞吐本身就是 DP 点，
// 故意不列在 when 里），并把 DP 专属 env 一并清掉。
{ id: "dpAttn", label: "DP-Attention",
  forceOff: [
    { when: { hw: ["mi355x"], strategy: ["low-latency", "balanced"],
              pdMode: ["prefill", "decode"] },
      stripEnv: ["SGLANG_DP_SHARED_EXPERT_LOCAL",
                 "SGLANG_DP_USE_GATHERV",
                 "SGLANG_DP_USE_REDUCE_SCATTER"],
      reason: "低延迟 / 均衡 PD role 是 TP-only，这正是 --cuda-graph-bs-decode 能等于完整 ceiling 的前提。" },
  ],
  values: [null, false, 4, 8, {
    value: 16, disable: { nodes: ["single"] },
    disableReason: "DP-Attention=16 需要 16 个 rank，请先把 Deploy 面板切到多节点。" }],
  labels: { "auto": "Auto", "false": "Off" } },

// PD 卡：role 自带 when，因此 ROCm 专属 sizing 不会泄漏到 CUDA。
// MORI 的 per-rank dispatch 预算按 role 定（prefill 16384 / decode 128），
// 所以它挂在 role 上而非 transport 上；prefill 跑 eager（dsv4 indexer 的
// prefill 路径未做 graph 捕获）并整批派发 chunked-prefill，decode 则捕获
// 小批量 ladder、每步最多派发一个 step 的 token 量。
modes: [
  { id: "prefill", label: "Prefill role",
    when: { hw: ["mi355x"], strategy: ["low-latency"] },
    env: ["SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK=16384"],
    flags: ["--load-balance-method round_robin", "--mem-fraction-static 0.86",
            "--max-running-requests 8", "--swa-full-tokens-ratio 0.1",
            "--disable-cuda-graph", "--context-length 1048576"] },
  { id: "decode", label: "Decode role",
    when: { hw: ["mi355x"], strategy: ["low-latency"] },
    env: ["SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK=128"],
    flags: ["--load-balance-method round_robin", "--mem-fraction-static 0.86",
            "--max-running-requests 8",
            "--cuda-graph-bs-decode 1 2 3 4 5 6 7 8"] },
],

// 均衡档：仍是 TP-only，但 ceiling 提到 96、chunked-prefill 预算 16384。
// TP-only 让 server 级 ceiling 同时就是 per-rank batch，decode ladder 因此
// 能一路铺到 96；高吞吐档的 256 在 DP 8 下每 rank 只得 32，ladder 相应只到 32。
// 下面只保留各 role 相对 base cell 真正变化的 flag，其余条目（tokenizer-worker-num、
// stream-interval、watchdog-timeout、enable-metrics 等）与原配方一致，此处省略。
roleOverrides: [
  { mode: "prefill", when: { hw: ["mi355x"], strategy: ["balanced"] },
    env: ["SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK=16384"],
    flags: ["--mem-fraction-static 0.86", "--max-running-requests 96",
            "--chunked-prefill-size 16384", "--disable-cuda-graph"] },
  { mode: "decode", when: { hw: ["mi355x"], strategy: ["balanced"] },
    env: ["SGLANG_MORI_NUM_MAX_DISPATCH_TOKENS_PER_RANK=128"],
    flags: ["--mem-fraction-static 0.86", "--max-running-requests 96",
            // 实际为 1..96 的完整 ladder，这里省略中间项。
            "--cuda-graph-bs-decode 1 2 … 96"] },
],

// 均衡档的卸载层级：prefill 挂 HiCache 主机层（ratio 2.5、page_first、
// write-through、best-effort 预取）；decode 不挂，因为 MORI 下 HiCache
// 在 decode role 上不被推荐。高吞吐档改用 UMBP，二者互斥。
// 该 override 的条件比 modes 更严：额外锁定 variant / quant / nodes。
{
  mode: "prefill",
  when: { hw: ["mi355x"], variant: ["pro"], quant: ["fp4"],
          strategy: ["balanced"], nodes: ["single"] },
  memLayout: "page_first",
  ioBackend: "direct",
  ratio: 2.5,
  writePolicy: "write_through",
  prefetchPolicy: "best_effort",
}

```

## 评论区精华

该 PR 的评审记录非常干净：0 条 review comment、0 条普通 comment，只有 reviewer HaiShaw 的一条空 body APPROVED。因此有信息量的讨论全部来自 PR body 的自我论证，需要按原话理解：

> "Disaggregation is not one operating point, so each serving strategy gets its own role sizing rather than a single 'PD on' toggle."

> "The decode graph ladders differ because `--max-running-requests` is server-wide and floor-divided by `attn_dp_size`"

> "Left to the reader, switching DP on would cut the per-rank batch below the captured graphs without changing either flag in the command, so the roles force DP-Attention off and gray the control."

> "Turning DP-Attention off stripped `--dp` and `--enable-dp-attention` but left `--enable-dp-attention-local-control-broadcast` orphaned in the command. ... This affected any cell carrying that flag, not just the new recipes."

可以看到，本 PR 最重要的设计权衡（role 为何强制 DP 关闭、高吞吐为何被排除在 forceOff 之外、roleOverrides 为何优于下拉重复项）由作者单方面给出并直接获批，缺少第三方对 " 把业务约束编码进文档生成器 " 这一做法的挑战。

## 风险与影响

| 风险 | 具体位置 | 说明 |
|---|---|---|
| 共享生成器连带变更 | `_playground.jsx` DP 分支 | 该文件被所有 cookbook 复用；孤儿 flag 的修复作者自述会影响任何携带 `--enable-dp-attention-local-control-broadcast` 的 cell，而 PR 无自动回归手段 |
| 恢复逻辑的隐式耦合 | `hadLocalBroadcast` | 恢复依赖 "apply 每次从 base cell 重新播种 " 这一契约，未来若引入缓存或增量渲染会静默失效 |
| env 去重粒度偏粗 | pdDisagg 轴 env 合并 | role env 以 `!env.includes(e)` 精确字符串去重，同 key 不同值可能生成两条；`stripEnv` 是白名单，未列入的 DP env 会残留 |
| 门控粒度不一致 | `modes` / `roleOverrides` 的 `when` | balanced/high-throughput 的 `when` 仅 `hw + strategy`，而 hicache 的 `when` 用了五个维度，前者更宽松 |
| 配方未验证 | `DeepSeek-V4.mdx` §3.8 | 所有新 cell `verified: false`，PR body 记录 Extra CI（Run #34826174449）失败；若 sizing 与实机漂移会误导用户 |
| 运行时无影响 | 全量 diff | 未改动 kernel、模型或运行时文件，服务行为不变 |

影响范围：MI355X 运维者可直接从 Playground 复制含 MORI-IO、rdmaN NIC 列表与容器 fabric flags 的完整 PD 命令；文档站所有 cookbook 共享 `_playground.jsx`，因此新扩展点与 DP 剥离修复会作用于其他模型的 cell；团队侧需要 AMD/DeepSeek 维护者在 flag 或拓扑变化时同步 `when` 条件，并补齐三档配方的端到端验证。

## 关联脉络

- **同硬件文档线**：#39358 把 Qwen3.5 MI355X cookbook 与 HiCache `page_first_direct` 对齐，与本 PR 的 `page_first` / write-through 卸载配置共用同一套约定；#39360、#39172、#39356 则显示 MI355X/ROCm 侧的注意力后端与 CI 仍在快速迭代，本 PR 的配方需要跟随其变化。
- **同模型运行时线**：#37413 在 gfx950 上启用 FP8 双池统一 KV，#37382 为 DSV4 补齐主机内存缓存管理，本 PR 的 HiCache/UMBP 层级划分正是这些运行时能力在文档侧的出口。
- **PD 与统一内存线**：#37506 打通统一内存池全形态 PD 传输，#37418 启用 prefill CUDA Graph 捕获；本 PR §3.9 关于 "UMBP 是 HiCache 的兄弟而非其后端 " 的表述，以及 prefill role 显式 `--disable-cuda-graph` 的理由，都直接引用这条演进线。
 - 从更宏观的角度看，这一系列 PR 反映出一个趋势：cookbook 的 Playground 正在从 " 参数展示 " 演变为 " 约束执行器 "，`forceOff`、`roleOverrides`、`notes` 这类扩展点把原本写在正文里的部署经验固化成可校验的配置，这是文档质量收敛的积极信号，但也意味着文档生成的命令开始需要测试来兜底。