Repository navigation
perf(qwen): integrate Mono decode and optimized TP1 MTP3 serving - #2610
Conversation
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
d73b540 to
0617ed4
Compare
There was a problem hiding this comment.
Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit
ruff
Unnecessary dict() call (rewrite as a literal)
ATOM/tools/qwen38_mono_repro/moe_reference.py
Lines 16 to 17 in 0617ed4
Unnecessary dict() call (rewrite as a literal)
ATOM/tools/qwen38_mono_repro/moe_reference.py
Lines 50 to 53 in 0617ed4
Unnecessary dict() call (rewrite as a literal)
Unnecessary dict() call (rewrite as a literal)
ATOM/tools/qwen38_mono_repro/moe_reference.py
Lines 102 to 103 in 0617ed4
Unnecessary dict() call (rewrite as a literal)
ATOM/tools/qwen38_mono_repro/moe_reference.py
Line 104 in 0617ed4
0557230 to
133e46e
Compare
613e115 to
8ddd75f
Compare
6bfe8ab to
42b73ab
Compare
|
@yifehuan Thanks for adding Quark MXFP4 + FP8 KV. We tried 1. On current aiter main, Mono declines every build. The startup log has 2. Mono's admission excludes MTP and online quantization.
So Mono is a clear win over the native path at the same settings. On this model, though, turning off MTP and PTPC costs 16–30% throughput, more than Mono recovers. With Mono declining (MTP on), #2610 does not change the native path (c1 / c2 / c4 within ±1.3%). Minor: with Mono on at c4, one warmup request per round ( Is MTP (and online-quant) support on the roadmap? On MI355X, Qwen3.8's low-concurrency points depend on MTP. |
42b73ab to
8a9a484
Compare
|
@ZLkanyo009 an update for your l35 lines on this head (198057d). Both of them now enable Mono by default, as long as your tree carries this PR together with #2636. It needs #2655 too: with FP8 QSA caches,
I have not run your actual integration tree. When this PR goes into it, expect conflicts in three files, because this PR carries the same mechanisms as two of yours:
I expect your side of those files to work with Mono, since Mono's PLE layer runs ATOM's own PLE first, but I have not tested that combination. Once #2663 and #2771 land, this PR will drop its copies. If you retest in your tree, the startup log should show these lines:
If something refuses, the log shows |
|
@valar-cop could you re-review? Since your review of 2b24902, the head is now 198057d.
Pre Checkin is green on 198057d, and the heavy CI is queued. The commits will be squashed at merge. |
|
ci triage for 24b2d68:
|
198057d to
28389c0
Compare
|
Restructured and force-pushed (198057d → 28389c0, rebased on
Same-card fixed load against the previous head: C1 1.900 / 1.871 / 1.895 ms, C2 within 1%. Full GSM8K with real MTP: 1293 / 1319. Details are in the description. Earlier replies in this thread name the old paths ( |
valar-cop
left a comment
There was a problem hiding this comment.
some changes needed..
ci Run non-GPU unit tests (Pre Checkin) failed — not sure yet (low): The captured log is 0 bytes, so the failure cannot be classified. The same job failed on main at 2026-11-03T14:17 with successes before it, so main-broken is plausible but unproven; the PR does add CPU tests importing new qwen38 modules, which I checked import cleanly without aiter/flydsl.
next: Re-run the job and attach the log; if it reproduces, check whether the new CPU tests collect (they use importorskip only for flydsl). https://github.com/ROCm/ATOM/actions/runs/41653380070/job/125977139564
ci ATOM Test failed — not sure yet (low): The run is reported as failure but no failed job or log was captured, so nothing can be attributed to this PR.
next: Inspect the run's job list for the failing job and its log. https://github.com/ROCm/ATOM/actions/runs/41653380244
Whole decoder layers as one resident FlyDSL program each (gfx950): stages (blocks/: linear, hyper-connection, GDN with the classic and the ReplaySSM verify, QSA with its indexer, MoE, select-row, prefetch) composed by recipe.compose_layer with the frozen per-scenario schedules, placed on the 256 resident CTAs (program, layer), handing off through tagged mailbox regions (device/), emitted as one kernel (emit, kernel), and the weight layouts the stages read, checked at bind time (weights/). The geometry is read from the checkpoint's config.json and checked against the one the kernels are written for. The package follows the other mono models (atom/models/<model>/mono/); the model itself stays the single-file qwen4_exp.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install_mono_decode (models.registry.mono_decode_installers) wraps the loaded Qwen4ExpForConditionalGeneration: a target MTP3 verify step of 1-4 requests runs its 48 decoder layers as 48 launches (36 GDN, 12 QSA programs; the PLE layer runs ATOM's PLE prelude first), reading and writing ATOM's GDN pools, ReplaySSM records and QSA caches in place, so a request moves between mono and Native steps. Every other step runs ATOM's model. Mono keeps its own A8W4 copy of the routed experts, so the PLE n-gram table (~95 GB BF16) goes to page-locked host memory, read over UVA (ple.py, chosen at the table's construction): in HBM it would leave no room for the per-request state at --max-num-seqs 160. #2771 does the same for every deployment; this copy goes once it merges. config_refusal checks the deployment once, at install: the full 48-layer Quark MXFP4 checkpoint, MI355X with all 256 CUs, TP1, MTP3, FP8 KV with BF16 index caches on 64-token blocks, an fp32 GDN state, BF16 draft caches, either GDN verify (ReplaySSM, ATOM's default, up to 64 records, or classic), no KV connector but LMCache CPU offload (resident_grid_refusals takes the connectors a model admits). Every served shape is built at startup; one that does not build stops the server (ATOM_MONO_ENABLE=0 serves it Native). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install_mono_decode wraps the loaded Qwen4ExpMTP as well (MonoDraftModel, as V4.1 wraps its DSpark model). A draft step of 1-4 requests runs the drafter's BF16 layer as one mono program (DraftRunner): the first step attends every verify row and runs the MLP on each request's last row only, later steps run one row. Any other step of at most 16 rows runs the fallback draft (fused QSA attention, ATOM's BF16 MLP, one-row CK experts built at startup), anything larger ATOM's layer. The draft ids come from a Group64 W4 screen with a global error certificate refined on the BF16 rows: the BF16 head's argmax for 1, 2 and 4 rows, ATOM's argmax head otherwise. EagleProposer: a draft model that sets draft_returns_head_input returns (state, head input) from forward_with_head (with selected_rows on step 0 of a decode) and maps the head input to ids (compute_draft_ids_from_head), in a declared draft pass and eagerly alike. Qwen3.8 is an MRoPE model, so on main its draft steps 1+ have no declared pass and run eagerly: the program is one launch a layer, so they cost the same as in a captured graph (measured with #2663 applied: request TPOT within 1%). The recipe, the environment variable table and CLAUDE.md name the deployment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
28389c0 to
795cb43
Compare
…-id install test - REPLAY_MAX is defined once, in kernels/geometry.py (CPU-safe); blocks/gdn.py and config.py import it instead of keeping two copies. - GDNPools takes ATOM's conv pool as it is, token-major [slots, W, conv_dim] (gdn_attn's mamba_k_cache): the branch that transposed a [slots, conv_dim, W] view never ran in serving, and a channel-major pool is refused with the layout mono reads. - test_install_reads_a_hub_id_checkpoint_from_its_local_directory is back, for the install-time drafter wrapping: the resolved directory reaches load_geometry, DraftRunner and TargetRunner, and the drafter is wrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yifehuan
left a comment
There was a problem hiding this comment.
Notes on the changes outside atom/models/qwen38/: why each one is there.
|
@valar-cop can we merge |
Mono refused every KV connector, since a connector's copy kernels can hold CUs the all-resident mono kernels wait on. A Mooncake kv_consumer queues no GPU work of its own for K3: its receives are NIC writes into the paged KV and state pools, and the only consumer kernels (the slot scatter, the DCP landing pool) are for DeepSeek-V4 and DCP decode. Serve that one transport; every other, or an offload tier beside it, is still refused. K3 mono now serves decode CP on its own, but a Mooncake decode running it stays refused: mono has never served one behind a connector. The K3 config passes Mooncake in resident_grid_refusals' `connectors` (the admit list #2610 added for Qwen's offload) only for such a decode, so the DSA indexer and every other mono model still refuse it. Co-authored-by: Cursor <cursoragent@cursor.com>
Conflicts with #2610 in atom/model_ops/qwen4_exp/ple_layer.py: - Construction: Mono's make_host_embedding is asked first; only when it returns None does the host table (on gfx950 / gfx942) or the HBM embedding get built, so at most one n-gram table is ever allocated. - Shard loader: an embedding with load_rows (Mono's) keeps #2610's branch; every other embedding goes through load_shard. tests/test_qwen38_mono_ple.py: the HBM-vs-Mono comparison pins _host_table_available to False, since a deployment Mono refuses now takes the host table on gfx950 / gfx942. Co-authored-by: Cursor <cursoragent@cursor.com>
Mono refused every KV connector, since a connector's copy kernels can hold CUs the all-resident mono kernels wait on. A Mooncake kv_consumer queues no GPU work of its own for K3: its receives are NIC writes into the paged KV and state pools, and the only consumer kernels (the slot scatter, the DCP landing pool) are for DeepSeek-V4 and DCP decode. Serve that one transport; every other, or an offload tier beside it, is still refused. K3 mono now serves decode CP on its own, but a Mooncake decode running it stays refused: mono has never served one behind a connector. The K3 config passes Mooncake in resident_grid_refusals' `connectors` (the admit list #2610 added for Qwen's offload) only for such a decode, so the DSA indexer and every other mono model still refuse it. Co-authored-by: Cursor <cursoragent@cursor.com>
Mono refused every KV connector, since a connector's copy kernels can hold CUs the all-resident mono kernels wait on. A Mooncake kv_consumer queues no GPU work of its own for K3: its receives are NIC writes into the paged KV and state pools, and the only consumer kernels (the slot scatter, the DCP landing pool) are for DeepSeek-V4 and DCP decode. Serve that one transport; every other, or an offload tier beside it, is still refused. K3 mono now serves decode CP on its own, but a Mooncake decode running it stays refused: mono has never served one behind a connector. The K3 config passes Mooncake in resident_grid_refusals' `connectors` (the admit list #2610 added for Qwen's offload) only for such a decode, so the DSA indexer and every other mono model still refuse it. Co-authored-by: Cursor <cursoragent@cursor.com>
Mono refused every KV connector, since a connector's copy kernels can hold CUs the all-resident mono kernels wait on. A Mooncake kv_consumer queues no GPU work of its own for K3: its receives are NIC writes into the paged KV and state pools, and the only consumer kernels (the slot scatter, the DCP landing pool) are for DeepSeek-V4 and DCP decode. Serve that one transport; every other, or an offload tier beside it, is still refused. K3 mono now serves decode CP on its own, but a Mooncake decode running it stays refused: mono has never served one behind a connector. The K3 config passes Mooncake in resident_grid_refusals' `connectors` (the admit list #2610 added for Qwen's offload) only for such a decode, so the DSA indexer and every other mono model still refuse it. Co-authored-by: Cursor <cursoragent@cursor.com>
This PR adds whole-layer mono decode for Qwen3.8-Flash-Next (Quark MXFP4) on MI355X, TP1, MTP3. It now follows the layout of the other mono models: everything that is Qwen3.8's lives in
atom/models/qwen38/mono/, as DeepSeek-V4.1's lives inatom/models/deepseek_v41/mono/. The model is the single-fileqwen4_exp.py, so the package is named after the model, asglm52is besidedeepseek_v2.py.Layout.
Outside the package the PR changes: one line in
models/registry.py(the installer); the PLE table's construction inmodel_ops/qwen4_exp/ple_layer.pyasksple.make_host_embeddingfirst (17 lines, until #2771);resident_grid_refusalsinatom/mono/runtime/deployment.pytakes the KV connectors a model admits (empty by default, so Kimi-K3 and the DSA indexer are unchanged);EagleProposergets one optional draft-model hook (below); the recipe, theATOM_MONO_ENABLErow and aCLAUDE.mdpointer. There are no lint exemptions: the kernels are black- and ruff-clean, andpyproject.tomlis untouched.What runs on mono. On a target MTP3 verify step with 1 to 4 requests (4 rows each), every decoder layer runs as one resident FlyDSL program: 36 GDN and 12 QSA layers; the PLE layer runs ATOM's PLE prelude first. The drafter's layer runs a BF16 program: the first draft step attends all 4 verify rows and runs the MLP on each request's last row; later steps run one row. The draft ids come from a Group64 W4 screen with a global error certificate, refined on the original BF16 rows: the BF16 head's argmax for 1, 2 and 4 rows, ATOM's argmax head for 3. Steps with more than 4 requests, prefill and mixed steps run ATOM's model; draft steps the program does not serve run the fallback draft (fused QSA attention, ATOM's BF16 MLP).
A layer program is an ordered list of stages placed on the 256 resident CTAs of one launch. Stages hand off through tagged (value, tag) mailbox regions in scratch, so one launch covers the whole layer without grid barriers. Hand-off contracts are checked at build time with
atom.mono.plan.check/trace, and the register primitives and MX helpers come fromatom/mono/device. Linear task shapes and schedules are frozen per (kind, batch, spec) inkernels/schedules.py; nothing is searched or cached at build time.The drafter.
install_mono_decodewraps the drafter's loadedQwen4ExpMTP(MonoDraftModel), the way V4.1 wraps its DSpark model;spec_decode/drafter.pyis untouched.EagleProposergets one optional protocol: a draft model that setsdraft_returns_head_inputreturns(state, head input)fromforward_with_head(withselected_rowson step 0 of a decode, so only each request's last row runs the MLP) and maps the head input to ids withcompute_draft_ids_from_head, in a declared draft pass and eagerly alike. Other draft models take the existing path. Qwen3.8 is an MRoPE model, so onmainits draft steps 1+ have no declared pass (that is #2663) and run eagerly; the program is one launch a layer, so they cost the same as in a captured graph (below).Deployment contract.
ATOM_MONO_ENABLE(default on) is the only switch, and it only turns mono off. The GDN programs run whichever MTP verify ATOM picks: ReplaySSM, its default under MTP (one checkpoint a request plus(k, u, g)records), or the classic verify (ATOM_ENABLE_REPLAYSSM=0). They read and write ATOM's checkpoint, records and pools in place, so a request moves between mono and Native steps.config_refusalchecks the deployment before any state is written (at the PLE table's construction and at install); any other deployment runs Native for the whole model, and the log names the reason (Qwen mono off: <reason>).lmcache_offload)--kv_cache_dtype fp8 --index-cache-dtype bf16 --block-size 64, FP32 GDN state, BF16 draft cachesAll 16 program shapes and the fallback draft's one-row experts are built at startup. A deployment the contract admits is served whole, so a shape that does not build stops the server;
ATOM_MONO_ENABLE=0then runs it Native.--modelmay be a local directory or a hub id. The recipe isrecipes/Qwen3.8-Flash-Next-Mono-Program.md.Related open PRs.
--max-num-seqs 160(19.7 GB) and the server does not start. Until perf(qwen4_exp): keep the PLE n-gram table in page-locked host memory, read over UVA #2771 merges,ple.pyplaces the table for the deployments mono serves; it goes when this branch rebases onto perf(qwen4_exp): keep the PLE n-gram table in page-locked host memory, read over UVA #2771.Memory. Mono keeps its own A8W4 copy of the 48 layers' routed experts next to ATOM's A4W4 tensors (the Native path still uses them), so its weights take about 81 GB more than Native's. At
--gpu-memory-utilization 0.95,--max-num-seqs 160, ReplaySSM, one MI355X:main)The decode measurements below are not limited by the pool; many long concurrent sessions are. Reading ATOM's expert layout directly would remove the expert copy, which is a follow-up.
Validation (049, one MI355X, full 48-layer checkpoint, TP1, MTP3, Zhang Ling's A_lo server). The first table is from this head; the checks after it were measured on
main825b63b before the rebase onto 63e64d8, whose commits share no code with this PR (the engram host-memory change, #2898, leaves the functionsple.pycopies through unchanged).Native against mono by concurrency, on this head (24b2d68), same card, Native (author default: ReplaySSM, the PLE table in HBM) -> mono -> Native; 4,000 output tokens, acceptance length forced to 2.32, 3 seeds; request TPOT in ms. GSM8K: full 1319 questions with real MTP, mono, at the same client concurrency (author-default Native at c2: 1287 / 1319).
Native drifts under 1% between its two arms. Mono serves verify steps of 1 to 4 requests, so at C8 the target runs Native and the draft runs the fallback, at Native's speed. At C4 the step with all four requests decoding takes 8.54 ms against Native's 9.85 ms (so mixed prefill steps are not what limits it): the B4 verify's 16 rows move many more expert weights than C2's 8 (5.62 ms against 9.10 ms).
Kernels: the 16 classic programs (target delta / qsa S4, draft mtp S4 / S1, batches 1 to 4) and the 4 ReplaySSM delta programs, built from this branch and from the previous head in one session: 20 / 20 identical normalized ISA, resources and stage lists. (The sparse-attention programs build into one of two ISA variants run to run on either source: one subtract on the scalar or the vector unit.)
Fixed load: prompts of 64K (C1) or 16K / 64K tokens (C2), 4,000 output tokens, acceptance length forced to 2.32, 3 seeds per arm, request TPOT. The restructure, with the previous head before and after it (seed-paired medians -2.3% / -2.2% at C1, within 0.7% at C2):
Then the drafter's steps 1+ in feat(spec): capture the Qwen3.8 MTP draft steps in CUDA graphs (MRoPE) #2663's graphs against this branch (eager, on the program), with feat(spec): capture the Qwen3.8 MTP draft steps in CUDA graphs (MRoPE) #2663 again after it; seed-paired medians within 1%. For comparison, the same steps eager on ATOM's layer instead of the program cost +54% at C1:
GSM8K, full 1319 questions, real MTP, default switches (ReplaySSM): 1293 / 1319 (0.980), MTP acceptance rate 0.734 (previous head 1291 / 1319, author-default Native 1287 / 1319). Under real MTP one build's greedy text differs run to run, so builds are compared by score.
Real-MTP smoke at 1, 2, 3, 4 and 6 requests (the last on the Native target and the fallback draft): all served, no errors, 76,955 KV blocks.
Unit tests: CI's CPU suite (
.github/scripts/run_unit_tests.sh, CPU torch, no aiter / Triton / FlyDSL) passes 11,025; its 5 failures are the same onmainin my runner (3 torch-inductor tests without Python headers, 2 that need a.gitcheckout). 219 GPU tests pass on MI355X. Black and ruff are clean on every changed file with the repository's configuration.Commits.
feat(qwen38): the mono layer programs (kernels/).feat(qwen38): the target's verify steps on mono: deployment contract, runner, step metadata, weights, the PLE host table, installer, tests.feat(qwen38): the MTP drafter on mono, the proposer hook, recipe and docs.fix(qwen38): review: oneREPLAY_MAX, token-major GDN conv pools only, the hub-id install test (ISA, GPU tests and a real-MTP smoke re-checked).🤖 Generated with Claude Code