Prhub

#39396 [docs] DeepSeek-V4: MI355X PD disaggregation recipes for all three strategies

原始 PR 作者 ichbinblau 合并时间 2026-09-14 17:11 文件变更 4 提交数 1 评论 0 代码增减 +544 / -40

执行摘要

DeepSeek-V4 MI355X 补齐三档 PD 配方及 Playground 生成器支持

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."——三档策略的并行度、running-request 上限、decode graph ladder 与卸载层级都不同,必须分角色表达。

值得精读,但读者要分层看。维护 cookbook 或 Playground 的人应重点读 _playground.jsx 的四组扩展点(roleOverrides、forceOff、notes、resolveRouter)及其隐含契约——它们实际上把"哪些 flag 属于哪个 operating point"这类业务约束编码进了展示层,是这套配置引擎的重要演进方向;只关心 MI355X 部署的人读 DeepSeek-V4.mdx 的 §3.8 与 deepseek-v4.jsx 的三档 role 配置即可。建议关注两个设计决策:一是用 forceOff 而非让用户自行判断 DP 开关(因为命令里两个 flag 不变、后果却不同),二是用 roleOverrides 而非在下拉里堆重复项来表达同一 role 的多档 sizing。后续最值得补的是生成命令的快照测试:当前改动会改变其他 cell 的输出,却完全依赖人工浏览器比对。

讨论亮点

该 PR 的 review 记录非常干净:review_comments_count = 0、comments_count = 0,只有 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."

也就是说,本轮最重要的设计权衡(为什么 role 要强制 DP 关闭、为什么高吞吐被排除在 forceOff 之外、为什么 roleOverrides 优于在下拉里堆重复项)由作者单方面给出并直接获批,缺少第三方对"文档生成器承载业务约束"这一做法的挑战。

实现拆解

  1. 共享生成器扩展(docs/src/snippets/_playground.jsx,+229/-27)
    - 新增 resolveRouter(fc, sel):允许平台用 routerOverrides: [{when, port?, command}] 覆写默认 PD 路由器,首个命中项生效,未提供字段回落默认,避免某平台的 router 改写污染其他 cell。
    - 新增 HICACHE_HEADS 常量族:HiCache 与 UMBP 互斥,umbp 轴需要整族剥离 HiCache 参数,故把 flag 头集中共享。
    - Attention 轴 apply() 引入 forceOff: [{when, stripEnv, reason}]:命中时强制清空 DP_HEADS、--enable-dp-attention 与 --enable-dp-attention-local-control-broadcast,并按白名单剥掉 DP 专属 env;render() 同步把该旋钮显示为灰色 Off 并给出 reason,避免界面显示一个 apply 实际丢弃的取值。
    - pdDisagg 轴引入 roleOverrides 与 role 级 when 门控:roleOverride 命中时整体替换该 role 的 flags/env(modeGate 因而不再生效),使同一 role 在不同 operating point 下复用同一 Mode 选项;新增 notes: [{when, mode, text}] 在卡片内提示跨轴前置条件。
    - 顺带修复:关闭 DP-Attention 时原先只剥离 --dp/--enable-dp-attention,会把 --enable-dp-attention-local-control-broadcast 留成孤儿;现在一并剥离,并在只是重新调整 DP 大小时按 base cell 恢复。
  2. DeepSeek-V4 配方(docs/src/snippets/configs/deepseek-ai/deepseek-v4.jsx,+221/-6)
    - attention.dpAttn 增加 forceOff:低延迟与均衡的 PD role 强制 TP-only,并剥离 SGLANG_DP_SHARED_EXPERT_LOCAL、SGLANG_DP_USE_GATHERV、SGLANG_DP_USE_REDUCE_SCATTER 三个 DP 专属 env。
    - speculative:mtp-314 从 pro-official 的 hide 列表中移除(0813 保留 MTP head,且 DSpark 无法在 PD 下运行),mtp-112 仍隐藏。
    - pdDisagg.modes:prefill/decode 两个 role 加 when: {hw: ["mi355x"], strategy: ["low-latency"]},各自带 MORI per-rank dispatch 预算(16384 / 128)与 TP-only sizing。
    - pdDisagg.roleOverrides:为 balanced(ceiling 96、chunked-prefill 16384、ladder 到 96)与 high-throughput(继承 TP8/DP8、ceiling 256、mem-fraction 0.92、ladder 到 32)重新描述同一组 role。
    - 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;hicache.roleOverrides 给 balanced 的 prefill 配 ratio 2.5 / page_first / write-through / best-effort。
  3. 部署面板(docs/src/snippets/_deployment.jsx,+17/-4):抽出共享常量 AMD_RDMA_DOCKER_FLAGS(/dev/infiniband、IPC_LOCK、memlock/stack/nofile 上限),挂到 MI300X/MI325X/MI350X/MI355X 四张卡的 multiNodeDockerFlags,让多节点 PD 的容器命令自带 RDMA 直通。
  4. Cookbook 正文(docs/cookbook/autoregressive/DeepSeek/DeepSeek-V4.mdx,+77/-3):新增 §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 的自有提交验证流程确认。
文件 模块 状态 重要度
docs/src/snippets/_playground.jsx 配置引擎 modified 8.65
docs/src/snippets/configs/deepseek-ai/deepseek-v4.jsx 配方配置 modified 7.09
docs/src/snippets/_deployment.jsx 部署面板 modified 5.18
docs/cookbook/autoregressive/DeepSeek/DeepSeek-V4.mdx 使用文档 modified 4.43

关键符号

resolveRouter matchConstraint apply(attention 轴 dpAttn 分支与 dpForced 处理) apply(pdDisagg 轴的 roleOverride / roleSpec / modeOk 解析) render(attention 轴 forceOff 镜像与 pdDisagg 轴 note 渲染)

关键源码片段

docs/src/snippets/_playground.jsx core-logic

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

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

// 约束匹配器:所有带 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 configuration

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 强约束

// 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",
}

评论区精华

为什么三档策略要各自分角色 sizing,而不是一个 "PD on" 开关 设计

PR body 原话:"Disaggregation is not one operating point, so each serving strategy gets its own role sizing rather than a single 'PD on' toggle." 作者进一步解释三档的差异:低延迟 / 均衡 TP-only、decode ladder 能跑到完整 ceiling;高吞吐继承 TP8/DP8、ceiling 256 但每 rank 只有 32,ladder 因此截到 32,与既有 GB300 高吞吐 cell 的 512/4 = 128 对齐。

结论:通过 pdDisagg.roleOverrides + role 级 when 落地:同一 role 在不同策略下整体替换 flags/env,Mode 下拉不再堆重复项。reviewer HaiShaw 直接 APPROVED,无异议。 · 已采纳(评审无异议)

关闭 DP-Attention 会遗留 --enable-dp-attention-local-control-broadcast 正确性

作者在测试中发现的顺带修复:原先关闭 DP-Attention 只剥离 --dp 与 --enable-dp-attention,local-control-broadcast 会变成孤儿 flag。作者强调 "This affected any cell carrying that flag, not just the new recipes." 影响面超出本 PR 新增配方。

结论:现在三者一并剥离;仅在重新调整 DP 大小(而非关闭)时,按 base cell 是否携带该 flag 决定是否恢复。恢复依赖 apply 每次从 base cell 重新播种这一隐含契约。 · 已修复,但恢复逻辑存在隐式耦合

投机解码 sizing 提示在 PD role 下被抑制 正确性

作者指出:sizing 提示在 --max-running-requests 显式给出时被抑制,而每个 PD role 都显式设置该参数,结果最需要 sizing 提示的角色反而看不到它。

结论:在 role 上下文下改为显示 sizing 规则本身(如目标并发 N 时两角色都设 N*2),并在 §3.8 的 Warning 块中再次强调。 · 已修复

新配方的端到端验证状态 测试

作者声明验证方式为 docs/scripts/check_cookbook_configs.mjs 加浏览器逐策略 × 角色渲染,配方本身来自 MI355X 实机运行;CI 状态显示 PR Test (Base) 通过而 PR Test (Extra) 失败。所有新 cell 保持 verified: false 并渲染 "Not Verified" 徽标。

结论:交付时接受未验证状态,等待 cookbook 自身的 "Submit verified cell" 流程确认;这是本 PR 唯一明确留待后续处理的事项。 · 未完全解决

风险与影响

  1. 共享生成器的连带行为变更:_playground.jsx 被所有 cookbook 复用。作者明确写到 --enable-dp-attention-local-control-broadcast 的剥离修复 "affected any cell carrying that flag, not just the new recipes",意味着其他模型(例如 cookbook §3.6 的 MI355X DP 配方)生成的 Docker/命令也发生变化,而 PR 未附任何自动回归手段。
  2. hadLocalBroadcast 恢复逻辑依赖隐式契约:恢复能力建立在 "applyAllDeltas 每次渲染都从 base cell 重新播种" 之上,代码注释也承认这一点。若未来引擎引入缓存或增量渲染,该恢复会静默失效,命令里少一个 flag。
  3. env 合并是精确字符串去重而非按 key 去重:role env 以 !env.includes(e) 追加,forceOff.stripEnv 则是白名单剥离。base cell 若携带同 key 不同值的 MORI/DP env,可能生成两条同名 env 或在白名单外的 DP env 残留。
  4. role 的 when 门控粒度不一致:modes 中 balanced/high-throughput 的 when 只写 hw + strategy,而 hicache.roleOverrides 用了 hw + variant + quant + strategy + nodes 五个维度。前者更宽松,若 Playground 的选择维度扩展或 merge 逻辑变化,ROCm 专属 sizing 有泄漏到其他 variant/quant 组合的空间。
  5. 配方本身未端到端验证:所有新 cell 保持 verified: false,PR body 记录 check_cookbook_configs.mjs 与浏览器渲染验证,并显示 Extra CI(Run #34826174449)为失败状态。若 --cuda-graph-bs-decode 1..96、--max-running-requests 96、MORI env 与真实 MI355X 行为发生漂移,文档会直接把用户引向错误配置。
  6. 无破坏性影响的确认:本次没有 kernel、模型或运行时文件改动,因此不存在服务侧回归;风险全部集中在文档正确性与生成命令可复制性。

面向 MI355X 运维者:以前只能靠 Issue/内部笔记拼 PD 命令,现在可在 Playground 直接选策略 × 角色组合出含 MORI-IO、RDMA NIC 列表、容器 fabric flags 与分角色 sizing 的完整命令;低延迟/均衡/高吞吐三种 operating point 的差异被显式表达,降低了"开了 PD 但 batch 与 graph ladder 对不上"的错配概率。面向文档站本身:_playground.jsx 是跨 cookbook 的共享引擎,新增的 roleOverrides/forceOff/resolveRouter/notes 成为后续所有模型配方可复用的扩展点,同时 DP flag 剥离修复会改变已有 cell 的渲染结果。面向团队:AMD 与 DeepSeek 维护者需要在新 flag 或新拓扑出现时同步更新 when 条件;cookbook 的 "Submit verified cell" 流程需要补齐三档配方的端到端确认。整体影响面局限于文档与工具链,不影响任何运行时行为。

共享引擎变更外溢 缺少自动化测试 配方未端到端验证 恢复逻辑隐式耦合 env 合并按字符串去重 when 门控粒度不一致 文档与运行时参数漂移

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论