Skip to content

[Perf] [MI355X] deepseek_v41: DSpark step glue in fewer launches, Markov draft sampling, faster K1 and K2 - #95

Merged
kevin-mii merged 2 commits into
sgl-project:mainfrom
limou102:dev/limou_dsv41_aklr_port_squash
Nov 3, 2026
Merged

kevin-mii merged 2 commits into
sgl-project:mainfrom
limou102:dev/limou_dsv41_aklr_port_squash

Conversation

@limou102

@limou102 limou102 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

DeepSeek-V4.1-Flash with DSpark on MI355X spent much of each decode round outside the monokernels: ~250 launches a round outside them, most of them ~4 µs SGLang glue kernels (attention metadata, accept, draft-KV inject, draft head); an eager sampled accept at temperature 1 (the AgentX default); and K1 waiting on its own weight loads. This PR moves that work into fewer launches and shortens K1, keeping SGLang's results bitwise where the kernels did before.

Modifications

  • K1 / K2
    • Stage hand-offs happen earlier.
    • A layer's owed mHC post folds into the next launch that reads it: the next K1, the Engram gate, or the head's combine.
    • DSpark captures are written into one packed tensor.
    • K1 issues its wq_b weights off the wqkv path. Up to 7 rows a rank, each wqkv row tile's SGLang-order chains run on two CTAs; results stay bitwise identical.
  • deepseek_v41/glue/ (new)
    • Verify and draft-block attention metadata: one launch each.
    • Accept: SGLang's one-launch tail without the accept-site TP broadcasts. Under simulated acceptance, a temperature-only sampled step is accepted and committed in the graph.
    • Draft-KV inject: one HIP launch.
    • Draft head in FP8.
    • Prefill indexer kernels compile once.
    • Each piece replaces SGLang's path only if its first call agrees with SGLang. SGLANG_AMD_MONO_DSV41_GLUE=0 turns them all off; per-piece env vars are listed in the README.
  • Sampled DSpark draft: the Markov steps run in two launches each, via a HIP extension (common/hip_ext.py).
  • fused.py: fused mHC post verdicts are made at startup on random rows, not on a graph capture's warmup buffers.
  • common/sync.py: adds Mailbox.poll_issue / poll_wait, which issue a poll's first loads ahead of later loads.
  • Tests: new check_glue.py, check_markov.py and time_layer.py; test_deepseek_v41_kernels.py runs the first two.

Parts are adapted from AMD-AGI/AKLR, as each adapted file's header says.

Tested with lmsysorg/sglang-rocm:v0.5.22-rocm10-mi35x-20261029 (SGLang 686cf984) on MI355X. Rebased on main @ 8039635; the _engram conflict with #65 keeps this PR's structure, which already passes input_ids and the image ids, and takes #65's Engram.forward(input_ids) contract pin.

Speed Tests and Profiling

InferenceX AgentX: dsv41flash-fp4-mi355x-sglang-agentic-dspark, recipe dsv41flash/sglang/mi355x-fp4-mtp/agentic.yaml.

  • Golden acceptance length 3.51; the measured mean is 3.51 on every run.
  • 3600 s per point; both runs of a point are on the same node.
  • main = 6777a89; this PR = the same changes on that main.
pr_pareto

Each point is one (TP, DPA, CONC) scenario, and each line connects one run's points; up and to the right is better.

Point Monokernel P90 interactivity (tok/s/user) main → PR Throughput (tok/s/chip) main → PR
TP2 DPA C64 off (control) 71.3 → 71.9 (+0.8%) 178,445 → 178,461 (+0.0%)
TP2 C32 off (control) 135.9 → 133.3 (−1.9%) 111,823 → 111,847 (+0.0%)
TP2 C16 on 177.1 → 263.4 (+48.7%) 54,192 → 57,992 (+7.0%)
TP2 C8 on 246.2 → 389.0 (+58.0%) 30,130 → 32,331 (+7.3%)
TP2 C4 on 339.5 → 474.9 (+39.9%) 18,022 → 19,174 (+6.4%)
TP2 C2 on 365.4 → 488.1 (+33.6%) 12,777 → 15,218 (+19.1%)
TP2 C1 on 448.6 → 621.2 (+38.5%) 12,663 → 15,291 (+20.7%)
TP4 C4 on 549.9 → 629.4 (+14.4%) 10,255 → 10,591 (+3.3%)
TP4 C2 on 570.7 → 708.9 (+24.2%) 7,706 → 8,501 (+10.3%)
TP4 C1 on 675.8 → 805.4 (+19.2%) 7,700 → 8,076 (+4.9%)
  • Mean ITL drops 16–37% on the monokernel points, and mean TTFT drops too (TP2 C4: 0.67 → 0.38 s).
  • The two control points don't use the monokernel (the recipe), so they bound run-to-run noise: throughput ±0.0%, P90 within 2%.

Fixed-length decode, single user: the AgentX TP4 C1 server, random prompts, 2048 output tokens, simulated AL 3.51 (measured 3.50–3.53 tokens a round on both). Values are medians over the requests; the two runs are on different nodes.

Input Temperature ms per decode round, main → PR Decode tok/s, main → PR
8K 0 4.606 → 4.019 (−12.7%) 766 → 881 (+14.9%)
64K 0 4.665 → 4.013 (−14.0%) 756 → 875 (+15.7%)
8K 1 4.875 → 4.091 (−16.1%) 722 → 856 (+18.5%)
64K 1 4.959 → 4.059 (−18.1%) 710 → 873 (+22.9%)

Kernel-only layer time: test/kernels/deepseek_v41/time_layer.py --widths 6,12, cold weights, median µs per layer (K1 + K2), main → PR.

6 rows 12 rows
TP4 92.22 → 87.48 (−5.1%) 123.75 → 118.64 (−4.1%)
TP2 172.81 → 128.88 (−25.4%) 244.28 → 187.41 (−23.3%)

Accuracy Tests

Repo test suite, full, MI355X ×8, main vs this PR (before the rebase: main @ a0c3997)

Suite main This PR
pytest test/unit 328 passed / 3 failed 342 passed / 3 failed
pytest test/kernels 84 passed / 3 failed 87 passed / 2 failed
pytest test/e2e -k deepseek_v41 (TP4: GSM8K, KL vs prefill, long prompts, passkey, fault stop) 9 / 9 9 / 9
  • Every failure also fails on main and is outside deepseek_v41: the minimax / minimax_m31 / glm53_flash spec and contract tests, and test_minimax_m31_ffn::test_o_proj_is_sglangs_to_the_bit.
  • main's extra kernel failure (TestMonoGlm53Kernels::test_indexer_selects_the_exact_top_k, retry limit exceeded) passes on this PR.
  • check_k1 passes at TP2, TP4 and preshuffled: the seam outputs and KV records are bitwise SGLang's, and q is within the spec's bound.
  • After the rebase onto 8039635:
    • pytest test/unit: 538 passed / 8 failed vs main's 524 / 8, with the same 8 failures.
    • The glue, Markov, guard and dispatch kernel tests pass.

Real DSpark acceptance (simulation off), AgentX server flags, main vs this PR

Config Code GSM8K (100) HumanEval (21) Needle 8K–128K (10) Probes (5)
TP2 C4 main 97 21 10 5
TP2 C4 PR 97 21 10 5
TP4 C1 main 98 20 10 5
TP4 C1 PR 97 20 10 5
  • The probe answers are identical on main and this PR.
  • Every glue piece reports one launch on every rank.

The _engram merge conflict: this PR at TP4 with the monokernel and DSpark, Engram host table in both layouts.

  • shared: SGLang's lookup, engram(..., input_ids=...).
  • per_rank: staged rows through the fused gate.
  • Both servers start and capture their graphs with no traceback, and serve text probes (5/5), image probes (3/3, so the image rows pass through the gate) and 32 concurrent requests.

Checklist

  • pre-commit run --all-files passes.
  • Tests under test/ cover the change (check_glue.py, check_markov.py, time_layer.py).
  • The SGLang commit or PR this was tested against is named above.
  • Accuracy and speed results are attached when kernel or forward code changes.
  • A new model adds one line to .github/CODEOWNERS that names its owner. (n/a: no new model)

@limou102
limou102 force-pushed the dev/limou_dsv41_aklr_port_squash branch from dfaa33e to a4a5f13 Compare November 2, 2026 06:31
@kevin-mii

kevin-mii commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Hi @limou102 Thanks! Awesome work really great improvements.

Two small points before merge:

1. Simulated acceptance takes a path real serving never takes

SGLANG_SIMULATE_ACC_LEN is meant to change only the accepted length; every other op should still run at real cost. This PR adds code that runs only when simulation is on:

  • glue/accept.py:151-184: under simulate_fold, the target argmax is skipped.
  • glue/accept.py:277-284: when SGLANG_SIMULATE_ACC_LEN > 0, it sets SGLANG_DSPARK_SIMULATE_FOLD=1 and folds accept/commit for temperature-only steps into the graph.

So the simulated runs in the PR body (AL 3.51, temperature 1) measure work that real serving still does. At TP4 the accept piece saves 0.25 ms/round under simulation (4.505 -> 4.252 ms), and about half of that comes from the fold and the skipped argmax.

Please keep the argmax under simulation and make no path depend on SGLANG_SIMULATE_ACC_LEN. If the in-graph accept is correct for real serving, enable and test it there; otherwise drop it from this PR.

2. Please add a precision point and a speed point (main vs PR, simulation off)

  • Precision: full GSM8K (1319 questions) and measured acceptance length at temperature 0 and 1. For temperature 1, average several --random-seed values: a fresh server replays the same draws, and single runs varied by up to ~15%.
  • Speed: ms/round with the measured AL next to it.

Other points

  • FP8 draft head lowers acceptance (greedy AL 4.88 -> 4.75 at TP4, 4.94 -> 4.80 at TP8) to save ~0.02 ms. That is a net loss of ~2.5% tok/s. Suggest defaulting it off, as SGLang's SGLANG_ROCM_OPT_DSPARK_FP8_LM_HEAD does.
  • Accept-site broadcast is removed even when SGLANG_SPEC_TP_SYNC=all is set explicitly (glue/accept.py:102-125).
  • agreed() (glue/__init__.py:37-50) decides per rank, so TP ranks can take different paths. Reuse the all-reduce MIN from draft_sampler.py.
  • stage_down no longer prefetches the next round's weights during the current round's MFMAs. Please time 24 and 48 rows.
  • IW0 now overlaps the QKV0 CTAs, so index wq_b starts later. time_layer.py doesn't cover this path.
  • Undeclared SGLang reads in accept.py, e.g. temperatures, is_all_greedy, need_*_sampling, verify_ids_2d. Add them to SGLANG_READS.
  • hip_ext.py needs hipcc, ninja and torch headers at runtime, and it sets PYTORCH_ROCM_ARCH process-wide. Please document this in the README.
  • inject.hip casts int64 slot mappings to int32 and doesn't bound pos against the RoPE table.
  • Tests: nothing covers accept, the fold, fused.py or prefill, or real acceptance.
  • Nit: the AKLR headers in inject.hip / markov.hip should say "at ".

FYI, with #82 merged on top, every TP8 kernel check passes. TP8 greedy gives 4.027 ms/round at AL 4.80, 7% faster than TP4.

kevin-mii pushed a commit that referenced this pull request Nov 2, 2026
Stack this PR on #95. front.py: the early qrdy publish takes FLAG_STRIDE, and
red_floats also covers the split's dead wave; spec.py keeps #95's _engram with
_engram_kv; the kernel test docstring takes both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
limou added 2 commits November 3, 2026 10:40
…kov draft sampling, faster K1 and K2

- K1 / K2: earlier stage hand-offs; a layer's owed mHC post folded into the launch that
  reads it next (the next layer's K1, an Engram gate, the head's combine); DSpark's
  captures written into one packed tensor; a partial MXFP8 round stored to LDS without a
  branch; K1's wq_b weights issued off the wqkv path, and up to 7 rows a rank each wqkv
  row tile's SGLang-order chains on two CTAs (bitwise as before).
- glue/: a DSpark verify step's and draft block's attention metadata in one launch each;
  the static verify's accept in SGLang's one-launch tail without the accept-site TP
  broadcasts, and under simulated acceptance a temperature-only sampled step accepted and
  committed in the graph; the verify's draft-KV inject in one HIP launch; the draft head in
  FP8; the prefill indexer kernels compiled once. Each piece keeps SGLang's path unless its
  first call agrees with SGLang; SGLANG_AMD_MONO_DSV41_GLUE=0 turns them all off.
- A sampled DSpark draft's Markov steps in two launches each (HIP extension).
- Fused posts decided at startup on random rows, not on a capture's warmup streams.
- Tests: check_glue.py, check_markov.py and time_layer.py; the kernel test runs them.

Parts are adapted from AKLR (https://github.com/AMD-AGI/AKLR, MIT), as each adapted
file's header says.

InferenceX AgentX (dsv41flash-fp4-mi355x-sglang-agentic-dspark, golden AL 3.51, 3600 s,
MI355X, lmsysorg/sglang-rocm:v0.5.22-rocm10-mi35x-20261029), main -> this change:
TP4 C1 p90 interactivity 675.8 -> 805.4 tok/s/user, throughput 7700 -> 8076 tok/s/chip;
TP2 C4 339.5 -> 474.9, 18022 -> 19174; on the monokernel points p90 +14% to +58%,
throughput +3% to +21%. Real acceptance (simulation off): gsm8k, humaneval and needles
match main.

Signed-off-by: limou <limou102@amd.com>
- accept: drop the in-graph fold of temperature-only sampled steps and the argmax skip,
  which ran only under SGLANG_SIMULATE_ACC_LEN; no path depends on it now. An explicit
  SGLANG_SPEC_TP_SYNC keeps SGLang's accept-site broadcast.
- agreed(): one verdict on every rank of the DP rank's TP group (all-reduce MIN).
- FP8 draft head off by default (SGLANG_AMD_MONO_DSV41_DRAFT_HEAD_FP8=1 turns it on).
- K1: the qkv CTAs past the index head weights' (IW0), not on them.
- inject.hip: rows whose position is past the RoPE table are not stored; the SWA slots
  must fit int32, as SGLang's committed_swa_locations makes them.
- e2e: the verify server asserts the glue pieces run (real acceptance).
- README: what hip_ext.py needs at runtime.

Signed-off-by: limou <limou102@amd.com>
@limou102
limou102 force-pushed the dev/limou_dsv41_aklr_port_squash branch from a4a5f13 to 881ec65 Compare November 3, 2026 10:45
@limou102

limou102 commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed in a second commit (881ec65), rebased on main @ 517040f.

1. Simulated acceptance

  • Removed the in-graph fold of temperature-only sampled steps, the argmax skip in _static_accept_tail and the SGLANG_DSPARK_SIMULATE_FOLD default. Nothing reads SGLANG_SIMULATE_ACC_LEN now.
  • The fold applied the greedy accept tail with a simulated bonus, so it cannot serve real sampled steps. I dropped it rather than enabling it.
  • What accept.py keeps: SGLang's fused tail without the accept-site broadcast (DSPARK_ACCEPT_GRAPH, which SGLang's init and rng presets leave off too), and the simulated length drawn from one seeded stream so the ranks agree without that broadcast. That changes which length is drawn, not what runs.

2. Precision and speed, simulation off

TP4, the AgentX TP4 C1 recipe flags (--max-running-requests 2), MI355X, lmsysorg/sglang-rocm:v0.5.22-rocm10-mi35x-20261029, main @ 517040f vs this PR (the runs predate the e2e assertion below; no other difference). A fresh server per --random-seed (1, 2, 3), main and PR interleaved on one node. Chat completions with return_meta_info: AL = Σ completion tokens / Σ spec_verify_ct; ms/round = each request's (e2e_latency − first_token_latency) / spec_verify_ct, median over the requests.

main PR
GSM8K, all 1319, conc 2, temp 0 96.66%, AL 3.778 96.36%, AL 3.753
GSM8K, all 1319, conc 2, temp 1, seeds 1 / 2 / 3 96.51 / 96.06 / 96.13% (mean 96.23%)
AL 3.653 / 3.639 / 3.634 (mean 3.642)
96.59 / 96.66 / 96.29% (mean 96.51%)
AL 3.669 / 3.663 / 3.653 (mean 3.662)
Single user, 200 GSM8K prompts, temp 0 4.591 ms/round at AL 3.740 3.999 ms/round at AL 3.809 (−12.9%)
Single user, 200 GSM8K prompts, temp 1, seeds 1 / 2 / 3 4.731 / 4.719 / 4.715 ms/round (mean 4.722) at AL 3.670 4.133 / 4.134 / 4.126 ms/round (mean 4.131) at AL 3.673 (−12.5%)

Other points

  • FP8 draft head: off by default, as SGLANG_ROCM_OPT_DSPARK_FP8_LM_HEAD; SGLANG_AMD_MONO_DSV41_DRAFT_HEAD_FP8=1 turns it on.

  • SGLANG_SPEC_TP_SYNC: when it is set, the accept piece is not installed and SGLang's sites stay as selected.

  • agreed(): one verdict per piece on every rank, an all-reduce MIN over get_dp_tp_group()'s CPU group, so a DP-attention rank never waits on another DP rank.

  • stage_down: timed with time_layer.py --widths 6,12,24,48, µs a layer (K1 + K2):

    main PR PR + round r + 1's weights during round r again
    TP4, 6 / 12 rows 92.32 / 124.02 86.99 / 119.60 87.28 / 120.66
    TP4, 24 / 48 rows 186.51 / 283.47 187.08 / 288.11 181.81 / 281.83
    TP2, 6 / 12 rows 170.91 / 242.17 129.84 / 188.37 165.06 / 236.61
    TP2, 24 / 48 rows 423.63 / 687.69 292.91 / 474.70 431.90 / 698.00

    The prefetch wins TP4 at 24 and 48 rows but gives back the whole TP2 gain, so I kept the PR's version: +0.3% / +1.6% vs main at TP4 24 / 48 rows, −25% to −31% at TP2.

  • IW0: on the index-query layers the qkv CTAs now start past the head weights' CTAs.

  • SGLANG_READS: the undeclared reads were the fold's and are gone; the new reads are declared, and test_contract.py passes.

  • hip_ext.py: the README names what it needs at runtime (hipcc, ninja, torch's headers) and that it sets PYTORCH_ROCM_ARCH for the process while it builds.

  • inject.hip: rows whose position is past the RoPE table are not stored. The SWA slots stay int32, as SGLang's committed_swa_locations makes them, and the op checks that every slot of the cache fits.

  • Tests: the e2e verify server (real acceptance) now asserts that verify_meta, draft_block_meta, inject_store and post_combine run, under its GSM8K and KL-vs-prefill checks.

  • AKLR headers: the AKLR repository does not hold the DSv4.1 sources yet; I'll add the commit once it does.

Tests on 881ec65

MI355X ×8, the image above.

  • pytest test/unit: 544 passed / 8 failed; main fails the same 8 (the minimax, minimax_m31 and muse_spark contract tests, test_glm53_flash_spec).
  • test_deepseek_v41_kernels.py: test_k1, test_k1_preshuffled, test_step_glue, test_sampled_markov_block, test_ffn_launch, test_ffn_launch_draft_route and test_k2 pass.
  • pytest test/e2e -k deepseek_v41: 9 / 9, the new glue assertions included.
  • pre-commit run --all-files passes.

@kevin-mii
kevin-mii merged commit ea88326 into sgl-project:main Nov 3, 2026
2 checks passed
kevin-mii pushed a commit that referenced this pull request Nov 3, 2026
#95 was rebased onto main; its first commit's content matches the a4a5f13
already merged here, so only 881ec65's changes are new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants