Skip to content

[DCP][Spec] EAGLE/EAGLE3 support for decode context parallelism - #31785

Open
thanhhao98 wants to merge 22 commits into
sgl-project:mainfrom
thanhhao98:htphan/dflash-dcp-tokenspeed
Open

thanhhao98 wants to merge 22 commits into
sgl-project:mainfrom
thanhhao98:htphan/dflash-dcp-tokenspeed

Conversation

@thanhhao98

@thanhhao98 thanhhao98 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Makes EAGLE / EAGLE3 work with decode context parallelism (--dcp-size > 1). Stacked on #21637.

DFlash × DCP already worked (see #33912, _dflash_draft_cell_size); it gets one shared fix here, not new support.

Root cause: the target KV pool is sharded across DCP ranks, but the draft pool is replicated. Draft-side code that reads the DCP topology therefore builds rank-local metadata against a full pool. Every change below follows from that.

Results

8×B200, K2.5-NVFP4 + EAGLE3 (ns2/topk1/ndt3), --dcp-comm-backend a2a --dcp-replicate-q-proj,
jobs 1496709–1496996. Accept length is the correctness gate: the DCP arm must match the off arm,
since a draft chain fed rank-local metadata degenerates after the first token.

config off (ms) DCP=8 (ms) ratio AL off → DCP
50K / cc4 4.96 5.93 0.84× 2.239 → 2.234
128K / cc16 19.58 20.44 0.96× 2.160 → 2.150
128K / cc24 37.66 26.78 1.41× 2.172 → 2.163
256K / cc8 20.63 22.19 0.93× 1.756 → 1.781
256K / cc16 59.51 38.14 1.56× 1.748 → 1.745

GSM8K 0.940–0.960 on every arm. Below the KV-pool edge DCP pays the verify addon for no benefit; past it the off arm loses admission first and DCP wins. Attention FLOPs are conserved under context sharding, so there is no headroom to beat the off arm at shallow overcommit — the ~16% cost at 50K/cc4 is the design point, not a regression.

Tests

test_dcp_draft_guard.py, test_dcp_draft_pool_bounds.py, test_dcp_spec_topk_guard.py — all three cover failures that are otherwise silent: a guard that stops unsharding draft forwards corrupts the chain with no error, an under-budgeted draft pool OOMs at the first large prefill far from its cause, and a mis-scoped topk validator returns wrong tokens rather than raising. The topk tests pin that DSPARK is not gated (is_dflash_family() would pull it in and reject the shipping Kimi-Linear + DSPARK + DCP path).

Registered DCP/spec suites pass on 8×B200, including main's test_kimi_linear_dcp4.py.


CI States

Latest PR Test (Base): ❌ Run #34432196662
Latest PR Test (Extra): ❌ Run #34432196449
Latest PR Test (AMD ROCm 10): ❌ Run #34432196657

@thanhhao98 thanhhao98 changed the title [DCP][Spec] Speculative decoding × decode context parallelism (DFlash/EAGLE) on tokenspeed_mla — stacked on #21637 [DCP][Spec][vibe-coded] Speculative decoding × decode context parallelism (DFlash/EAGLE) on tokenspeed_mla — stacked on #21637 Jul 20, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Decode Context Parallelism (DCP) optimizations, benchmarks, and tests, including a 2-pass target-verify cascade (non-causal prefix fold and local causal draft fold) and an alternative All-to-All communication backend. The review feedback highlights several critical runtime bugs where torch.dtype objects are incorrectly accessed with .itemsize (which will raise an AttributeError in PyTorch) across activation, kernel, benchmark, and test files, suggesting the use of .element_size() instead. Additionally, the reviewer recommends asserting contiguity for tensors in the new all_to_all_single NCCL wrapper to prevent potential silent data corruption.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/sglang/srt/layers/activation.py
Comment thread python/sglang/kernels/ops/attention/dcp_kernels.py Outdated
Comment thread benchmark/kernels/all_reduce/benchmark_symm_mem.py Outdated
Comment thread python/sglang/srt/distributed/device_communicators/pynccl.py
Comment thread test/registered/dcp/test_dcp_draft_pool_bounds.py Outdated
Comment thread test/registered/dcp/test_dcp_mask_pack.py Outdated
Comment thread test/registered/dcp/test_dcp_mask_pack.py Outdated
@thanhhao98
thanhhao98 force-pushed the htphan/dflash-dcp-tokenspeed branch from c93cf54 to 264324e Compare July 20, 2026 14:15
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 20, 2026
@thanhhao98
thanhhao98 force-pushed the htphan/dflash-dcp-tokenspeed branch from 36bc9d2 to a8f6ef5 Compare July 20, 2026 14:35
@thanhhao98 thanhhao98 changed the title [DCP][Spec][vibe-coded] Speculative decoding × decode context parallelism (DFlash/EAGLE) on tokenspeed_mla — stacked on #21637 [DCP][Spec][vibe-coded] Speculative decoding × decode context parallelism (DFlash/EAGLE) — stacked on #21637 Jul 20, 2026
@kpham-sgl kpham-sgl self-assigned this Jul 20, 2026
@thanhhao98

Copy link
Copy Markdown
Contributor Author

Per-GPU aggregate view of the same fix-tip EAGLE3 matrix (complements the per-user table above; tok/s/gpu = aiperf aggregate output throughput ÷ 8; K2.5-NVFP4 + EAGLE3 ns2/topk1/ndt3, DCP=8 with --dcp-comm-backend a2a --dcp-replicate-q-proj, AL parity per the fix, same-day same-commit pairs, jobs 1496709–1496996):

config DCP off (tok/s/gpu) DCP=8 (tok/s/gpu) ratio
50K / cc4 82.2 72.0 0.88×
128K / cc16 77.7 73.8 0.95×
128K / cc24 63.0 84.6 1.34×
256K / cc8 33.3 32.3 0.97×
256K / cc16 20.6 35.5 1.72×

Aggregate ratios sit slightly above the per-user ones (e.g. 0.88 vs 0.84 at 50K/cc4) — run-to-completion aggregates fold in ramp/tail effects; same underlying runs. The regime story is unchanged: below the KV-pool edge DCP pays the verify addon with no capacity benefit; past the edge (overcommit ≥ ~1.0) the off arm loses admission first and DCP wins on both axes.

🤖 Generated with Claude Code

@thanhhao98 thanhhao98 changed the title [DCP][Spec][vibe-coded] Speculative decoding × decode context parallelism (DFlash/EAGLE) — stacked on #21637 [DCP][Spec] Speculative decoding × decode context parallelism (DFlash/EAGLE) — stacked on #21637 Jul 27, 2026
@thanhhao98
thanhhao98 force-pushed the htphan/dflash-dcp-tokenspeed branch 2 times, most recently from 68998b1 to f4583ee Compare August 6, 2026 14:45
@thanhhao98
thanhhao98 force-pushed the htphan/dflash-dcp-tokenspeed branch 4 times, most recently from bda5795 to 82e00b0 Compare August 12, 2026 08:20
@thanhhao98 thanhhao98 changed the title [DCP][Spec] Speculative decoding × decode context parallelism (DFlash/EAGLE) — stacked on #21637 [DCP][Spec] Speculative decoding × decode context parallelism (DFlash/EAGLE) Aug 12, 2026
@thanhhao98
thanhhao98 marked this pull request as ready for review August 12, 2026 08:33
Hao Phan added 2 commits August 12, 2026 22:22
…dget

Both behaviours are load-bearing and both fail quietly, so they get CPU unit tests.

draft_forward_guard is now the ONLY mechanism that unshards a draft forward. Upstream
removed ModelRunner.dcp_size/dcp_rank and made the attention-facing accessors derive from
dcp_enabled (attn_dcp_size = dcp_size if dcp_enabled else 1), so the guard alone drives
attn_dcp_size to 1. If that stops holding, draft forwards silently take DCP branches
against an unsharded pool -- the chain-decode corruption this exists to prevent, with no
error at the point of failure. The test pins size/rank inside and outside the guard,
across nesting, exceptions, and the DCP-off case.

The draft-pool budget under-allocates rather than erroring: the widened draft pool eats
the transient headroom and the server OOMs at the first large prefill, far from the cause.
The test pins the dcp=1 case to the exact upstream ratio, the dcp scaling, monotonicity,
that the draft worker budgets nothing, and -- explicitly -- that an unknown draft depth
yields 0 rather than a guessed floor.
…alidation

Three review findings.

1. SGLANG_DCP_DRAFT_CHAIN_GUARD could produce the corruption it was meant to let us
   study. The graph CAPTURE in _capture_cuda_graphs was guarded unconditionally while
   the chain draft (which replays those graphs) was guarded only when the flag was set,
   so setting it to 0 -- documented as "revert for A/B" -- left capture and replay
   disagreeing about dcp_enabled. That is exactly the capture/replay divergence the
   capture-site comment describes as causing an IMA. The flag is removed and all four
   guard sites are now unconditional. It was introduced in this same PR, so nothing
   released depended on it.

2. _validate_dcp_spec_topk defaulted its algorithm predicates through getattr. Those
   come from SpeculativeAlgorithm, an Enum that always defines is_eagle/is_dflash, so
   the defensiveness bought nothing and would have silently let tree drafts through
   under DCP if a predicate were ever renamed -- the guard failing open on the exact
   configuration it exists to reject. Now called directly. The getattr on dcp_size and
   speculative_eagle_topk is kept and explained: test_spec_registry drives this hook
   with a deliberately minimal SimpleNamespace. The topk comparison is also explicit
   about None rather than relying on `or 1`.

3. prepare_decode_context_parallel_metadata took Optional[torch.Tensor] while the five
   model-side wrappers that forward into it still declared torch.Tensor. Annotations
   propagated so the contract is consistent along the whole call path.

Adds test_dcp_spec_topk_guard.py, which had no coverage at all: rejection for EAGLE and
EAGLE3 tree drafts, acceptance for chain drafts and with DCP off, and -- the scoping most
likely to regress -- that DSPARK is NOT gated, since is_dflash_family() would pull it in
and newly reject the Kimi-Linear + DSPARK + DCP path that already ships.

Also cuts the comments back by two thirds (63 -> 21 lines). What is left is only the
non-obvious why: the is_dflash_family trap, why the predicates must not be getattr, why
banded_depths needs the raw draft depth rather than the DCP-replicated one, and the
capture-must-match-replay invariant. Everything that merely restated the code, or
recorded history that belongs in a commit message, is gone.
@thanhhao98
thanhhao98 force-pushed the htphan/dflash-dcp-tokenspeed branch from 6728a71 to e23befe Compare August 12, 2026 15:23
Comment thread python/sglang/srt/arg_groups/speculative_hook.py Outdated
def verify_mask(self) -> Optional[VerifyMask]:
return self._verify_mask

def update_verify_buffers_to_fill_after_draft(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this no-op function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this func, we will get NotImplementedError in

target_worker.model_runner.attn_backend.update_verify_buffers_to_fill_after_draft(

Comment thread python/sglang/srt/layers/dcp/comm.py Outdated
Comment on lines +50 to +59
def draft_forward_guard(is_draft: bool):
"""Run a draft forward with DCP disabled; no-op when is_draft is False.

The draft KV pool is replicated, not sharded, so every DCP branch must see
dcp_enabled == False for the whole draft forward (this also drives
attn_dcp_size to 1 and attn_dcp_rank to 0).
"""
if not is_draft:
return contextlib.nullcontext()
return get_parallel().override(dcp_enabled=False)

@kpham-sgl kpham-sgl Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered this approach many times but decided to go against it because get_parallel() is a runtime static parallel state. .override() is only meant to be used for unit testing.

Let's brainstorm how to best do this offline

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw for this part, I considered many previous approaches but ultimately rejected them all

#33955
kpham-sgl#3
YAMY1234#5
YAMY1234#6
YAMY1234#7

Comment thread python/sglang/srt/arg_groups/speculative_hook.py Outdated
logger = logging.getLogger(__name__)


def _eagle_draft_layers(kvc: KVCacheConfigurator) -> int:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self note: acceptable currently but might need better approach in the future

def prepare_context_parallel_metadata_for_dcp(
self,
seq_lens: torch.Tensor,
extend_prefix_lens: torch.Tensor,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why make this Optional?

@thanhhao98 thanhhao98 Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed those Optional.

@nvpohanh

Copy link
Copy Markdown
Collaborator

@thanhhao98 could you address the comments and fix the conflicts? thanks!

Hao Phan added 3 commits August 20, 2026 06:52
…enspeed

# Conflicts:
#	python/sglang/srt/model_executor/pool_configurator.py
…comments

Review actions on sgl-project#31785:

1. _validate_dcp_spec_topk -> _validate_dcp_spec, the single entry point for
   DCP x speculative validation (EAGLE / DFlash / DSpark).

2. Remove or shorten the generated-sounding docstrings and comments flagged in
   review; the no-op update_verify_buffers_to_fill_after_draft override now
   matches the bare-pass style of trtllm_mha / triton.
…bing

Target-verify is excluded at the single call site (EagerRunner), so the DCP
chunked-prefix planner never receives None; the planner None-return and the
Optional annotations on the six pass-through signatures were redundant
defenses. Reverts planner.py and the four model files to upstream state.
@thanhhao98

Copy link
Copy Markdown
Contributor Author

/tag-and-rerun-ci

@nvpohanh

Copy link
Copy Markdown
Collaborator

/rerun-failed-ci

Hao Phan added 2 commits August 24, 2026 11:38
…enspeed

# Conflicts:
#	python/sglang/srt/model_executor/pool_configurator.py
STANDALONE inherits the EAGLE V2 tree-draft path, so the chain-only DCP
validation must cover it. The draft-extend CUDA graph was captured and
replayed outside draft_forward_guard (unlike the draft-decode graph and the
eager fallback), so its out-of-graph metadata re-plan saw live DCP state
against the replicated draft pool. Also hoist the guard imports to module
level (layers.dcp has no reverse dependency; forward() is a hot path).
@thanhhao98 thanhhao98 changed the title [DCP][Spec] EAGLE/EAGLE3 support for decode context parallelism — stacked on #21637 [DCP][Spec] EAGLE/EAGLE3 support for decode context parallelism Aug 28, 2026
@nvpohanh

Copy link
Copy Markdown
Collaborator

@thanhhao98 could you fix Lint failure?

@nvpohanh nvpohanh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[by Codex] Three inline review findings.

def _validate_dcp_spec(server_args: ServerArgs) -> None:
if (
server_args.speculative_algorithm is None
or getattr(server_args, "dcp_size", 1) <= 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[by Codex] Severity: style | Confidence: High

Please avoid the new dynamic getattr reads here and at line 197. _validate_dcp_spec is typed to receive ServerArgs, where both dcp_size and speculative_eagle_topk are defined, so read those fields directly and make any lightweight test fixture satisfy that contract explicitly. This also prevents a renamed or missing production field from silently disabling the validation.

model_runner.model, "prepare_context_parallel_metadata_for_dcp"
if (
model_runner.ps.attn_dcp_size > 1
and hasattr(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[by Codex] Severity: style | Confidence: High

Please remove the retained hasattr check from this modified condition. Model capability should be represented explicitly by the model interface/base implementation or by a typed capability flag selected during initialization, then this path can call prepare_context_parallel_metadata_for_dcp structurally. Runtime method probing makes unsupported models silently skip required DCP metadata preparation.


algo = SpeculativeAlgorithm.from_string("EAGLE3")
for name in ("is_eagle", "is_dflash", "is_dspark"):
self.assertTrue(hasattr(algo, name), f"SpeculativeAlgorithm lost {name}()")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[by Codex] Severity: style | Confidence: High

Please avoid hasattr in the new test. Call each required predicate directly (and assert its expected result) so a missing or renamed method raises naturally; that checks the stated contract without dynamic attribute probing.

@nvpohanh

nvpohanh commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@thanhhao98 could you fix lint issue? Thanks

Hao Phan and others added 9 commits September 8, 2026 22:01
isort orders layers.cp.utils before layers.dcp; the branch had them
swapped, which failed the pinned pre-commit hook in CI.
TRTLLMMLABackend gained update_verify_buffers_to_fill_after_draft on main
in sgl-project#33561, so the copy this branch added is a duplicate definition in the
same class. Removing it also retires the review question about the no-op.
attn_dcp_size is now a derived parallel width resolved at publish time
(sgl-project#36790, sgl-project#38113), so it no longer follows an overridden dcp_enabled and a
draft forward kept the target's DCP width. State the widths the guard
means, the way the other production override sites do.
_validate_dcp_spec reads the resolving view instead of defaulting the two
ServerArgs fields through getattr; the view also answers with the topk the
speculative hook itself resolves, which a raw field read now misses since
declarations stopped writing back to the record (sgl-project#36253). The spec-registry
fixture states the two fields the validator reads.

The eager DCP-metadata branch selects on a runtime_checkable
SupportsDecodeContextParallelMetadata protocol rather than probing the model
for the method name. The branch stays: a non-MLA DCP target (triton backend)
carries no attn_dcp_metadata and must keep skipping it.
register_cpu_ci takes the (stage, runner_config) pair or a suite, never a
bare stage: the three files were the only ones in the tree that failed to
parse, and collect_tests aborts every CI suite job on the first one. With
the registration valid the taxonomy gate applies, so they move under
test/registered/unit/dcp/.
The suite fed _validate_dcp_spec an already-final topk, so nothing turned
red when the guard read the record instead of the resolving view - the
state the speculative hook is actually in once it has declared an
auto-chosen topk.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants