Skip to content

[Feat] [AMD] monokernels: Serve a draft spec's own steps, and skip the DSA replay metadata they never read - #47102

Open
heslami wants to merge 4 commits into
sgl-project:mainfrom
MarloweAI:up/mono-draft-step-modes
Open

heslami wants to merge 4 commits into
sgl-project:mainfrom
MarloweAI:up/mono-draft-step-modes

Conversation

@heslami

@heslami heslami commented Oct 28, 2026 •

Copy link
Copy Markdown

Motivation

An AMD monokernel spec today serves the target's decode or verify step. An MTP draft runs other steps: EAGLE's draft decode (1 token per request) and draft extend (N tokens). So a draft spec has to wrap the model's forward itself, and SGLang neither checks its graphs nor knows which of its steps are routed.

On routed steps the DSA backend also rebuilds metadata on every replay that a monokernel forward never reads: lengths, page tables, the indexer schedule and the kpool write plan. This happens on the verify step, the draft extend and every draft decode step.

The unread-metadata contract (#44601) needs every served key to be routed. Some kernels can't serve every width at every TP size; GLM-5.3-Flash's 8-row sparse attention doesn't fit LDS at TP2, for example. Those specs fall back to the model's forward inside their own, and the backend would then skip metadata that the fallback reads.

Modifications

  • kernel_plugins/amd/spec.py:
    • MonoSpec.step_modes(model): the (forward mode, tokens per request) steps a draft spec serves, e.g. [("decode", 1), ("draft_extend", 4)]. A draft spec returning none keeps the normal forward. ShapeKey.forward_mode gains "draft_extend".
    • DeviceLimits.max_rows: TP size → the most rows a step is routed with, when below widths. Wider steps run the model's forward. Every served key is then a routed key.
    • BuildContext.served: the keys routed to the built forward.
    • StepMetadata.DSA_REPLAY_PLAN: lengths, indexer schedule, kpool write plan. StepMetadata.DSA_REPLAY_PAGE_TABLE: the page table, skipped only together with the plan.
  • kernel_plugins/amd/binding.py:
    • served keys are collected per step mode and capped by max_rows;
    • shape_key keys DRAFT_EXTEND_V2 steps, and a draft decode replay that plans its metadata before input_ids is set;
    • the attention-backend check runs once per mode;
    • _CaptureCheck checks each graph runner's captures on their own, since a draft model has a draft decode and a draft extend runner.
  • eagle_draft_cuda_graph_runner.py, eagle_draft_extend_cuda_graph_runner.py: run the model runner's capture tail hooks after each captured graph, as DecodeCudaGraphRunner does. On a draft model runner those hooks are the AMD and NVIDIA plugin capture checks; the DSpark and DFlash draft-sampler hooks sit on their own workers' runners, not these.
  • dsa_backend.py, on a routed replay:
    • both metadata members unread: the single-step replay (decode, verify, draft extend) and the multi-step draft decode replay keep the captured metadata;
    • only the plan unread: a decode replay rebuilds just the page table the attention reads;
    • every other step builds everything as before.
  • CPU tests in test_mono_framework.py:
    • a draft spec's step modes are routed by graph key and the target's verify is not;
    • a draft spec with no steps keeps the normal forward;
    • each draft graph runner is checked separately;
    • max_rows at TP4, and not at TP2, leaves 6-row steps to the model's forward, and their metadata is built;
    • the DSA replay keeps the captured metadata with both members unread. With only the plan unread, it rebuilds SGLang's real page table (req_to_token at each page's first token, divided by the page size, at page sizes 4 and 1); negative control: the requests in another order. It builds everything for an unread page table alone, an unserved width, or a page-table-only verify.
    • test_mono_contract.py lists the draft runners' capture_bs.

The draft spec this serves is a GLM-5.3 MTP draft spec for sgl-amd-mono, outside this repo and not yet upstream there. It declares both DSA members, as does the target spec, and builds the page tables it reads from req_to_token in its own step-table launch.

Stacked on #47101 (merge that first; this PR's own change is its last commit): both edit shape_key, and the GLM-5.3 plugin needs both.

Accuracy Tests

The page tables the plugin builds match SGLang's req_to_token[req, 64j] // 64 word for word. That is the plugin's own kernel check, outside this repo.

Accept length at real acceptance (SGLang's default sampling), TP8, 1 request, 8K, two servers per arm in ABBA order, 30 requests each, paired by prompt:

#47101 this PR change
accept length 3.229 3.262 +0.032 (se 0.039); A/A +0.011 and −0.013
per-position acceptance 0.885 / 0.724 / 0.574 0.888 / 0.730 / 0.594

test/registered/unit/kernel_plugins/amd (CPU): 24 passed (19 on its base). The neighbouring runner and DSA tests (test_decode_cuda_graph_runner.py, test_eagle_draft_cuda_graph_runner.py, test_dsa_backend_dp_padding.py, test_graph_routing_context.py): 44 passed, as on main.

Speed Tests and Profiling

8× MI355X, TP8, GLM-5.3 FP8 with --enable-amd-monokernel, EAGLE 3-1-4, FP8 KV, 8K. Acceptance is simulated at 2.99. One server per arm, 6 waves, paired by prompt:

#47101 this PR change
MTP cycle, 1 request (ms) 7.69 7.64 −0.7%
MTP cycle, 2 requests (ms) 12.01 11.88 −1.0%

One round, so there is no same-arm A/A pair on this run; at C=1 the change is within what one round resolves. The change is the median per-request log-ratio, requests paired by prompt; at 2 requests only each wave's last-prefilled request is used, since the first one waits out the other's prefill. The trace shows where it comes from.

Time outside the launches per cycle: rank 0, 1 request, CPU+GPU trace (median of 37 cycles, profiled), wall / GPU-busy µs:

#47101 this PR
draft glue (draft extend and draft decode preparation, metadata) 240 / 219 190 / 168
verify preparation 83 / 65 70 / 52
sampling and accept 195 / 153 197 / 154
total outside the launches 522 461

Setup:

Checklist


CI States

Latest PR Test (Base): ❌ Missing run-ci label -- add it to run CI tests.
Latest PR Test (Extra): ❌ Blocked -- run-ci is required first.
Latest PR Test (AMD ROCm 10): ➖ No AMD PR run found for this commit.

Added: no verify broadcast when every rank's logits are the same

On HIP, a greedy EAGLE verify broadcasts its results from rank 0. Without it, ranks whose draft tokens differ would accept different counts and deadlock the next collective. A monokernel forward that all-gathers its logits over the TP group returns them bitwise equal on every rank, so every rank's draft and verify reach the same result on their own.

The commit makes two changes:

  • A spec declares this with MonoSpec.gathered_logits. binding.gathers_logits(model, key) reports it for the routed keys only.
  • On HIP with topk=1, the verify, draft-extend and draft-decode steps of the batch size may all be served by declaring specs. When they are and rejection sampling is off, eagle_sample skips the broadcast.

Every other case keeps the broadcast: rejection sampling on (SGLang's HIP default for EAGLE), sampled requests, an undeclared or unrouted step, and CUDA.

The CPU test test_mono_framework.py checks that only routed steps of a declaring spec gather their logits; a width that isn't built, an undeclared spec, and a model without monokernels don't. test/registered/unit/kernel_plugins/amd: 25 passed (24 before).

Development measurements on our GLM-5.3 monokernel stack, at TP4, 1 request, with rejection sampling off:

  • the broadcast and the idle after it took 23 µs a cycle, profiled;
  • real accept length +0.091 (se 0.047), and every request completed.

The sgl-amd-mono side is one line per spec, gathered_logits=True on the GLM-5.3 target and draft specs, and it follows this PR.

Added: the capture check skips draft runners whose modes the spec doesn't serve

With this PR, the EAGLE draft decode and draft extend runners run the model runner's capture hooks, so the monokernel capture check now sees their graphs.

That would break draft specs that don't declare step modes. A draft spec that wraps the model's forward serves the server's verify key, and none of its draft runners' graphs are of that mode. The check would refuse them ("none of the N graphs EAGLEDraftCudaGraphRunner captured … runs …"), so such a spec could not start with CUDA graphs on. This includes the GLM-5.3 NextN spec in sgl-amd-mono today.

_CaptureCheck now skips a runner none of whose captured graphs is of a forward mode the spec serves:

  • Forward-wrapped draft specs keep working as on main.
  • A runner of a served mode is still refused if none of its graphs run the monokernels. That covers the target's decode or verify runner, and the draft runners of a spec with step modes.

The CPU test test_mono_framework.py checks that the draft runners of a spec without step modes pass, and that a verify runner whose graphs aren't routed still raises. It fails without the fix. test/registered/unit/kernel_plugins/amd: 26 passed.

Hassan Eslami added 2 commits October 27, 2026 22:48
…hout the host read

On HIP the DSA decode and verify graphs come in a dense and a sparse variant,
chosen per replay by the batch's longest request. A speculative verify keeps
no host copy of the lengths, so the choice reads them from the device
(seq_lens.max().item()) and the host waits there for the draft to finish
before it can prepare and launch the verify graph. A monokernel forward runs
none of SGLang's attention, so both variants of a routed batch size run the
same launches: when the spec declares StepMetadata.ATTENTION_GRAPH_VARIANT
unread, those sizes are captured once, without variants, and their replay
skips the choice.
…e DSA replay metadata they never read

A draft spec declares its (forward mode, tokens per request) steps in
MonoSpec.step_modes: SGLang routes EAGLE's draft decode and draft extend
steps to it by graph key, checks each mode's attention backend and each
draft graph runner's captured graphs (the EAGLE draft graph runners now run
the capture tail hooks), and passes the served keys in BuildContext.served.
A draft spec with no step keeps the normal forward.

DeviceLimits.max_rows caps the rows a step is routed with per TP size, so a
spec whose kernels cannot serve a width at some TP size leaves those steps to
the model's forward instead of falling back inside its own: every served key
is a routed key, which is what the unread-metadata contract relies on.

Two StepMetadata members let the DSA backend skip its per-replay work on a
routed step: DSA_REPLAY_PLAN (lengths, indexer schedule, kpool write plan)
and DSA_REPLAY_PAGE_TABLE. With both unread the single-step replay (decode,
verify, draft extend) and the multi-step draft decode replay keep the
captured metadata; with only the plan unread a decode replay rebuilds just
the page table the attention reads.
…s are the same

On HIP a greedy EAGLE verify broadcasts its results from rank 0, because ranks
whose draft tokens differ would accept different counts and deadlock the next
collective. A monokernel forward that gathers its logits over the TP group
(each vocabulary shard copied from the rank that computed it) returns them
bitwise equal on every rank, and the top-1 and the greedy verify reduce them
deterministically. A spec declares it with MonoSpec.gathered_logits; when the
target's verify step and the draft's draft extend and draft decode steps are
all served by such forwards and no rejection sampling is on, the greedy verify
skips the broadcast.
… modes the spec does not serve

The EAGLE draft decode and draft extend runners now run the model runner's capture
hooks, so the monokernel capture check sees their graphs. A draft spec that wraps
the model's forward instead of declaring step modes serves the server's verify key,
and none of its draft runners' graphs are of that mode: the check refused them
("none of the N graphs EAGLEDraftCudaGraphRunner captured ... runs ..."), so such a
spec could not start with CUDA graphs on.

The check now skips a runner none of whose captured graphs is of a forward mode the
spec serves. A runner of a served mode still has to run the monokernels.

This branch has not been deployed

No deployments
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