SGLang · 技术汇报 · 2026-08 · 姊妹篇

PP × 组合场景

从主报告拆出的组合场景专辑:TP / DP-attention / CP / EP 与流水线并行的组合机制、 chunked prefill 动态分块、PD 分离 × Mooncake、EAGLE 推测解码及其传递链。

姊妹 deck · 主报告:pp-hicache-l3-slides.htm 文中「主 deck 第 N 页」= 主报告页码

/ 空格 下一页 · 上一页 · ← 返回主 deck

本页引用 — 总纲 issue #22607 · 代码基线 sglang/srt(行号基于 2026-08 基线,链接指向 main)

组合场景 · 01

DPA + PD + PP + Mooncake + EAGLE:先说结论

五件套叠不进同一个实例:server_args.py:6832 硬断言 pp_size > 1 → speculative_algorithm is None 且 decode 端要跑 EAGLE,prefill 端必须同配 EAGLE(才会产出 hidden states / topk 给 decode 起 draft)。 所以现实形态只有两种:A = PP-prefill + 普通 decode(无 EAGLE);B = TP/DPA-prefill(+EAGLE 采集) + EAGLE-decode(无 PP)。下图画的是形态 B。
LB / Router PD-prefill 默认 follow_bootstrap_room:room % dp_size Prefill 实例(EAGLE 同配,用于采集) attn-DP rank 0 attention 只算本组 token MoE/MLP 全局 gather bootstrap_queue → prefill → inflight_queue cuda graph 强制关闭 attn-DP rank 1 …同左 若走形态 A:此处可换成 PP0→PP1 stage 栈(无 EAGLE) Decode 实例(EAGLE,无 PP) EAGLE Worker draft + target 共享 KV allocator/pool prealloc_queue → transfer_queue → running (× dp ranks) radix cache 默认关 (chunk cache 退化) decode-radix 与 spec 互斥(hook:41) KV 平面:mooncake batch_transfer_sync(RDMA,按 page 分块) aux 平面:MetadataBuffers(首 token / logprobs / topk / hidden_states) Bootstrap server(prefill 侧 HTTP):room 注册与两端握手
图 15 · 形态 B 的部署拓扑。KV 与元数据走两个独立平面:KV 页走 RDMA 批传,spec 起步所需的 hidden states / topk 走 aux buffer(第 9 页展开)。DPA 与 PD、Mooncake、EAGLE 均可叠加;PP 只能出现在放弃 EAGLE 的实例里。
组合结论依据
PP × 任意 speculative❌ 硬断言互斥server_args.py:6832(PP 同时强制关 overlap schedule)
EAGLE × DP attention✅ 允许可加 --speculative-skip-dp-mlp-sync;STANDALONE / DFLASH / NGRAM × DPA 均 ❌
PD-decode radix cache × spec❌ 互斥pd_disaggregation_hook.py:41;decode 默认本就关 radix(chunk cache)
PD-prefill强制 disable_cuda_graphpd_disaggregation_hook.py:78
DP attention × PP✅ 允许DPController 按 pp_rank × tp_rank 双重循环拉起(data_parallel_controller.py:481
TP × PP✅ 基础组合三个通信平面正交(第 3 页);pp>1 强制关 overlap schedule(server_args.py:6832
CP × PP✅ 互斥已解除assert pp_size==1 已删(server_args.py:5166 起仅整除检查);DSA prefill CP 专门适配了 PP 传输(第 5 页)
EP × PP✅ 无互斥断言EP 只活在每层 MoE 内部,与 PP 互不知晓(第 5 页);moe_dp>1 时例外(server_args.py:5187

本页引用 — 源码:data_parallel_controller.py · server_args.py(行号基于 2026-08 基线,链接指向 main)

组合场景 · 02

TP × PP:切宽 × 切深,三个通信平面

TP 把每一层切「宽」(head / 矩阵列),PP 把模型切「深」(层段)。world = pp × tp,每个 (pp_rank, tp_rank) 都是一个独立 Scheduler 进程(engine.py:618 双重循环拉起)——两把刀维度正交,靠三个各司其职的通信平面粘合。

① 请求平面 · gloo CPU 组 · 每调度步一次 ZMQ 入口 pp0 · tp0 pp0 · tp1-3 pp1 · tp0 pp1 · tp1-3 broadcast_pyobj p2p 仅 tp0↔tp0 broadcast_pyobj leader = pp0 ∧ attn_tp0 ∧ cp0 scheduler.py:629 全网格同一请求序列 ② hidden 平面 · NCCL P2P stage i · tp0-3 stage i+1 · tp0-3 每 rank 只发 1/tp 切片:reshape(tp, -1)[rank] 收端 attn_tp all_gather 复原(parallel_state.py:1479 / 1530) 4 条并行管道 同 tp_rank 对同 tp_rank, PP 带宽 ÷ tp ③ 层内平面 · NCCL 设备组 · 每层 ×2(最高频) attention → o_proj 后 all_reduce MLP → down_proj 后 all_reduce 只存在于 stage 内部,PP 边界零参与; 这也是 TP 必须留在 NVLink 域内的原因
图 15b · TP × PP 的三个通信平面(PP2 × TP4 为例)。频率与量级相反:③ 每层两次、但每次只有激活大小(微秒级);② 每 microbatch 一次、GB 级但被切成 1/tp(发切片、收端 all_gather 是 send_tensor_dict 的带宽优化);① 每调度步一次、纯 CPU 消息。三者互不阻塞——① 走 gloo,②③ 走 NCCL 设备流。请求经「ZMQ → TP 广播 → PP 点对点接力 → 再 TP 广播」三级分发:PP 间只有 attn_tp0 互相 relay,到站后组内广播(scheduler_pp_mixin.py:2141)。

KV cache:字节分家,元数据必须同构

  • MHAtoken_to_kv_pool 每 rank 只存 num_heads/tp 个 head 的 K/V——同一 token 的字节散在 tp 个 rank 上,缺一个 rank 数据即不完整;MLA:latent 每 rank 全量相同 → L3 只让 tp0 写(主 deck 第 4 页卡片 ③)。
  • radix 树 / req_to_token:每 rank 一棵/一份,树形与 slot 编号必须跨 tp × pp 全员同构(主 deck 第 23 页)——slot 编号即「第 i 个 token」,各 rank 用同一编号找到自己那份字节。
  • 推论:HiCache 的元数据共识要覆盖整个网格——prefetch 命中长度在 _create_sync_groups 建的 attn_cp / attn_tp / tp / pp 各同步组上逐组 all_reduce(MIN)cache_controller.py:1141),PP 维靠 pp_sync / MIN 投票(主 deck 第 13 / 16 页)。

一致性原语与两种失效模式

  • 采样一致:last stage 各 tp rank 各自采样,靠「同输入 + 同 seed」确定性;grammar 或 SYNC_TOKEN_IDS_ACROSS_TP 时改为 all_reduce(MIN) 硬对齐 token id(sampler.py:382;DPA 下同步组换成 attn_tp 组)。
  • 失效模式不对称(主 deck 第 11 页):TP 错位 = 两个不同 collective 相遇 → 立即挂死(形态 C,#30760);PP 错位 = proxy tensor 行数不符 → 迟发 shape mismatch(形态 A)。TP 是快刀,PP 是慢性病。
  • 组合约束:pp > 1 强制关 overlap schedule、禁 speculative(server_args.py:6832);chunked prefill 不但允许,还有 PP 专属的动态分块机制(第 6 页)。

本页引用 — PR / issue:#30760 | 源码:cache_controller.py · engine.py · parallel_state.py · sampler.py · scheduler.py · scheduler_pp_mixin.py(行号基于 2026-08 基线,链接指向 main)

组合场景 · 03

DP attention:attention 各算各的,MoE 合起来算

attn-DP 0 · Attention 只算本组请求的 token(KV 各自独立) attn-DP 1 · Attention 只算本组请求的 token 全局 MLP / MoE 对 concat 后的全局 batch 计算(expert 均衡靠大 batch) dp_gather_partial(all_gatherv / padding all_gather) dp_scatter / reduce_scatterv(回到各组,进下一层 attention) dp0 下一层输入 dp1 下一层输入
图 16 · 一层 transformer 里的 gather / scatter。attention 段每 dp 组独立(各组 batch 不同);进 MoE 前 gather 成全局 batch,出来再 scatter 回去。gather 的形状依赖 global_num_tokens——这就是调度侧每步都要做 MLP sync 的原因。(layers/dp_attention.py:596 / communicator.py:1078)
# 调度侧每步的全局对齐(dp_attn.py:143)
local = num_tokens(batch)      # decode=bs, extend=Σextend, 空=0
MLPSyncBatchInfo.all_gather()  # 一次拿全局 num_tokens/mode
if max(global_num_tokens) > 0 and 本 rank 没活:
    batch = get_idle_batch()   # 必须发起 idle batch!否则 MoE 的
                               # all-gather 缺席 → 全员挂死
  • rank 布局attn_tp_size = tp_size / dp_size / cp_size,layout = (dp, cp, tp),tp 变化最快(dp_attention.py:245)。
  • 收请求:每 dp 组的 attn-tp-rank0 各收各的;工作请求只在组内 broadcast,控制消息全 tp 组 broadcast(request_receiver.py:246)。
  • 参数副作用:开 DPA 后 schedule_conservativeness ×0.3chunked_prefill_size ÷= dp_sizeserver_args.py:5207)。
  • overlap 决策要用全局量:spec+DPA 下用 all_gather 出的 is_extend_in_batch 而非本地 mode,否则各组决策分叉 → 死锁(scheduler.py:1753)——与 Part 2 同款「本地状态→全局决策」定律。

本页引用 — 源码:dp_attention.py · scheduler.py · server_args.py(行号基于 2026-08 基线,链接指向 main)

组合场景 · 04

CP × PP:互斥已解除;EP × PP:天然正交

各并行维度与 PP 的关系分三档:TP / DPA / EP 直接叠(切的维度不相交),speculative / PD-Multiplexing 被断言禁止,CP 则走完了「从 assert pp_size == 1 到打通」的全程——最新代码已移除互斥断言,并为 DSA prefill CP 适配了 PP 传输层。本页讲 CP 切了什么、怎么打通,以及 EP 为何对 PP 完全无感。

prefill 输入序列(token_idx % cp_size 交错分配,cp=2 示意) t0 t1 t2 t3 t4 t5 t6 t7 attn-CP rank 0 Q = t0 t2 t4 t6 partial attn:本 rank 的 Q × 全量 KV (causal:各 token 看各自前缀) attn-CP rank 1 Q = t1 t3 t5 t7 partial attn:本 rank 的 Q × 全量 KV attn_cp all_gather transpose 复原 t0…t7 顺序 完整 hidden → MoE / 下一层 dsa/communicator.py KV 存储不切:DSA/MLA 的 latent KV 每个 rank 全量都有——CP 切的是计算(Q 的 token 维),不是 KV 池,radix / HiCache 元数据对 CP 无感 dsa/utils.py:115 dsa_cp_round_robin_split_data · server_args.py:3706 起启用条件与断言
图 16b · DSA CP interleave 切分(旧名 round-robin-split)一层里的数据流(cp=2 示意)。为什么交错切而不是连续切段:causal mask 下 token 越靠后、要扫的前缀越长——连续切段会把最贵的尾段全塞给最后一个 rank;token_idx % cp_size 让每个 rank 摊到长短均匀的一篮子。输出经 all_gather + transpose 复原全序列,CP 的存在对上下游算子完全透明。

CP 切的是什么:序列维,且只切计算

  • 从 TP 里再分一刀attn_tp_size = tp / dp / cp,rank layout (dp, cp, tp)(dp_attention.py:245)——CP 组内各 rank 持相同权重,各算一部分 token;DSA CP 下要求 attn_tp_size == 1(o_proj 的部分和不做 attn-TP all-reduce)。
  • 两种策略cp_strategyserver_args.py:3712):interleave(上图,token_idx % cp_size,要求 dp_size==1;旧名 round-robin-split)与 zigzag(要求 DPA + DeepEP + ep=tp)。
  • 现状边界:仅 prefill、仅 DeepSeek V3.2 DSA、Hopper 实验性、跨机有精度问题 → 限单机 tp_size ≤ 8server_args.py:3723);PD 分离下只在 prefill 侧。

CP × PP:互斥已解除,靠传输层适配

旧版曾对通用 attn_cp 直接 assert pp_size == 1;最新代码已移除这条断言(server_args.py:5166 起只剩整除与 fusion 检查),仅 moe_dp_size > 1 仍禁 PP(:5187)。

DSA prefill CP 与 PP 同开还做了专门适配(#16380 引入,后随 NSA→DSA 更名):CP 下 attention 权重各 rank 复制、hidden 全量,PP 边界因此跳过第 3 页的「切 1/tp + all_gather」、proxy tensor 全量直发(require_attn_tp_allgather=Falsescheduler_pp_mixin.py:1744)。

能打通的原因正是主 deck 第 9 页判据:CP 切分纯在算子内部、不给 ③ 组批引入 per-rank 输入——所以只需改传输层,不需要新共识。

EP × PP:天然正交,零断言

  • EP 切 MoE 的 expert 维moe_tp_size = tp / ep / moe_dpparallel_state.py:2198 起),expert 权重按 rank 分桶,token 靠 all-to-all(DeepEP)去找 expert、算完回来。
  • 全程发生在每层 MoE 内部:PP 边界传的仍是 hidden states,EP 与 PP 互不知晓——代码里没有任何 ep × pp 互斥断言。
  • 对 KV / HiCache 零影响:EP 不碰 attention,KV 字节与元数据与纯 TP 完全相同。约束都在 TP 侧:ep × moe_dp ≤ tp,且 ep > 1 时须 == tpserver_args.py:5185)。
判别口诀(与 PP 能不能叠):看两条——① 它切的维度是否与「层」相交;② 它是否给 ③ 组批引入新的 per-rank 输入。TP / EP / DPA 两条都不沾,✅ 直接叠;HiCache 维度不沾但引入per-rank 输入,✅ 但共识修了半年(主 deck Part 3);speculative 要改采样与 KV 计量双时序,❌ 硬互斥(server_args.py:6832);CP 正因为两条都不沾,互斥断言已被移除、DSA 路靠传输层适配打通(本页),只有 moe_dp>1 的组合仍 ❌(:5187)。

本页引用 — PR / issue:#16380 | 源码:dp_attention.py · parallel_state.py · scheduler_pp_mixin.py · server_args.py(行号基于 2026-08 基线,链接指向 main)

组合场景 · 05

chunked prefill × PP:动态分块(--enable-dynamic-chunking)

chunked prefill 与 PP 相性天生不好:同样 token 数的 chunk,history 越长 attention 越贵,而 PP 的 microbatch 流水在 ★ 同步点锁步——最慢的 chunk 拖住整环。最新修复给 PP 配了专属方案:把「等 token 数」换成「等耗时」,且延迟这个 per-rank 量只在启动阶段进入系统一次。

读图:条宽 = 实际耗时(不是 token 数);条内 = 本片 token 数 + 要扫的前缀长度。示例 base = 8k。 等 token 数(固定 chunk)——为什么越算越慢 8k tok 扫前缀 0 8k tok 扫前缀 8k 8k tok 扫前缀 16k 8k tok 扫前缀 24k T 1.35 T 1.7 T 2.1 T ← 最慢,整环等它 每个新 token 都要对全部前缀做 attention → 同样 8k token 越来越贵;红段 = 超出 T 的溢出——流水在 ④⑥ ★ 锁步,这段时间全环陪等(主 deck 第 7 页) 等耗时(动态 chunk)——固定耗时,反解 token 数 8k tok 扫前缀 0 6.6k tok 扫前缀 8k 5.6k tok 扫前缀 14.6k 4.9k tok 扫前缀 20.2k token 递减, 每片 ≈ T,无溢出 前缀照样越来越长——但每片装的新 token 变少,两者相抵,耗时拉平(token 数为示意值) ① 启动:PP0 实测拟合 128 个递减 chunk 逐个前向计时 f(l) = a·l² + b·l + c(累计耗时曲线) ② 定目标 T T = f(base) − f(0) = 第一片(无前缀)的耗时 ③ 运行时逐片解方程 f(L+x) − f(L) = T → 本片装 x 个 token L = 当前已算前缀长度 数值防线:下限 base/4 · 对齐 max(page, 64) · 平滑 0.75 · 上限 context − L − 100 · 画像失败 → 回退固定 chunk
图 9b · 为什么要动态分块,以及它怎么算。上排:固定每片 8k token,但每个新 token 都要对全部前缀做 attention——前缀 0→24k,同样的 8k token 耗时 T→2.1T;红色溢出段就是 PP 全环陪等的气泡(TP-only 无人等你,所以这是 PP 专属问题)。下排:反过来固定每片耗时 = T,用启动时拟合的耗时曲线反解每片该装多少 token(8k→4.9k 递减)。底部三步:实测拟合 → 定 T → 逐片解方程——per-rank 量只在 ① 进入系统一次,之后全是纯函数(左卡展开)。

per-rank 量只进入系统一次:画像 → 广播 → 纯函数

  • PP0 单点画像scheduler_pp_mixin.py:1776):启动时构造 128 个 token 数递减的 dummy 请求逐个前向实测延迟;DPA 下还须填 global_num_tokens(本 dp 组 = 长度、其余 0)发起 idle batch 参与 MLP sync(:1831#17339)——呼应第 4 页的「必须发起 idle batch」。
  • 广播原始样本:先 attn_tp 组内 broadcast,再 pp_group.broadcast_object_list(src=0):1930)——发的是 (seq_lens, latencies) 原始数据,不是各 rank 自己测。
  • 全员同拟合:每个 rank 用同一份数据跑同一个确定性 lstsq:2654;丢弃首样本防无 warmup 偏差 #17198,样本 < 8 或 a ≤ 0 拒绝拟合)。此后 predict(history_len) 是纯函数,history_len 本受请求流契约保护——全网格解出同一个 chunk size,运行时零通信
  • 失败安全:画像抛异常 → 告警并退回固定 chunk(scheduler.py:1058);预测无解返 None → 用 base。

#27285 / #27010 同款范式(主 deck 第 13 / 16 页):不对齐 IO、只对齐决策输入——延迟这个 per-rank 量在启动时被采样一次,随即转为协议数据。

落点与修复沉淀

  • 只作用于续 chunk:组批时仅当 chunked_req 非空才预测(scheduler.py:3130)——首 chunk 用 base,后续按 history 递减;prefill buffer 按 1.25×base 预留探测余量(server_args.py:5368)。
  • 修复沉淀#15372 采样 32→128 点、对齐下限 page→max(page, 64);#16140 chunk 下限 base/4——防长 history 下解出微小 chunk、固定开销反超收益;#17198 拟合丢首样本;#17339 DPA 画像参与 collective。平滑系数 SGLANG_DYNAMIC_CHUNKING_SMOOTH_FACTOR=0.75 防尺寸骤降。
  • centralized 方案再进一步:本地 plan-driven 分支里,chunk 尺寸随 extend_lenschunked_rid 编入 PPPrefillPlan 由 PP0 统一下发,其余 rank build_batch_from_plan 镜像 chunked_req 指针(scheduler_pp_mixin.py:143/794)——从「全员同构地各自算」升级为「单点算、协议发」。

chunk 与缓存的交界不变:每片结束仍走 cache_unfinished_req 入树 + 锁移交(主 deck 第 8 / 22 页),动态的只是「下一片切多长」。

长 prompt 的折中(为什么有 base/4 下限):token 单价随位置线性上涨(f′(l) = 2a·l + b),纯解方程时 x ≈ T/(2aL) 按 1/L 收缩——超长 history 下 chunk 会掉到几百 token,片数暴涨,每片的固定开销(恰是 T 刻意排除的常数 c:调度一步、kernel 发射、PP 边界一次 P2P)摊不薄,吞吐反而下降。两道闸在数学上殊途同归:平滑 0.75 的极限值 = 0.25·base;硬下限 = base/4(#16140)——超长 prompt 下 chunk 钉在 base/4,「等耗时」不再严格成立、气泡部分回归,但有界。三选一:固定 chunk = 气泡无界 ❌;纯解方程 = 片数与固定开销无界 ❌;解方程 + base/4 下限 = 气泡有界 + 片数有界 ✓。
一句话:动态分块示范了在 PP 上引入per-rank 输入的「正确姿势」——采样单点化、数据协议化、决策纯函数化。HiCache 的教训(Part 2–3)与它是同一问题的反例与正解。

本页引用 — PR / issue:#15372 · #16140 · #17198 · #17339 · #27010 · #27285 | 源码:scheduler.py · scheduler_pp_mixin.py · server_args.py(行号基于 2026-08 基线,链接指向 main)

组合场景 · 06

PD 分离 × Mooncake:KVPoll 五态与两个数据平面

Prefill · KVSender Decode · KVReceiver Bootstrapping WaitingForInput Transferring Success Bootstrapping WaitingForInput Transferring Success ① receiver 经 bootstrap server 注册 room、握手 ② decode 预分配后 send_metadata(kv_indices, aux_index, decode_prefix_len) ③ KV 平面:transfer_worker batch_transfer_sync(RDMA) 分块传 KV page;前缀页可与 forward 重叠提前发 ④ 末片:set_buf → aux 平面 (含 spec hidden/topk) ⑤ 状态回写: decode 提交请求 → PREBUILT batch → 直接进 decode → Failed 的四条路 bootstrap/waiting 超时 · session 探活失败 · 心跳发现 节点挂 · decode abort 握手
图 17 · 一次 KV 交接的五步。方向值得注意:是 decode 端先分配、把目标 kv_indices 告诉 prefill,prefill 才照址写入(RDMA write)。prefill 端队列:bootstrap_queue → 跑批 → inflight_queue(等传完才释放 KV);decode 端:prealloc_queue → transfer_queue → waiting_queue。(disaggregation/prefill.py / decode.py / mooncake/conn.py:1574 transfer_worker)

与 HiCache 的联动即主 deck 第 20 页那条链:decode 端也可开 L3 prefetch(#26227),abort 时走 _clean_hicache_prefetch_resources;PP+PD 下 bootstrap/release 决策由共识统一(#31869 / 本地 centralized 方案)。

本页引用 — PR / issue:#26227 · #31869(行号基于 2026-08 基线,链接指向 main)

组合场景 · 07

EAGLE:draft–verify 循环,与 target 共享 KV 池

① draft 小模型 k 步自回归, 每步 top-k 分叉成树 EagleDraftWorker.draft ② 建树 build_tree_kernel tree_mask / positions / retrieve_index 直写 buffer ③ verify target 单次前向 (is_verify:跳过 sample) eagle_sample → accept ④ 接受 + bonus 取最长被接受路径, 追加 1 个 bonus token new_seq_lens += accept ⑤ draft_extend:把接受结果喂回 draft 模型暖 KV,进入下一轮 一轮接受 n 个 token(1 ≤ n ≤ k+1)——decode 吞吐提升的来源;被拒的 draft KV 在请求结束时按 [committed, allocated) 区间整体释放
图 18 · 一轮 EAGLE 解码。draft 与 target 共享 req_to_token_pool 与 KV allocator(draft 只多出自己那几层的 KV);被接受路径的 KV slot 会被压实到 block 前端(_finalize_accept_tree_path,topk>1 时最微妙的一步)。(speculative/eagle_worker_v2.py:1074 双分支)
  • 与 radix cache 的交互:EAGLE 下 RadixKey 全部转 bigram 视图——draft KV 绑定 (prev, cur) token 对,普通 unigram 前缀语义会错配(radix_cache.py:139)。
  • 超额分配:每轮按预留量分配而非 +1(kv_allocated_len 先行,kv_committed_len 只随接受推进);结束时 pop_overallocated_kv_cache() 释放被拒段。
  • retract 限制:spec 下只能从队尾踢请求(filter API 限制,schedule_batch.py:2511)。
  • req_to_token 行宽为 draft 预留 4 + max_draft_tokens 列(common.py:253)。
# prefill 分支顺带为 decode 备好 draft 起点(:1074)
if batch.forward_mode.is_extend():
    batch.capture_hidden_mode = FULL  # target 吐全量 hidden
    out = target_worker.forward_batch_generation(batch)
    draft_worker._draft_extend_for_prefill(...)
else:
    verify_input = draft_worker.draft(batch)
    out = self.verify(batch)          # 含 eagle_sample
    draft_worker._draft_extend_for_decode(...)

本页引用 — 源码:common.py · radix_cache.py · schedule_batch.py(行号基于 2026-08 基线,链接指向 main)

组合场景 · 08

PD + EAGLE:hidden states 的三跳旅程

decode 端的第一轮 draft 需要 target 模型在 prefill 时的 hidden states 和 top-k 分布——这些不走 KV 平面,而是搭 aux buffer 的便车。

跳 1 · Prefill 采集 process_batch_result_disagg_prefill req.output_topk_p / _index req.hidden_states_tensor ← batch.spec_info(.cpu().clone()) 跳 2 · aux buffer 装车 MetadataBuffers.set_buf(末片时) 10 个定长 buffer:output_ids、logprobs、 topk_p[≤16] · topk_index · hidden_states → send_aux(RDMA)/ send_aux_tcp(ZMQ) 跳 3 · Decode 重建 _commit_transfer_to_req 取回三件套 build_eagle_disagg_draft_input → EagleDraftInput(topk, hidden, bonus_tokens=首 token) PREBUILT batch → 第一轮 draft 起跑 要点:aux buffer 始终分配并注册这 10 个指针(非 spec 时内容为 0)—— 所以「prefill 不开 EAGLE + decode 开 EAGLE」不会报错,只会让 decode 拿到全 0 的 hidden states,draft 从第一步就在胡猜。两端 spec 配置必须一致。
图 19 · spec 状态的搬运不占 KV 平面。注释原话:「use tensor instead of list to transfer hidden_states when PD + MTP」。decode 端 bootstrap_room 会随 buffer 一起校验,防 aux slot 撞车。(prefill.py:658 / utils.py:395 / decode.py:1562 / eagle_disaggregation.py:17)
串起来看一次完整请求(形态 B):Router 按 room 选 dp rank → Prefill(DPA 组内 attention、全局 MoE;HiCache 可命中 L1/L2/L3 前缀)→ chunked prefill 边算边把前缀页 RDMA 给 decode → 末片写 aux(首 token + hidden/topk)→ Decode prealloc 好的 kv_indices 接收 → PREBUILT 入批 → EAGLE draft–verify 循环吐 token → 结束时 cache_finished_req 入树、释放超额 draft KV。abort 在任何一跳发生,都走主 deck 第 18–19 页的路径。

本页引用 — 总纲 issue #22607 · 代码基线 sglang/srt(行号基于 2026-08 基线,链接指向 main)