Skip to content

perf(minimax): scatter-grouped i8-WMMA + WMMA-dense prefill (gfx1151) - #479

Closed
fivetide wants to merge 9 commits into
warpfront:masterfrom
fivetide:minimax-prefill-perf
Closed

perf(minimax): scatter-grouped i8-WMMA + WMMA-dense prefill (gfx1151)#479
fivetide wants to merge 9 commits into
warpfront:masterfrom
fivetide:minimax-prefill-perf

Conversation

@fivetide

@fivetide fivetide commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

MiniMax-M2 prefill optimizations on gfx1151 (Strix Halo, RDNA3.5), validated
on MiniMax-M2.7.mq2 (256 experts / top-8, mixed k-map gate_up=MQ2-Lloyd /
down=MQ3-Lloyd). Two compounding wins on the batched prefill path:

  1. WMMA dense projections — q/k/v/o/router move from the scalar
    gemm_q8_0_batched (1-wave-per-row) to gemm_q8_0_batched_chunked
    gemm_q8_0_wmma. (~2× the projection cost; rocprofv3 then showed MoE = 93%
    of prefill.)
  2. Scatter-grouped MoE — the indexed-batched GEMV re-reads each expert
    weight once per routed token; the new path scatters tokens by expert and
    runs a WMMA grouped GEMM (each weight read once per chunk), with an
    i8-WMMA variant on gfx1151 (2-bit/3-bit Lloyd → int8 codebook LUT, i8
    WMMA at ~2× FP16). Kernel selection (i8 vs FP16) is resolved in the
    rdna-compute dispatcher by arch; the forward pass is arch-agnostic.

Also enables the MQ3-Lloyd indexed-batched down path so the mixed-k-map
MiniMax-M2.7.mq2 model can use batched prefill at all (it previously fell back
to per-token decode_step prefill).

The grouped i8 kernels are .gfx1151.hip builtins and arch-gated; other WMMA
arches take the FP16 grouped path, non-WMMA archs the indexed path. No env
flags — all unconditional.

Prefill metrics (MiniMax-M2.7.mq2, gfx1151, 2048-token prompt, fresh process, median)

path tok/s
master — per-token decode_step prefill (mq3-down had no batched support) ~25
this PR — indexed-batched (small chunks) ~70
this PR — scatter-grouped i8-WMMA (chunk 512, default ≥256 rows) ~235 (warm; 185 cold-warm)

3.4× over indexed-batched within the same run; ≈ 9× over master's actual
path
for this model (which couldn't batch mq3-down prefill).

Decode metrics (before/after)

Unchanged — this is a prefill-only PR. It does not touch decode_step; AR
decode ≈ 25 tok/s before and after.

Correctness / validation

  • scripts/coherence-gate-minimax.sh (new) drives the real serving path:
    prompts as chat messages → daemon renders the MiniMax chat template → model
    answers with natural EOS. Runs short (indexed) + a ≥256-token passage
    (grouped-i8). GATE: PASS (7/7 coherent, max single-token freq ≤ 0.09,
    no attractor/leak/loop, 0 GPU faults).
  • Grouped vs indexed final-logits: argmax matches; cosine 0.998 (FP16 grouped) /
    0.995 (i8 grouped) — the i8 path is numerically close and coherence-clean
    (the same FP16/i8 grouped machinery deepseek4 ships).
  • A real OOB was found + fixed during bring-up: grouped m_total_max was not
    16-aligned for odd batch (k_top=8), so an odd-length last prefill chunk left
    the kernel grid one tile longer than the expert_tile_ids buffer → GPU page
    fault. Fixed via next_multiple_of(BLOCK_M).

Notes

🤖 Generated with Claude Code


Rebased onto current master (2026-07-07)

Was 97 commits behind master; rebased clean onto master (no conflicts). Re-validated on gfx1151 (MiniMax-M2.7.mq2):

  • cargo build --release --workspace --lockedgreen (exit 0).
  • scripts/coherence-gate-minimax.shPASS (production indexed-short + grouped-i8 long path, no attractors, 0 GPU faults).

Merge note — deepseek4 kernel-selection skew vs #476

This branch carries a cherry-pick of #476's deepseek4 i8 path. Since then, #476 evolved: deepseek4 Q8_0 kernel selection moved into the rdna-compute dispatcher (gemm_q8_0_wmma_prefill_auto) and the i8-MoE gate became default-on (dropping the HIPFIRE_DEEPSEEK4_MOE_I8=1 opt-in). Whichever of #476/#479 merges second should take the newer (dispatcher-based, default-on) deepseek4 side and keep this PR's genuinely-new gemm_mq3g256_lloyd_moe_grouped_mmq_gfx1151 minimax kernel. (Validated in a local #480→#476→#479 integration stack — builds green, minimax coherence PASS.)

@fivetide
fivetide requested review from Kaden-Schutt, nwoolmer and unverbraucht and removed request for nwoolmer June 30, 2026 04:55
Bjoern Agent and others added 9 commits July 7, 2026 09:50
Cherry-pick of the 3 additive kernel commits from upstream PR warpfront#476
(d1f6115, 80920ce, 586ab0c) — NOT a merge, since warpfront#476 branched off
pre-minimax-perf master and would delete this branch's spec.rs + the
mq3-lloyd batched-verify kernel.

Brings in:
- gemm_mq2g256_lloyd_moe_grouped_mmq.gfx1151.hip — i8-WMMA grouped GEMM
  for MQ2-Lloyd MoE experts (+ widened weight-index loads)
- gemm_q8_0_mmq_4w.gfx1151.hip — i8-WMMA 64x64 dense Q8_0 GEMM
- rdna-compute gemm.rs/kernels.rs wrappers
- deepseek4 opt-in wiring (HIPFIRE_DEEPSEEK4_MOE_I8 / _Q8_I8, default-off)

Foundation for porting grouped-GEMM MoE to minimax prefill (the 93%
bottleneck per rocprofv3). No active minimax/qwen path changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tok/s)

forward_batch_hidden ran q/k/v/o/router through gemm_q8_0_batched (scalar
1-wave-per-row). Switch to gemm_q8_0_batched_chunked, which on gfx1151
(and all wave32-WMMA archs) routes to gemm_q8_0_wmma — documented as ~65%
of A3B prefill GPU time when left scalar. Also lifts the B>64 forward_batch
cap (attention/MoE kernels take B as a grid dim; chunked GEMM tiles the
projections internally) so larger prefill chunks are now valid.

Bench (MiniMax-M2.7.mq2, gfx1151, 2048 tok, chunk 64, fresh process,
median of 3): 34.4 → 70.5 tok/s = 2.05x. Chunk size is neutral (73→69
across 64→1024) — the win is the kernel, not amortization.

Correctness: forward_batch vs sequential decode_step cosine 0.9999
(B=2/4/8); chunk-64 vs chunk-512 cosine 1.000000; real-prompt greedy
generation fluent (Rayleigh-scattering answer, no attractor).

rocprofv3 now shows MoE indexed-GEMV = 93% of prefill (gate_up 55.6% +
down 37.4%); dense projections dropped to 4.5%. Grouped-GEMM MoE is the
next lever.

Adds minimax_prefill_bench example (chunk sweep + --verify + MINIMAX_GEN
coherence mode) mirroring the daemon prefill path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The indexed-batched MoE GEMV re-reads each expert weight ONCE PER ROUTED
TOKEN — at 256 experts/top-8 it was 93% of prefill (rocprofv3). Add a
scatter-grouped path in forward_batch_hidden that reads each expert
weight ONCE PER CHUNK via WMMA grouped GEMM: scatter (moe_scatter_fused_k8)
→ grouped gate_up (mq2-lloyd WMMA) → unscatter → AWQ silu·mul·rotate →
grouped down (mq3/mq2-lloyd WMMA) → weighted combine into x. Mirrors the
deepseek4 SGLang-style pipeline; reuses the existing FP16 grouped kernels.

Gated on chunk size (HIPFIRE_MINIMAX_MOE_GROUPED_GATE, default 256): below
it the 256 experts get too few rows/expert to beat the indexed path, so
small chunks (and spec-verify B<=64) stay on the indexed GEMV. Daemon
prefill now chunks at 512 (HIPFIRE_MINIMAX_PREFILL_CHUNK) to activate it.

Bench (MiniMax-M2.7.mq2, gfx1151, 2048 tok, fresh process, median):
  chunk-64 indexed  74.5 tok/s
  chunk-512 grouped 166.4 tok/s  = 2.23x
Combined with the WMMA dense-projection win this session: 34 -> 166 = 4.9x.

Correctness: grouped vs indexed final-logits cosine 0.9982, argmax match
(FP16 WMMA vs higher-precision indexed accumulate — the same FP16 grouped
path deepseek4 ships); real-prompt greedy generation fluent and accurate
(Rayleigh-scattering answer, no attractor). FP16 grouped only; i8 grouped
(mq2 PR kernel + mq3 analog) is the next step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the i8 MMQ path to the scatter-grouped MoE prefill (gfx1151):
- gate_up (MQ2-Lloyd) routes to the PR warpfront#476 i8 kernel
  gemm_mq2g256_lloyd_moe_grouped_mmq_gfx1151.
- down (MQ3-Lloyd) has no i8 kernel upstream, so this adds
  gemm_mq3g256_lloyd_moe_grouped_mmq.gfx1151.hip — the MQ3 twin of the
  MQ2 i8 kernel: 8-entry Lloyd codebook quantized to int8 + group scale
  once/group, 3-bit indices decoded via a two-u32 byte-extract LUT, i8
  WMMA (wmma_i32_16x16x16_iu8) at ~2x FP16. Plus the gemm.rs wrapper +
  kernels.rs SRC const.

Opt-in via HIPFIRE_MINIMAX_MOE_I8=1 (gfx1151 only; FP16 grouped stays the
default). Kept opt-in because i8 lowers grouped-vs-indexed cosine
0.9982 → 0.9948; default-on awaits a multi-prompt coherence gate.

Bench (MiniMax-M2.7.mq2, gfx1151, 2048 tok, chunk 512, median):
  FP16 grouped 168.5  →  i8 grouped 184.9 tok/s (best 190) = +9.7%
Correctness: argmax matches indexed; full-i8 greedy generation fluent and
accurate (Rayleigh-scattering answer, no attractor). Session prefill
total: 34 → 185 tok/s = 5.4x.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry; i8 default-on

Adds minimax_coherence_battery (hipfire-runtime example): 6 diverse prompts
× 3 prefill modes (indexed / grouped-FP16 / grouped-i8), 256-tok greedy
decode each, fed through the hipfire-detect bank. A cell is DEGENERATE only
on a true single-token attractor (max_freq>0.50) or a garbage/structural
detector — the spec-decode unique-ratio sub-check is excluded since it fires
on legitimate greedy repetition (the indexed baseline trips it too).

The battery FOUND A REAL BUG: the grouped path sized m_total_max as
`b*k_top + n_exp*BLOCK_M`, which is only 16-aligned when b is even (k_top=8).
An odd-length last prefill chunk (e.g. a 553-tok prompt → 512 + 41) left the
grouped kernel grid (ceil(m_total/16) tiles) one tile longer than the
expert_tile_ids buffer (m_total_max/16, integer-div) → OOB read → GPU page
fault. Standalone tests only used even b=512 so never hit it; a production
prompt with an odd final chunk would. Fixed by rounding m_total_max up to a
whole BLOCK_M.

With the fix the battery is clean: 18/18 cells coherent, no attractors
(max_freq ≤ 0.28 everywhere, < 0.50 threshold), all three modes track the
indexed baseline. On that result, i8 grouped is flipped DEFAULT-ON (gfx1151,
opt out HIPFIRE_MINIMAX_MOE_I8=0): cosine vs indexed 0.9948, +10% over FP16
grouped. Warm default-path prefill measured up to ~261 tok/s (DPM-dependent;
conservative back-to-back number is 166 FP16 → 185 i8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dispatcher

The arch→kernel-variant choice (i8 MMQ on gfx1151 vs FP16 WMMA elsewhere)
belongs in the dispatch layer, not the forward pass — mirroring how
gemm_q8_0_batched_chunked already resolves scalar-vs-WMMA internally. Add
arch-resolving wrappers gemm_mq2g256_lloyd_moe_grouped /
gemm_mq3g256_lloyd_moe_grouped in rdna-compute; forward_batch_hidden now calls
them arch-agnostically (drops the moe_i8 flag, the HIPFIRE_MINIMAX_MOE_I8 env
knob, and the inline if/else). i8 is gated purely on ArchCaps::is_gfx1151()
(the i8 kernels are .gfx1151.hip builtins). Behavior-preserving.

Battery example modes simplified to indexed vs grouped (i8/FP16 is no longer
env-toggleable — it's arch-determined).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/coherence-gate-minimax.sh drives the real serving path: sends a
6-prompt matrix as chat `messages` so the daemon renders the MiniMax chat
template (model answers + emits EOS, no greedy rambling), in two MoE-prefill
modes — grouped (production; i8 on gfx1151) and indexed baseline. Per-request
attractor detection over the daemon JSONL (zero-tokens / max_freq>0.50 /
unique<0.30 / panic = hard fail).

Result: GATE PASS. Chat-templated outputs are healthy (unique 0.46–0.82,
max_freq 0.04–0.09) — far cleaner than the raw-prompt example battery where
greedy repetition is expected. grouped-i8 tracks the indexed baseline on
every prompt, confirming the scatter-grouped i8 prefill is coherence-
indistinguishable from indexed through the daemon. Complements the GPU-direct
minimax_coherence_battery example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coherence is confirmed (coherence-gate-minimax.sh PASS through the chat
template), so the prefill optimizations need no opt-in/opt-out knobs. Remove:
- HIPFIRE_MINIMAX_MOE_GROUPED / _GATE (forward.rs) → grouped MoE is on whenever
  b >= MOE_GROUPED_GATE (const 256) on a WMMA arch with MQ2-Lloyd gate_up.
- HIPFIRE_MINIMAX_PREFILL_CHUNK (daemon.rs) → fixed 512.
(HIPFIRE_MINIMAX_MOE_I8 was already removed; i8 is arch-gated in the dispatcher.)

Simplify coherence-gate-minimax.sh to a single env-free production run and add
a >=256-token passage so the grouped-i8 path is exercised through the daemon
(short prompts → indexed, long → grouped). Gate re-run: PASS (7/7 coherent).
Remove the now-redundant minimax_coherence_battery example (its indexed-vs-
grouped A/B required the removed env knobs; minimax_prefill_bench MINIMAX_GEN
and the daemon gate cover GPU-direct and serving-path coherence).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fill prereq)

MiniMax-M2.7.mq2 is mixed k-map (gate_up=MQ2-Lloyd / down=MQ3-Lloyd). master's
batched forward_batch handled MQ2/MQ4/MQ6 down but not MQ3, so
forward_batch_supported returned false for this model (daemon fell back to
per-token decode_step prefill) and a direct forward_batch at b<256 errored
"down dtype MQ3G256Lloyd has no batched kernel yet".

Add the MQ3 indexed-batched down path so the batched-prefill optimizations in
this branch actually engage for the mq3-down model:
- gemv_mq3g256_lloyd_moe_down_indexed_batched_k4.hip + rdna-compute wrapper +
  kernels.rs SRC const,
- the MQ3G256Lloyd arm in forward_batch's indexed down match, and
- MQ3G256Lloyd in forward_batch_supported's down tier.

(Extracted from the minimax batched-verify work; the grouped MoE path's mq3
i8/FP16 kernels land in the prefill commits.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fivetide
fivetide force-pushed the minimax-prefill-perf branch from 3e3244e to 831ffe5 Compare July 7, 2026 08:20
Kaden-Schutt added a commit that referenced this pull request Jul 11, 2026
…h too

The dry-run resolution preview showed the "stack-clobber" #479 rebases cleanly onto
master. Refine §11: staging is agent-owned, so the agent resolves ON STAGING —
rebases any PR (fork or not) there (mechanical conflicts auto-resolve, semantic ones
escalate to codex merge-fix), never needing to push to the PR branch. Fork limits
only the Gate-4 in-PR fix, not stacking. Codex fork-resolutions logged for contributor review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kaden-Schutt added a commit that referenced this pull request Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Folded into #534 as a cleanly rebased/cherry-picked delta on beta for v0.3.0, preserving the newer Redline and modular-runtime line. Closing this source PR in favor of the consolidated release PR. Thank you for the contribution.

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