Repository navigation
[ROCm][Perf][DSv4.1] Mono decode layer for DeepSeek-V4.1-Flash on MI355X - #60397
Conversation
Opt-in (VLLM_ROCM_DSV41_MONO_DECODE=1): each eligible backbone layer of a decode step runs as two persistent FlyDSL launches -- the mHC attention seam, the attention, its TP all-reduce, the FFN seam, the MoE and its all-reduce -- with both all-reduces in-kernel over symmetric peer memory. - vllm/models/deepseek_v41/amd/mono/: the kernels (attention stages; mHC seam and MoE stages adapted from ROCm/ATOM, MIT) and their runner. The layers' weights are read as vLLM loads them, no second copy. - vllm/models/deepseek_v41/amd/mono_decode.py: per-layer eligibility (the 30 layers with the standard seam and no compressor / indexer) and the per-step gate (decode only, <= 48 rows, causal SWA windows, eager or a FULL graph); everything else runs the layer as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
Layer 0, the Engram layers and the compressor / indexer layers keep vLLM's attention seam and attention and run the rest -- the attention TP all-reduce, the FFN seam, the MoE and its all-reduce -- as one persistent launch from wo_b's unreduced output: K2 with its attention back replaced by a push of that output to every rank. K2's FFN half is factored out and shared; K2 compiles to identical code. Those layers' wo_b leaves its TP reduce to the decoder layer, which reduces it itself on the steps the launch does not take. ROCm's preshuffled wo_b path honors reduce_results as the linear does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
The kernels' arch gate reads get_cdna_version() against CDNA_VERSIONS (CDNA4, gfx950, today) instead of on_gfx950(), so a later generation is one entry once the kernels are ported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
The API docs render docstrings as Markdown, where comb[r][c] and the like parse as reference links; mkdocs --strict fails on their unresolved targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
One switch for every model's mono decode kernels; DeepSeek-V4.1 is the first to take it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
/ci run |
|
✅ Triggered Buildkite CI #93417 for commit |
|
Given the scope of this change, I don't think GSM8k only is sufficient for correctness testing. Can you please ensure that at least GPQA-D and a hard eval like AIME25 are run to ensure there is no numerical leakage? I haven't formalized this proposal yet but I outline some eval guidance in this RFC, which may be helpful. |
|
/ci run |
|
✅ Triggered Buildkite CI #93467 for commit |
vllm-project#60397 now rejects the deployments its kernels cannot run (dea9fa3), runs its numerics test at TP4 with error bounds and a replay stress (b9423a8), and merges main up to 18c19ea. The two conflicts, the imports of runner.py and the helpers of mono_decode.py, keep both sides. On gfx942, _unsupported_moe skips the MoE backend check. The gfx942 kernels read their own copies of the experts, which are made before vLLM's MoE backend converts the loaded tensors, so the AITER_MXFP4_BF16 requirement holds on gfx950 only. Signed-off-by: Markus Hartikainen <markus.hartikainen@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…tests vllm-project#60397 now checks a layer's MoE tensors before its kernels read them (MonoLayerWeights.check), because their buffer loads are unbounded. On gfx942 the kernels read the copies of weights942 instead: the routed experts without the loader's padding, as FP4 bytes in tile-major order, and the shared expert as FNUZ bytes. MonoLayerWeights.check_942 checks those copies, for the target's 384 experts and the DSpark draft's 128. tests/models/test_dsv41_mono_decode.py failed 7 of its 33 tests on this branch. The CDNA message is now "needs CDNA3/4", the layer mocks had no engram, and the FFN launch takes topk. The failure that needed the TP group is gone with the merge, because vllm-project#60397's _deployment helper sets the TP size. The run fixture now sets the row limit itself, so the tests do not depend on the GPU they run on. New cases cover gfx942: a step of 42 rows takes the kernels and a step of 48 rows does not, the window-only, index and draft layers, TP2, and a MoE backend other than AITER_MXFP4_BF16. On MI325X with the merged files, the CPU test passes (46 tests) and the gfx942 kernel tests pass (239 tests). Signed-off-by: Markus Hartikainen <markus.hartikainen@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com>
| sink = f32(H, gen=gs) | ||
| e, sh = moe.experts.routed_experts, moe.shared_experts | ||
| u8 = lambda t: t.view(torch.uint8) # noqa: E731 | ||
| runner = DSV41MonoLayer(tp, rank, get_tp_group().cpu_group, device) |
There was a problem hiding this comment.
one small question, I know we don't want to add more to this PR if we don't have to. But can we test the full DeepseekV4DecoderLayer, for a step? The DSV41MonoLayer helps run what a single gpu will see, but I'm a little nervous we don't run the full layer, we may miss something if a change is made to DeepseekV4DecoderLayer after your change.
There was a problem hiding this comment.
Good call. Added in 7c69067 and c25b7e9: tests/models/test_dsv41_mono_layers.py, in the same MI355 CI step (4 GPUs), at TP2 and TP4.
It serves a five-layer DeepSeek-V4.1-Flash (hf_overrides) holding both layer kinds this PR changes:
- layers 2 and 4, a ratio-2 and a ratio-1 consumer, run whole on the mono kernels;
- layers 0, 1 and 3 (the sliding-window layer and the KV / index sources) keep vLLM's attention and run the FFN launch.
The model runs twice on the same random weights, with VLLM_ROCM_MONO_DECODE on and off. Every DeepseekV4DecoderLayer output, x and the residual stream, must match at every decode token taken in a decode-only step. So it exercises the real forward: the hook, the per-step path choice, weights() off the layer, the metadata, and the top-k from the real indexer layers. It also checks that every layer took its mono path on every step. At TP4 it adds that size's own kernel builds, the padded routed intermediate (576 → 640) and the hand-offs across four ranks.
Details that made it reliable:
- Weights: plain dummy weights leave the E8M0 scales zero, so both paths agree trivially. A test-only loader fills checkpoint-range weights before post-load processing (with the routed experts' padding zero, as the loader keeps it). They're small enough that the random stack isn't chaotic: at larger scales, atomics' order alone moves vLLM's own logits run to run.
- Row matching: a request's row and step differ between engines, so rows are matched by prompt length and position.
- Bounds: layer 0 at a request's first decode token sees identical inputs on both paths and gets the op-level test's 1.5%. Deeper layers inherit small differences, which random weights amplify to 2–4%, so they get 8%.
- Routing near-ties: a token whose top-6 routing is a near-tie on vLLM's path may pick another expert. Once it does, it carries the flip in its residual stream, so its later layers aren't compared. Every other token is checked at every layer.
- Sensitivity: a routed scale off by 3% fails it at layer 0, at both TP sizes. A layer the kernels no longer follow would be off on every token.
It takes about 1.5 minutes at TP2 and 3 at TP4.
|
Excited to learn more about the kernel! 🚀 |
A five-layer DeepSeek-V4.1-Flash at TP2 with both layer kinds the mono path takes (the whole layer at a ratio-2 and a ratio-1 consumer, the FFN launch at the sliding-window and the KV / index source layers), served with VLLM_ROCM_MONO_DECODE on and off on the same random weights. Every decoder layer's outputs at every decode token of a decode-only step must match. Unlike the op-level numerics test, it runs DeepseekV4DecoderLayer.forward as the engine does, so a change to the layer that the kernels do not follow fails here. - A test-only loader fills weights in the checkpoint's ranges before post-load processing. Plain dummy weights leave the E8M0 scales zero, and both paths would agree on zeros. - Rows are matched by prompt length and position, since a request's step and row differ between engines. - Tokens whose top-6 routing is a near-tie on vLLM's path are exempt at that layer. - A routed scale off by 3% fails at layer 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
/ci run |
|
✅ Triggered Buildkite CI #93923 for commit |
- TP4 builds its own kernels, pads the routed intermediate (576 -> 640), and runs the hand-offs across four ranks. The random-weight loader now keeps the routed experts' padding zero, as the loader does. - A token that picks another expert at a near-tie (6th / 7th routing score within 5e-3 on vLLM's path) carries the flip in its residual stream. Its later layers are no longer compared; every other token is, at every layer. A routed scale off by 3% still fails at layer 0, at both TP sizes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fangzhou Ai <fangzhou.ai@amd.com>
|
/ci run |
|
❌ This PR is 4 commits behind upstream |
|
/ci run |
|
✅ Triggered Buildkite CI #93936 for commit |
dllehr-amd
left a comment
There was a problem hiding this comment.
Approving. We look well guarded and this will be a continual work in progress. Let's get the first piece in so future PRs have something to aim against. #60904 will be updated as we solidify the monokernel approach
- Gate on VLLM_ROCM_MONO_DECODE (the switch vllm-project#60397 adds), not a K3 flag. - Lay the kernels out like vllm-project#60397: attention/ (K1, the attention back), stages/ (MoE, GEMV), layer.py (K2), runner.py (state), with the vLLM glue in amd/mono_decode.py and the ATOM primitives merged into fewer common/ modules. Kernel code moves unchanged. - Turn the path off when a KV connector is configured: its copy kernels on another stream can hold CUs while every mono CTA spin-waits on the rest. - Run the numerics test as a dedicated 8x MI355 CI step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: hyukjlee <hyukjlee@amd.com>
- Set VLLM_ROCM_MONO_DECODE=1 (vllm-project/vllm#60397): two persistent launches a layer for decode steps of up to 48 rows; larger steps take the regular path. - Image TBD until the first ROCm 10.0 nightly with #60397 is published. - Drop the a4w4 notes and shorten the comments and the changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With SGLANG_ROCM_MONO_DECODE=1, decode steps of 1-48 rows run each layer's attention all-reduce, FFN mHC seam, MoE and MoE all-reduce as one persistent FlyDSL launch. The kernels in dsv41_mono/ are copied from vllm-project/vllm#60397 (from ROCm/ATOM#2479, MIT) and keep their licence notices.
Replace the TBD placeholder with nightly-rocm100-7d0b4e57, the first ROCm 10.0 nightly built after vllm-project/vllm#60397 merged, so the VLLM_ROCM_MONO_DECODE=1 setting on this recipe has kernels to run. 将 TBD 占位符替换为 nightly-rocm100-7d0b4e57,这是 vllm-project/vllm#60397 合并后构建的首个 ROCm 10.0 nightly,使本 recipe 中的 VLLM_ROCM_MONO_DECODE=1 设置有对应的 kernel 可用。 Co-authored-by: Cursor <cursoragent@cursor.com>
With SGLANG_ROCM_MONO_DECODE=1, decode steps of 1-48 rows run each layer's attention all-reduce, FFN mHC seam, MoE and MoE all-reduce as one persistent FlyDSL launch. The kernels in dsv41_mono/ are copied from vllm-project/vllm#60397 (from ROCm/ATOM#2479, MIT) and keep their licence notices.
… attention front Follow the conventions of the merged DeepSeek-V4.1 mono decode (vllm-project#60397) and RFC vllm-project#60904: - An explicit VLLM_ROCM_MONO_DECODE=1 that cannot run raises, listing every reason: CDNA version, TP8, PP, expert / data / context parallelism, EPLB, LoRA, speculative decoding, a KV connector, fewer than 256 compute units (a partitioned GPU). A weight layout the kernels cannot read raises at the first decode step. A model variant outside the kernels stays on vLLM's path, logged once. - Launch argument checks raise ValueError instead of asserting, so -O keeps them; the expert weights and scales are checked for their exact shapes. - CPU tests of the refusals and the per-step routing. The full-attention layers' front ran as ~38 eager kernels a layer, because this forward is not torch.compiled and the CustomOps resolve to their native paths. It now runs the fused Triton split / q, k norm / RoPE / gate kernel the model's CUDA path uses, and a compiled Gemma add + RMSNorm and sigmoid gate (compiled at init, not inside a graph capture). Tested against the model's eager ops. Signed-off-by: Mikko Tukiainen <Mikko.Tukiainen@amd.com> Co-authored-by: Cursor <cursoragent@cursor.com>
- The vLLM side moves to amd/mono_decode.py (MonoLatentMoE: which layers and steps take the launch, and the launch call), as vllm-project#60397 docks its kernels; ROCmLatentMoERunner keeps only the dispatch in _forward_impl. - Tests move under tests/models/kimi_k3/ (test_amd_mono_moe.py, test_amd_mono_gate.py), where the existing MI355 Kimi K3 step runs them, so this change needs no CI step of its own. - README sections follow vllm-project#60397 (Scope, Launch, Execution model and hand-offs, Numerics, Validation). Kernel code is unchanged. Signed-off-by: mghanimi <mghanimi@amd.com>
- Update the image from nightly-rocm100-ac9126e58aa7bbab1856ba6593ba4d5003fea516 to nightly-rocm100-7d0b4e57aac4c4323225b0eee9ae960ce75ffffe, the first ROCm 10.0 nightly with the DeepSeek-V4.1 mono decode kernels of vllm-project/vllm#60397, and update model.container to match. The image also brings vllm-project/vllm#59685, #56638, #60159 and #60389. - Enable the mono decode kernels (VLLM_ROCM_MONO_DECODE=1) at TP2 and TP4: a decode step of up to 48 rows, i.e. concurrency 1-8 with five DSpark drafts, runs each backbone layer as two persistent launches (30 of 40 layers whole, the other 10 the FFN launch only). Larger steps take the regular path, so C16-C64 are expected unchanged. - Enable VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=INT4 (vllm-project/recipes#1061) for tensor-parallel all-reduces above the quick-reduce size threshold. - Restore --max-num-batched-tokens 16384 at TP2 c64: vllm-project/vllm#58014 removed the profiling over-reservation the 8192 cap worked around. The prefill chunk is now 16384 at every point and the ladder stays at concurrency 1-64. - No data-type or precision change to the DSpark draft: vllm-project/vllm#60397 skips the draft layers (layer id >= num_hidden_layers), so the bundled DSpark draft of deepseek-ai/DeepSeek-V4.1-Flash runs on vLLM's regular path as shipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Purpose
Opt-in (
VLLM_ROCM_MONO_DECODE=1, meant as the one switch for every model's mono decode kernels) fast path for DeepSeek-V4.1-Flash decode on MI355X (gfx950). Every backbone layer of a decode step runs on persistent FlyDSL launches:All TP all-reduces run in-kernel over symmetric peer memory. K1 + K2 replace the ~40 kernels this path launches per standard layer today; the FFN launch replaces the attention all-reduce, the FFN seam and the MoE's kernels on the others.
Scope
fp8_ds_mlaKV records.Layout
stages/andcommon/are adapted from ROCm/ATOM's DeepSeek-V4.1 mono decode (MIT License, commit 97359d6). Each adapted file names its origin and keeps AMD's copyright notice. The changes from ATOM:mxfp8_e4m3_quantizeandsilu_and_mul_with_clamp;Design
wo_bis built withreduce_results=False, as the sequence-parallel path already does. A decode step hands the partial to the FFN launch; any other step reduces it in the decoder layer with the sametensor_model_parallel_all_reduce. ROCm's preshuffled-FP8wo_bpath now honorsreduce_resultsas the linear does. This also fixes a bug on main: with sequence parallelism and an FP8-block checkpoint,wo_bwas all-reduced and then reduce-scattered.get_cdna_version()inCDNA_VERSIONS, CDNA4 / gfx950 today, so a later generation is one entry once ported), TP other than 2 or 4, FlyDSL missing, expert / data parallelism or EPLB,VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4, a routed-expert backend other thanAITER_MXFP4_BF16, or fewer than 256 compute units (a partitioned GPU: every CTA of a launch must stay resident). The MoE and shared-expert tensors are checked for the kernels' shapes and dtypes at the first step, as the attention ones are. A layer outside the kernels' scope is logged once.flydslwheel, plus two small AITER helpers that path already depends on:aiter.ops.flydsl.kernels.buffer_ops(FlyDSL's wheel no longer ships it) andUncachedIpcHeapfor peer memory.Duplicate check
Test Plan
pytest tests/models/test_dsv41_mono_decode.py(CPU, wired into the MI355 Basic Models job): a decode step takes the mono path and hands the kernels[blocks, block, 584]cache views. A padded FULL-graph step takes it too (the kernels skip pad rows, whose slot is -1). PIECEWISE steps, prefill or mixed steps, more decode tokens than rows, non-causal windows, missing metadata, more than 48 rows and the first layer all take the original path. Deployments the kernels cannot run raise.pytest tests/models/test_dsv41_mono_numerics.py(4 × MI355, its own CI step). One layer with random weights in the checkpoint's formats, at TP2 and TP4: the kernels against the ops vLLM runs for the same decode step (the mHC seams, the ROCm attention chain, the all-reduce,DeepseekV4MoE). Each stage is checked on its own input, by cosine and relative error, for the whole layer and the FFN launch, at 1×6 and 8×6 rows. Then both widths' launches are captured in one graph and replayed 300 times, bit for bit against the first run.pytest tests/models/test_dsv41_mono_layers.py(same CI step). A five-layer DeepSeek-V4.1-Flash at TP2 and TP4 holding both layer kinds the mono path takes: the whole layer at a ratio-2 and a ratio-1 consumer, and the FFN launch at the sliding-window and KV / index-source layers. It is served with mono on and off on the same random weights, and every decoder layer's outputs at every decode token must match. It runsDeepseekV4DecoderLayer.forwardas the engine does, so a change to the layer that the kernels do not follow fails it.COMPILE_ONLY=1) of the in-tree kernels: K1 and K2 at TP2 and TP4, M = 6 and 48, both layer types.MHCPreDelayedOp,DeepseekV4MoEwith its all-reduce) on the real weights of the 10 layers, with the attention partial from a real ROCm attention chain. 48 points: TP2/TP4 × concurrency 1/2/4/8 × 1–6 tokens per request; the 10 layers chained in one HIP graph, cold weights.DeepseekV4MoE, the mHC seams, the ROCm attention chain, the all-reduce) in a TP process group, with real checkpoint weights. 96 points: TP2/TP4 × concurrency 1–8 × 1–6 tokens per request × the two layer types. Timing: 8 layers chained in one HIP graph, cold weights, vLLM's path captured undergraph_capture.reasoning_efforthigh, 8 requests in flight a server (every decode step within 48 rows), against the same build withVLLM_ROCM_MONO_DECODE=0. Long context: RULER NIAH (single, multikey 2 and 3) at 32K, 128K and 256K.Test Result
Unit: 33 passed. Compile-only: all K1 / K2 / FFN builds compile.
Numerics test (GPU): passed at TP2 and TP4: every token's output cosine ≥ 0.9993 against vLLM's chain, relative errors 0.3–0.5% (bounds 1–1.5%; a routed scale off by 3% fails), 300 replays bit-exact. Race stress: no mismatch in 100K replays on each layer. Before the per-width scratch, layer 3 diverged at replay 13 on every run. Model-level test: passed at TP2 and TP4 (9 of 9 local runs of the final version; 40 of 41 of the TP2-only one before it, the failure not recurring and its log not kept). A routed scale off by 3% fails it at both sizes.
Accuracy, TP2, per RFC #59665 (details):
VLLM_ROCM_MONO_DECODE=0;reasoning_efforthigh, temperature 0.6, 8 requests in flight a server;RULER NIAH, long context (details): single, multikey 2 and multikey 3 at 32K, 128K and 256K, 40 samples a cell. Every cell is 40/40 for both mono and the reference.
FFN launch, kernel level, µs per layer (vLLM's FFN half → the FFN launch, at 6 tokens per request). Every point is faster or at parity: TP2 1.01–2.14×, TP4 1.12–2.32×. The residual and the MoE input match vLLM's bit for bit at every point; the layer output's cosine is ≥ 0.99999, and every token's ≥ 0.9999.
Kernel level, µs per layer. Every one of 96 points is faster, 1.10–2.18×. Accuracy: residual cosine ≥ 0.99999. MoE output: 1,234 of 1,260 tokens at cosine ≥ 0.999. Each of the other 26 is a top-6 routing near-tie: the reference's own 6th/7th expert score gap is ≤ 3.2e-3, against medians near 1e-2. On the same FFN input the MoE matches vLLM's at cosine 0.99999.
GSM8K (strict / flexible; DSpark mean acceptance length):
AgentX with the FFN launch (all 40 layers) vs K1 + K2 alone (30 layers). Same recipe and protocol. AgentX replays a fixed request sequence, so the runs' requests match one to one; the last column is the median ratio of a request's inter-token latency (FFN launch / K1 + K2).
Decode on the same requests is 7–9% faster at every point. TP2 is a re-run: the first TP2 round hit node-wide engine stalls of 20–40 s, on all four servers at the same moments, that cost its throughput. Against InferenceX's run, the FFN launch reaches 1.56× / 1.46× / 1.47× / 1.30× p90 interactivity and 1.15× / 1.14× / 1.11× / 1.05× total throughput at TP2 c1 / c2 / c4 / c8, and 1.66× / 1.56× / 1.64× / 1.38× and 1.19× / 1.20× / 1.14× / 1.04× at TP4. The tables below are K1 + K2 alone.
AgentX, TP2. Against the same-node mono-off baseline, run the same way (four points at once on one node):
AgentX, TP4. Against InferenceX's own run of the same image and recipe (run 37510490434). The TP2 same-node baseline above matches InferenceX's TP2 run within a few percent, so InferenceX serves as the TP4 reference.
Provenance of the numbers. The kernel, GSM8K and AgentX numbers come from these kernels while they were hosted in AITER during development (ROCm/aiter#6196). Moving them here changed only import paths, the merge of a few small helper modules, and formatting. The kernel-level accuracy check re-run against this in-tree copy (TP2 layer 21 M=48, TP2 layer 3 M=6, TP4 layer 21 M=48) reproduces the earlier results exactly, to six decimals of cosine. The TP4 GSM8K and every FFN-launch number come from the in-tree code. The kernel, GSM8K and AgentX numbers predate the per-width scratch. It leaves kernel time unchanged: 24 TP2/TP4 points, mean ratio 1.000, range 0.996–1.009.
AI assistance
This change was written with AI assistance (Claude Code). It is opened as a draft for the submitter to review every changed line before it is marked ready.
🤖 Generated with Claude Code