Skip to content

[ROCm][Perf][Qwen3.8] Dense mono decode on 128-CU gfx950 (MI350P) - #65078

Draft
jhartika-amd wants to merge 24 commits into
vllm-project:mainfrom
jhartika-amd:rocm/qwen38-dense-mono-128cu
Draft

jhartika-amd wants to merge 24 commits into
vllm-project:mainfrom
jhartika-amd:rocm/qwen38-dense-mono-128cu

Conversation

@jhartika-amd

@jhartika-amd jhartika-amd commented Nov 2, 2026 •

Copy link
Copy Markdown

Overview

Run #61573's Qwen3.8-27B dense mono decode (K1: GDN mixer, K2: MXFP4 FFN) on 128-CU gfx950 (MI350P), and on MTP verify steps. Stacked on #61573: the first two commits are #61573's, review the rest. Draft until #61573 lands.

Claims

Validation

Setup: 1x MI350P (128 CUs), TP1, vLLM v0.32.0 + AITER main with the MI350P enablement PRs, FlyDSL 0.4.1, this branch's vllm/ files overlaid. Before is the same image with the flag unset.

VLLM_ROCM_USE_AITER=1 VLLM_ROCM_USE_AITER_FP4_ASM_GEMM=1 VLLM_ROCM_MONO_DECODE=1 \
vllm serve amd/Qwen3.8-27B-Quark-AWQ-MXFP4 --tensor-parallel-size 1 \
  --attention-backend ROCM_AITER_UNIFIED_ATTN --gdn-prefill-backend aiter_flydsl --kv-cache-dtype fp8 \
  --reasoning-parser qwen3 --max-model-len 262144 --max-num-seqs 128 --max-num-batched-tokens 32768 \
  --gpu-memory-utilization 0.92 --no-enable-prefix-caching
# MTP-3: add --speculative-config '{"method":"mtp","num_speculative_tokens":3}'

Performance

sglang.bench_serving, random, 8k / 1k, range ratio 0.8; 8 / 16 / 32 / 64 prompts at concurrency 1 / 2 / 4 / 8. Default row caps (4).

Conc Out tok/s, MTP off TPOT p50 Out tok/s, MTP-3 TPOT p50
1 104.6 -> 124.6 (+19.1 %) 9.21 -> 7.67 ms 223.7 -> 268.2 (+19.9 %) 3.98 -> 3.29 ms
2 196.4 -> 225.5 (+14.8 %) 9.83 -> 8.52 ms 353.3 -> 366.4 (+3.7 %) 5.22 -> 5.10 ms
4 344.3 -> 385.6 (+12.0 %) 11.05 -> 9.84 ms 592.7 -> 592.1 (-0.1 %) 5.97 -> 5.98 ms
8 571.8 -> 574.6 (+0.5 %) 13.19 -> 13.19 ms 930.4 -> 932.9 (+0.3 %) 7.67 -> 7.65 ms

A second prompt seed at concurrency 1 agrees within 1.2 pt. Above the caps nothing changes: concurrency 16 / 32 / 64 +0.4 to +0.5 %, 128k input at 4 / 8 +1.6 / -0.1 %.

python3 -m sglang.bench_serving --backend vllm --dataset-name random --random-input-len 8192 \
  --random-output-len 1024 --random-range-ratio 0.8 --request-rate inf --max-concurrency 1 \
  --num-prompts 8 --warmup-requests 1 --seed 21 --model amd/Qwen3.8-27B-Quark-AWQ-MXFP4

Accuracy

Eval Before After
GSM8K, model card setting, thinking (mean of 3 seeds; paired -0.18 +- 0.36 pt) 0.9333 0.9315
GSM8K, model card setting, non-thinking (mean of 3 seeds; paired -0.45 +- 0.45 pt) 0.8987 0.8941
GSM8K chat, 5-shot, 250 questions, flexible / strict 0.920 / 0.916 0.932 / 0.916
GPQA Diamond, thinking, 198 questions 0.773 0.768
RULER niah_single_2 64k / 128k, 50 samples 1.000 / 1.000 1.000 / 1.000
MTP-3, GSM8K model card setting, thinking (paired +0.53 +- 0.67 pt) 0.9265 0.9318
MTP-3, GSM8K model card setting, non-thinking (paired -0.68 +- 0.80 pt) 0.8961 0.8893

MTP off at 8 concurrent with row caps 8, so K1 and K2 produce nearly every token; MTP-3 one request at a time. Model card setting: all 1319 questions, sampled (thinking: temperature 1.0, top_p 0.95, top_k 20; non-thinking: the card's gsm8k_nothink task), the same request seed in both arms of a pair.

lm_eval --model local-completions --tasks gsm8k --num_fewshot 5 \
  --model_args model=amd/Qwen3.8-27B-Quark-AWQ-MXFP4,base_url=http://localhost:8000/v1/completions,tokenizer=amd/Qwen3.8-27B-Quark-AWQ-MXFP4,num_concurrent=8,tokenized_requests=False,max_length=16384,seed=2004 \
  --gen_kwargs max_gen_toks=3072,temperature=1.0,top_p=0.95,top_k=20,do_sample=True

Tests

pytest tests/models/qwen3_5/test_amd_dense_mono_tiling.py   # CPU only: 55 passed
pytest tests/models/qwen3_5/test_amd_dense_mono.py          # MI350P

test_amd_dense_mono.py on MI350P: 10 passed, 231 subtests (K2 TP1 / TP2, K1 TP1 / TP2, K1 on verify steps, all bit-exact). 16 TP2 cases with K1's out_proj skip (below), and the fused-MTP test skips on this base; on a recent nightly with #61855 it passes too (11 passed, 295 subtests). Compile-only (COMPILE_ONLY=1 ARCH=gfx950): all 80 K1 / K2 configurations at 128 and 256 CTAs, no VGPR spills or scratch. The MI355 Buildkite job also runs the GPU tests with VLLM_ROCM_QWEN38_DENSE_MONO_GRID=128.

Details

Why #61573 does not run on 128 CUs. K1 and K2 run one CTA per CU, all resident, exchanging data through spin-waited mailboxes, so the grid must equal the CU count; #61573 hard-codes 256 (a 256-CTA grid deadlocks on 128 CUs), and several stage layouts assume one task per CTA.

Change.

  • mono/tiling.py (new, pure Python) picks the grid from the CU count (256 or 128, else a clear error) and holds the per-stage pass plans with their invariants. The grid is a build-key field; 128-CTA kernels end in _g128.
  • Stages with more tasks than CTAs take more passes; every task is computed as before. At 256 every stage is one pass and the code objects are unchanged.
  • K2 probes the installed AITER's split-K once at startup (current AITER gives 2 / 2 where [ROCm][Perf][Qwen3.8] Add dense mono decode for Qwen3.8-27B on MI355X. #61573 hard-codes 4 / 4, so [ROCm][Perf][Qwen3.8] Add dense mono decode for Qwen3.8-27B on MI355X. #61573's startup self-check refuses to start).
  • K1 is bit-exact with the current stock GDN decode: fp32 beta ([Bugfix][Kernel] Keep packed GDN decode beta in FP32 #53877) and the gated norm's CU-dependent reduction layout.
  • MTP (TP1): K1 runs each request's 1 + k tokens in order (conv window from the last accepted token, per-token state slots), K2 the rows; only whole verify steps enter, from tensor shapes, no host sync. Where vLLM serves verify steps with the fused MTP kernel, K1 takes its reduction orders (K1Build.spec_fused).

Limitations.

Not a duplicate. Closest is #63341 (DSv4.1 mono, draft), which refuses 128 CUs pending the task decomposition this PR does for the Qwen3.8 dense kernels.

This PR was created with AI assistance.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

abrahamzewoudie and others added 11 commits October 14, 2026 06:16
Co-authored-by: Cursor <cursoragent@cursor.com>
…, 128)

K1 and K2 run one resident CTA per compute unit and were laid out for a
256-CU MI355X. Make the grid width a build parameter taken from the device
and add 128-CTA stage plans for 128-CU gfx950 parts (MI350P):

- mono/tiling.py (pure Python): resolve_grid() picks the width from the CU
  count, refuses counts without a validated plan, and accepts
  VLLM_ROCM_QWEN38_DENSE_MONO_GRID to run a validated width narrower than the
  GPU. FGeo / Geo move here with the grid as a field, plus the per-stage
  pass plans and their invariants (checked at import for 256 and 128).
- FfnBuild.grid / K1Build.grid: part of the JIT key; the 128-CTA kernels get
  a _g128 symbol suffix, the 256-CTA names are unchanged. A build whose grid
  exceeds the device's CUs raises instead of hanging.
- Stages whose tasks outnumber 128 CTAs take more passes a CTA, each task
  computed as before: o_proj / K1 front / TP2 out_proj owners 128 + b in a
  second pass; in_proj_qkvz 8 groups a CTA in two passes of 4 (TP1); GDN in
  passes of whole tokens when a CTA has more than 24 tasks; K2 down's two
  whole row groups, then the split group's 8 rows; gate_up 8 whole groups
  and 8 columns of a split one. Barriers between passes where the next one
  rewrites LDS the previous one reads.
- On 256 CTAs every stage is a single pass and the traced program is the
  same as before (offsets of 0 emit nothing; the down store keeps its
  original form).
- dense_mono: refuse with the resolver's message instead of the < 256 CU
  check, and pass the grid into both build keys.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
test_amd_dense_mono_tiling.py needs no GPU, FlyDSL or torch: it replaces
the device query, checks the grid choice and refusals, pins the 256-CTA
plan to the layout the kernels were written for, and replays every stage's
task placement at 256 and 128 CTAs to check that each task runs exactly
once and that no GDN pass waits on a later one.

The GPU numerics tests build for the device's grid, so they run on a
128-CU part, and on a 256-CU one with VLLM_ROCM_QWEN38_DENSE_MONO_GRID=128.
README: the grid width and the 256 vs 128 stage table.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
At TP2 on 128 CTAs a pass covers 4 qkvz groups, and above 8 rows its
outputs (4 groups x 16 rows x S) outnumber the 512 threads, so the rows
past the first 512 were never written. Write them in rounds of THREADS
(Geo.qkvz_out_rounds). Every 256-CTA build and TP1 on 128 CTAs need one
round and keep the original single-round code.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…t fixes

K2 is bit-exact with the stock Triton gemm_afp4wfp4 only when it uses the
same K splits. The counts were fixed (gate_up 4, down 4 / 2, o_proj 4), but
they come from AITER's get_splitk and config tables: AITER main splits
gate_up and TP1 down in 2 and TP2 down in 1 at up to 8 rows, so the startup
self-check (bit-exact) refused to start. K2 now probes the stock GEMM's split
count per width once (stock_split_fields, before any graph capture) and
builds with it (FfnBuild.gu_split / d_split / o_split; 0 keeps the default,
so builds on an AITER that agrees are unchanged). A count other than 1, 2 or
4 refuses with the reason.

Tests: enter a VllmConfig with every custom op on for the module (the
references construct SiluAndMul and RMSNormGated, which assert without one,
and the kernels follow the ops' ROCm kernels, not the PyTorch fallbacks);
import the Gemma norm functions by name (the ops package re-exports a
function that shadows the module attribute); StockSplitTest checks that the
build's split counts are the stock GEMM's on random data.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…ock model

With Model Runner V2 the piecewise cudagraphs of the compiled stock model
are captured by dummy steps of each width, and those dummy steps look like
pure decode, so the mono graph took them over and the stock piecewise
graphs for widths up to the cap were never captured. The first prefill or
mixed step of such a width (a 5-token prompt pads to 8) then tried to
capture at run time on the default stream and the engine died
("CUDA graphs must be captured on a non-default stream").

Skip the mono graph when the forward context's cudagraph runtime mode is
PIECEWISE. Pure decode steps run under FULL cudagraphs (or eagerly) and keep
taking the mono graph.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
- SPDX copyright lines in dense_ffn.py / dense_gdn.py, ruff import order
  and format, E741 in runs_total, and a markdown line that started with
  "vllm-project#57560" (read as a heading) in the mono README.
- tiling: torch.accelerator.current_device_index() instead of a
  torch.cuda call; d_pass_tiles returns an explicit pair.
- The Qwen3.8 dense mono Buildkite job also runs
  test_amd_dense_mono_tiling.py (CPU only).

No kernel changes: the 80 compiled K1 / K2 code objects at 256 and at 128
CTAs are byte-identical before and after.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
At TP1 a CTA of the 128-CTA K1 has 6 S GDN tasks (48 at 8 rows). They ran
in passes of at most 24 tasks (the widest pass of the 256-CTA build), so
S > 4 took two passes, the second one's SSM state loads serialized behind
the first. One pass of up to 48 compiles without spills (247 VGPRs at 8
rows, 59 KB LDS) and is faster on MI350P: K1 at 8 rows 62.0 -> 57.9 us,
at 6 rows 54.4 -> 51.9 us, at 5 rows 50.5 -> 48.5 us (CUDA-graph replay).
Outputs are bitwise identical to the multi-pass build (checked at 5 and 8
rows against one token a pass).

GDN_PASS_TASKS is now per TP: 48 at TP1, 24 at TP2 (one TP2 pass of 48
compiles but spills up to 127 SGPRs; not measured). The 256-CTA builds are
unchanged (byte-identical code objects); the CPU tests cover the pass
split at smaller caps.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
TP1 on one MI350P, same image for both arms, MTP off: throughput and
TPOT at 8k / 1k concurrency 1-8 with row caps 8 and with the default
caps, and GSM8K / GPQA at 8 concurrent requests.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
The MI355 job runs test_amd_dense_mono.py a second time with
VLLM_ROCM_QWEN38_DENSE_MONO_GRID=128, so the 128-CTA (MI350P) builds are
covered on the existing 256-CU runner.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
@mergify mergify Bot added ci/build new-model Requests to new models qwen Related to Qwen models rocm Related to AMD ROCm labels Nov 2, 2026
@github-project-automation github-project-automation Bot moved this to Todo in AMD Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify

mergify Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @jhartika-amd.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Nov 2, 2026
Jouni Hartikainen added 9 commits November 3, 2026 16:50
…nd NIAH

The MI350P throughput table now compares against the same image with
the flag unset. Adds RULER NIAH 64k / 128k and GSM8K in the model card's
sampled setting (three seeds per arm).

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
K1's state-slot mapping (copy, then null slot 0 -> -1) ran for every GDN
layer: three small launches x 48 layers a decode step. GDN layers of a
KV-cache group share the indices, so map each group's once, starting
over at the first GDN layer of a step. On MI350P at one request this
removes 144 launches and 0.5 ms of GPU time a step (8.6 -> 8.1 ms).

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
On 128 CTAs K1's front ran the input norm through 160 owner tasks (a
second pass on CTAs 0..31) and two grid-wide hand-offs: the
sum-of-squares partials, then the quantized x blocks. Up to 4 rows every
CTA now runs the norm on the whole rows itself with K2's run_norm, which
reduces in the owners' order, so x is bit-identical (in_proj_qkvz stays
bit-exact with the stock GEMM) and no CTA waits on another before
in_proj. Wider steps keep the owners: the kept rows spill there.

K1 on MI350P (CUDA-graph replay): 30.3 -> 27.6 us at 1 row, 35.1 ->
32.3 us at 2, 43.5 -> 40.5 us at 4. The 256-CTA code objects and every
K2 code object are unchanged.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
VLLM_ROCM_MONO_DECODE refused speculative decoding because K1 takes one
token a request. K2's rows are independent, so at TP1 the mono graph now
runs with K1 off: each GDN layer is the compiled stock layer up to
out_proj's input (a view of the module whose out_proj passes its input
through), and K2 takes the verify step's rows with out_proj folded.
TP2, whose graph runs K1 on every GDN layer, still refuses.

Also fixes the TP1 path for a GDN layer above K1's row cap: the hook that
should skip out_proj there was never installed, so K2 got out_proj's
output instead of its input. It is now a context manager around the
eager stock call.

MI350P, MTP-3, one request, same image: 5.42 -> 5.05 ms a token, the
acceptance rate unchanged.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…ecode

K1's strict test failed on this stack (about one bf16 ulp in a few
percent of the outputs). Three causes, none in the 128-CTA decomposition:

- beta: K1 rounded sigmoid(b) to bf16, as the stock packed-decode kernel
  did before vLLM vllm-project#53877 ("Keep packed GDN decode beta in FP32"); it now
  stays fp32. This also applies to the 256-CTA build.
- gated norm: the stock RMSNormGated launch takes two rows a program once
  S x nv rows exceed 2 x the GPU's CUs (FLA calc_rows_per_block), which
  changes its reduction: a thread's 4 columns as two pairs added
  elementwise, then 32 threads with xor 16, 8, 4, 2, 1. On 128 CUs that is
  6 rows and up at TP1 (11 and up at TP2). K1Build.norm_rows (from the
  device's CUs, tiling.gated_norm_rows) selects that reduction.
- TP2 out_proj: K1 hard-coded 4 K splits; the installed AITER's are now
  probed (K1Build.o_split, as K2's). Current AITER splits it in 3, which
  K1 cannot reproduce, so dense_mono declines TP2 with K1's out_proj and
  the oproj test skips those widths.

On MI350P: test_amd_dense_mono.py 9 passed, 167 subtests passed (K1 TP1 at
every width 1-8 and TP2 with stock out_proj now bit-exact), 16 skipped.
K2 code objects unchanged.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
With speculative decoding K1 was off: a verify step runs each request's
1 + k tokens through the GDN conv and recurrence in order, from the state
after its last accepted token, with a state slot a token and a widened
conv state. K1Build.spec now does that: a GDN task is a request's tokens;
the conv slides its window from offset n_acc - 1 and rewrites the widened
state; the recurrence starts from slot idx[r, n_acc - 1] and stores each
token's state to idx[r, j]; null slots (<= 0) are skipped. The accepted
counts travel in the w_os argument (unused without out_proj), so the
kernel ABI and every non-spec code object are unchanged.

dense_mono takes vLLM's persistent spec-decode buffers as they are (no
per-step copies) and enters only whole verify steps (requests x (1 + k)
== rows); warm-up and other steps run stock. A startup self-check compares
K1 on verify steps with the stock spec-decode kernels and fp32.

Against the stock spec path the conv state, res_out and the slots written
are exact; the recurrence keeps the decode kernel's reductions, so its
outputs agree with fused_sigmoid_gating_delta_rule_update within fp32
rounding (DenseGdnSpecTest). MI350P, MTP-3, one request: 5.42 (MTP) ->
5.05 (K2 only) -> 4.50 ms a token, acceptance unchanged.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…ck spec path

The stock spec-decode recurrence (fused_sigmoid_gating_delta_rule_update)
launches 4 warps; its q / k l2-norm sums an element a lane over two waves
(each wave in the DPP order, then the two added), where the decode kernel
K1 mirrors sums two elements a lane over one wave. Read from its TTGIR;
the state reductions, gates and update already match. K1's spec path now
reduces that way, and DenseGdnSpecTest asserts exact equality: res_out,
core, the conv state and every state slot, 1 and 2 requests of 4 tokens
and a padded request. Non-spec code objects unchanged.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…ing (TP1)

With MTP-3 a verify step is 4 rows a request. On MI350P, K1 and K2 beat
the stock path on verify steps of one and two requests (4 and 8 rows):
8k / 1k output throughput +19.3 % at one request and +10.2 % at two with
caps 8, against +3.7 % at two with the default caps (4), which leave
those steps stock. Plain decode steps keep the 4-row default.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
@jhartika-amd
jhartika-amd force-pushed the rocm/qwen38-dense-mono-128cu branch from 591a94d to 068b383 Compare November 4, 2026 12:01
Jouni Hartikainen added 4 commits November 4, 2026 15:16
…rify steps

8k / 1k output throughput with MTP-3, flag unset vs set (K1 and K2 on
verify steps, caps 8): +19.4 / +19.7 % at one request, +10.2 % at two,
unchanged at four and eight. GSM8K in the model card's setting at one
request: thinking +0.53 +- 0.67, non-thinking -0.68 +- 0.80 points,
paired. The README no longer says speculative decoding runs stock.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…ernel's numerics

Main serves ROCm MTP verify steps with the fused Triton GDN decode kernel
(fused_gdn_decode_post_conv_mtp, vllm-project#61855) where it applies, in place of the
sigmoid-gated delta rule kernel + RMSNormGated that K1's spec mode
reproduces. Launched, that kernel keeps the decode kernel's state layout
(4 columns a lane, 32 lanes a row), so the recurrence is unchanged; two
reductions differ:

- q / k l2 norm: an element a lane over two warps, each warp summed with
  DPP row_shr 8..1, row_bcast 15 / 31 and the first step contracted into
  an fma by the upper lane (scan_dot), the two warp sums added.
- gated norm: the sum of squares of the bf16 o in tiles of 32 rows (rows
  16 apart contracted, then xor 8, 4, 2, 1), the four tiles added
  (t0 + t2) + (t1 + t3).

K1Build.spec_fused selects them; dense_mono sets it when the GDN layers run
verify steps on the fused kernel (enable_fused_gdn_spec_decode), and the
startup self-check then compares against that kernel. stock_fields refuses
launches the fused kernel would tile wider than 32 rows (requests x value
heads >= CUs; not reachable within the 8-row caps at TP1).

DenseGdnSpecTest::test_verify_steps_fused (skipped where vLLM lacks the
kernel) is bit-exact on MI350P against it on a nightly stack: core, every
state slot, the conv state, res_out and a padded request, 1 and 2
requests x 32 draws; test_verify_steps still passes there and on the base
stack. All other code objects are unchanged (80 / 80 at 128 and 256 CTAs,
and the non-fused spec builds).

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
…ive decoding

Reverts the 8-row default caps under speculative decoding. Whether K1 + K2
beat the stock path on an MTP-3 verify step of two requests (8 rows)
depends on the stock MXFP4 decode GEMM: with the AITER ASM GEMM, 8k / 1k at
two requests ran +10.2 % with caps 8 on MI350P, but with AITER's FlyDSL
A4W4 decode GEMM the same steps ran about 5 % slower than stock (412.6 /
407.4 vs 432.8 / 433.4 tok/s; caps 4: 431.8). One request (4 rows) gains
on both (+19 % / +6 % output throughput) and is inside the default caps.
VLLM_ROCM_QWEN38_DENSE_MONO_K1_MAX_ROWS / _K2_MAX_ROWS=8 still select the
wider steps; the README gives both measurements.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>
… rows)

With o_proj folded, K2's norm took two grid-wide hand-offs after the o_proj
owners: the sum-of-squares partials to the owners, then each owner's fp4 x
to every CTA. On 128 CTAs up to 4 rows the owners now publish their bf16
o_proj outputs once and every CTA gathers the rows, adds the residual from
memory and runs the norm itself with run_norm (the reduction order the
owners used), as K1's front does. Bit-exact: DenseFfnTest passes.

MI350P, one launch: 66.9 -> 65.5 us at 1 row, 69.9 -> 67.0 at 2, 70.2 ->
69.2 at 4. At 5-8 rows the owners stay faster (72.9 vs 73.4 us at 5, 78.1
vs 79.8 at 8), so wider steps keep them. Only those four 128-CTA builds
change; the 256-CTA code objects and the other 128-CTA builds are
byte-identical, no spills.

Signed-off-by: Jouni Hartikainen <jhartika@amd.com>

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

Labels

ci/build needs-rebase new-model Requests to new models qwen Related to Qwen models rocm Related to AMD ROCm

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants