Skip to content

[Refactor] Enter draft TP scopes by attention ownership and drop ModelRunner.tp_group - #41817

Merged
ch-wan merged 2 commits into
mainfrom
cheng/refactor/spec-draft-scope
Sep 30, 2026
Merged

ch-wan merged 2 commits into
mainfrom
cheng/refactor/spec-draft-scope

Conversation

@ch-wan

@ch-wan ch-wan commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

This PR is part of a stack (oldest at bottom):

Motivation

Each speculative worker states three times whether its draft owns attention:

  • self.draft_owns_attention;
  • self.draft_tp_context, which is draft_tp_context under DP attention and a no-op otherwise;
  • the TP group every call site passes in, taken off the draft runner.

That last one is the only reason ModelRunner.tp_group has to be a snapshot that outlives the scope the runner is built in.

Modifications

draft_tp_context(owns_attention). It now takes only owns_attention.

  • With it, the scope installs the target's attention-TP group, which is the group an attention-owning draft is built on.
  • Without it, there is nothing to enter.

The 28 scopes in the EAGLE, DFLASH, frozen-KV MTP, multi-layer EAGLE, Standalone and DSpark workers call it with their ownership flag.

Why this installs the same group as before:

  • The scope is always entered from outside any draft scope; patch_tensor_parallel_group does not nest. There, get_parallel().attn_tp_group is the group the owning draft was built on.
  • For a draft that does not own attention under DP attention, the old call re-installed the TP group that was already installed. That no-op scope is gone.
  • DSpark's two scopes already had this shape and now use the same helper.
  • The Standalone worker's attention-backend and CUDA-graph overrides only re-entered scopes that its base class already enters, so they are removed.

The old two-argument form of draft_tp_context is gone. A caller that still passes a group gets a TypeError rather than a different scope.

ModelRunner.tp_group is removed. With drafts entering their scope from their ownership flag, every remaining reader runs while the runner is built, during a graph capture or replay, during a forward, or only on the target. They read get_parallel().tp_group, which in those scopes is the group the runner copied:

  • graph-runner barriers;
  • the CUDA-graph backends (full, breakable, piecewise, NPU, XPU), which keep the group they capture with;
  • the FlashInfer autotune sync group;
  • patch_model;
  • the elastic EP joiner and EP-join paths;
  • the PP + speculative seed broadcast in TpModelWorker.

ModelRunner keeps only its PP group. Code outside the tree that reads model_runner.tp_group should read get_parallel().tp_group in the scope it runs in.

Two readers can run on a draft outside its scope:

  • The HiSparse coordinator, built with the pools, reads the TP group only without DP attention. There no draft owns attention, and a draft's TP group is the target's.
  • Recapturing graphs after a weight update (recapture_cuda_graph=True) runs with no draft scope entered. The draft forward there already issues its collectives on the target's TP group. Its capture barriers now use that group too, and every rank of the group runs the recapture.

The DFLASH and DSpark rank-0 gates that run inside the draft scope used to ask the target runner's TP group for the target's rank; inside the scope, the context answers the draft's. Those workers now record the target's TP rank in their constructors, outside any draft scope.

Tests.

  • test_draft_tp_context.py covers both ownership cases.
  • The speculative attention test kits state their TP group on the context.
  • The full CUDA-graph backend, DFLASH sampler, FlashInfer autotune and DP speculative-prefill tests no longer fake a runner's TP group.

Accuracy Tests

H200, Qwen3-8B, --tp-size 2, 4 greedy prompts × 64 tokens and GSM8K (200 questions), compared with main at the top of this stack:

draft config greedy text + spec_verify_ct GSM8K main / this stack
EAGLE3 (AngelSlim/Qwen3-8B_eagle3) TP2 identical 0.955 / 0.955
EAGLE3 TP2 + DP attention (attention-owning draft) identical 0.945 / 0.955
Standalone (Qwen/Qwen3-0.6B) TP2 text identical 0.950 / 0.955

For the Standalone draft, one prompt's spec_verify_ct alternates between 20 and 21 across repeated runs of main itself, and the same happens on every layer of this stack.

test/registered/unit at this PR's head, compared with main: no new failures.

Speed Tests and Profiling

Not applicable.

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): 🚫 Run #36772917062
Latest PR Test (Extra): 🚫 Run #36772916590
Latest PR Test (AMD ROCm 10): ❌ Run #36772917133

@ch-wan
ch-wan force-pushed the cheng/refactor/make-pp-layers branch from ebb248e to be4156e Compare September 30, 2026 04:24
@ch-wan
ch-wan force-pushed the cheng/refactor/spec-draft-scope branch from 5353ac8 to 215ad7b Compare September 30, 2026 04:25
@ch-wan
ch-wan force-pushed the cheng/refactor/make-pp-layers branch from be4156e to e92827d Compare September 30, 2026 05:06
@ch-wan
ch-wan force-pushed the cheng/refactor/spec-draft-scope branch from 215ad7b to 233eec5 Compare September 30, 2026 05:06
@ch-wan
ch-wan force-pushed the cheng/refactor/make-pp-layers branch from e92827d to cf8b583 Compare September 30, 2026 20:28
Base automatically changed from cheng/refactor/make-pp-layers to main September 30, 2026 20:28
Each speculative worker said whether its draft owns attention three
times: `self.draft_owns_attention`, `self.draft_tp_context` (the real
scope under DP attention, else a no-op), and the TP group every call
site passed in, taken off the draft runner. `draft_tp_context` now takes
only `owns_attention`: with it, the scope installs the target's
attention-TP group, the group an owning draft is built on; without it,
there is nothing to enter.

The scope is always entered from outside a draft scope, where
`get_parallel().attn_tp_group` is the group the owning draft was built
on, so every scope installs the same group as before. For drafts that do
not own attention under DP attention, the old call patched the TP group
with the group already installed; that no-op scope is gone. DSpark's two
scopes already had this shape and now use the same helper. The
Standalone worker's attention-backend and CUDA-graph overrides only
re-entered scopes its base class already enters, so they are gone.

A unit test covers both cases.
With drafts entering their scope through
`draft_tp_context(owns_attention)`, nothing reads a runner's TP group to
enter a scope any more. Every remaining reader runs while the runner is
built, during a graph capture or replay, during a forward, or only on
the target: graph-runner barriers, the CUDA-graph backends (full,
breakable, piecewise, NPU, XPU) that keep the group they capture with,
the FlashInfer autotune sync group, `patch_model`, the elastic joiner
and EP-join paths, and the PP+spec seed broadcast in `TpModelWorker`.
They read `get_parallel().tp_group`, which in those scopes is the group
the runner copied. `ModelRunner` now keeps only its PP group.

Two readers can run on a draft outside its scope:

- the HiSparse coordinator, built with the pools, reads the TP group
  only without DP attention, where no draft owns attention and a draft's
  TP group is the target's;
- recapturing graphs after a weight update (`recapture_cuda_graph=True`)
  runs with no draft scope entered. The draft forward there already
  issues its collectives on the target's TP group; its capture barriers
  now use that group too, and every rank of it runs the recapture.

The DFLASH and DSpark rank-0 gates that run inside the draft scope asked
the target runner's TP group for the target's rank; the context there
answers the draft's. Those workers now record the target's TP rank in
their constructors, outside any draft scope, and read it there.

Tests that faked a runner's TP group state it on the context instead:
the speculative attention test kits, the full CUDA-graph backend test,
the DFLASH sampler test, and the FlashInfer autotune cache-phase test.
@ch-wan
ch-wan force-pushed the cheng/refactor/spec-draft-scope branch from 233eec5 to 2a1a9b6 Compare September 30, 2026 20:28
@ch-wan
ch-wan merged commit 986cb82 into main Sep 30, 2026
2 checks passed
@ch-wan
ch-wan deleted the cheng/refactor/spec-draft-scope branch September 30, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant