Repository navigation
Marlin int4 tensor-core GEMM for MatMulNBits M>1 (capture-safe, WIP) - #960
Merged
Merged
Conversation
…age 1-2) Add crates/onnx-runtime-ep-cuda/src/kernels/marlin_gemm.rs: a from-scratch SM80 mma.sync.m16n8k16 fused fp16xint4 GEMM adapting Marlin techniques (repacked per-lane weight layout, per-group scale applied after the tensor-core accumulate) to the ONNX MatMulNBits format (N-major nibble packing, asymmetric nibble zero-points, group sizes 16/32/64/128). - repack_int4_weights: bijective reorder of packed int4 into the per-lane tensor-core fragment layout (same byte count, verified by roundtrip test). - MARLIN_GEMM_SRC: NVRTC CUDA source using raw mma.sync inline PTX (no <mma.h>/crt dependency), per-group scale-after-accumulate for precision. - launch_marlin_gemm: static launch grid (capture-safe by construction) + SM80 arch guard (device_supports_marlin) for the Rule 11 fallback contract. - Host unit tests: repack roundtrip (all group sizes) + device-support gate. Refs #957 (speculative-capture feasibility, Marlin-gated). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… parity/bench
Interleave the 8 columns of each n-tile so a 32-lane warp reads one
contiguous 64-byte weight chunk per K slice (lane l -> bytes [2l,2l+1]),
Marlin's key coalescing move. M=1 K=N=5120 drops 323->89us (3.6x);
N=13824 M=1 reaches 7.4% HBM peak. Fragment math unchanged (parity holds).
Add GPU tests (ignored, SM80+):
- marlin_parity_vs_f64_oracle: tolerance parity across M in {1,2,7,16,33,64},
group {16,32,64,128}, fp16/fp32 scales, symmetric+asymmetric zp. PASSES.
- marlin_bandwidth_microbench: honest achieved weight-DRAM GB/s + %peak per
shape (gates the M=1 feasibility precondition).
N is padded to a multiple of 8 in the repack; tail columns hold zero codes
and are never stored to output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ppy) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbers) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add matmul_nbits_marlin_repack: an on-device permutation from ONNX packed int4 into the tensor-core layout, so the op path (weights arrive on device as immutable initializers) can repack once and cache. launch_marlin_repack + repacked_bytes helpers. device_repack_matches_host GPU test confirms byte-identical output to the host repack_int4_weights across n/group. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #960 +/- ##
==========================================
- Coverage 79.01% 79.00% -0.01%
==========================================
Files 356 356
Lines 150096 150158 +62
Branches 150096 150158 +62
==========================================
+ Hits 118596 118635 +39
- Misses 26849 26871 +22
- Partials 4651 4652 +1
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Route the com.microsoft::MatMulNBits M>1 prefill/verify path through the SM80 Marlin tensor-core GEMM when ONNX_GENAI_MARLIN_M_GT_1 is set. The path is opt-in and tier-scoped (Rule 11): default OFF, gated on SM80+, int4, no fused SwiGLU/RMSNorm epilogue yet, and falls back byte-compatibly to the portable tiled GEMM on any ineligibility or launch error. - marlin_gemm.rs: add marlin_m_gt_1_enabled() flag and a module-level repack cache (ensure_repacked) keyed by (packed_ptr, ordinal, n, k, group). Weights (immutable initializers) are repacked on-device once during warmup; captured replays hit the warm cache with no allocation, so the static-grid kernel is capture-safe. A cold miss during capture is rejected so the caller can fall back rather than allocate inside capture. - matmul_nbits.rs: add try_launch_marlin_gemm helper and dispatch at the `if m > 1` seam; set last_call_capture_safe = warm. Records the gemm_marlin_int4 kernel variant. - Add op-level test marlin_m_gt_1_op_parity_and_capture_safety: drives the real op with the flag on, validates vs f64 dequant->GEMM oracle (asymmetric zp, worst_abs=0.013 << tol=0.67), and asserts cold=NOT capture-safe / warm=capture-safe + byte-identical replay. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Times the real MatMulNBits op through both the portable tiled GEMM (flag off) and the Marlin tensor-core GEMM (flag on) across an M sweep at K=5120 N=13824 block=128 (H200 median us): M tiled_us marlin_us speedup 1 69.5 69.0 1.01x (M=1 stays on GEMV) 2 478.0 140.0 3.41x 4 494.1 138.5 3.57x 8 536.4 176.9 3.03x 16 658.1 251.2 2.62x 32 1076.7 385.0 2.80x 64 2010.6 821.6 2.45x 128 3851.5 1357.9 2.84x The M=1->M=2 cliff collapses from 6.9x (69.5->478us tiled) to ~2x (69.5->140us) once M>1 routes to Marlin; Marlin is 2.4-3.6x faster than the tiled path across all M>1. M=1 correctly stays on the existing GEMV. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ulNBits Document that the opt-in Marlin M>1 tensor-core GEMM participates in the existing last_call_capture_safe contract: its launch grid is a pure function of (M, N), so on a warm repack-cache hit it does no allocation, D2H, or synchronization and is CUDA-graph capture-safe. A cold (repacking) first call is reported unsupported; only warm replays advertise support. This is what unlocks capture-stable speculative verification at M>1 (the 240 MatMulNBits KernelCaptureUnsupported seams collapse once the path is served by Marlin and the weights are pre-repacked during warmup). Comment/message-only change at the capture_support seam; the runtime behavior is already driven by the dispatch storing the cache-warm flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
M>1 wired (opt-in, capture-safe when warm) + apples-to-apples wall-vs-tiled (2.4-3.6x across M>1; M=1->M=2 cliff collapsed 6.9x -> ~2x). M=1 stays on GEMV. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the opt-in Marlin M>1 path to the rmsnorm_prologue fused epilogue (the first of the fused epilogues MatMulNBits must preserve). Mirrors the tiled launch_f16_gemm_rmsnorm_prefill: stage the per-token normalized activation into scratch via launch_rmsnorm_prefill (byte-identical to the standalone prologue), then run the Marlin int4 tensor-core GEMM over the normalized rows. The scratch allocation keeps this call off the advertised capture contract (as with the tiled rmsnorm prefill); prefill is outside the persistent decode graph. Falls through byte-compatibly to the tiled rmsnorm GEMM on ineligibility or launch error. Adds op-level parity test marlin_m_gt_1_rmsnorm_op_parity comparing the fused Marlin path to the trusted fused tiled path through the real op (shared rmsnorm staging isolates the GEMM): worst_abs=0.0156 << tol=0.90. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an ignored native-CUDA e2e test that runs each real int4 model twice through the engine — tiled GEMM (flag off) then Marlin M>1 GEMM (ONNX_GENAI_MARLIN_M_GT_1=1) — and asserts the greedy token streams match. Prefill runs MatMulNBits at M = prompt length, so this exercises the Marlin path on production asymmetric-zero-point weights while decode stays on the M=1 GEMV. Verified on H200 (GPU 6): both glm-4-9b-int4 and qwen2.5-14b-instruct-int4 produce byte-identical greedy tokens with Marlin vs the tiled path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fused RMS-norm Marlin path (worst_abs=0.0156 << tol=0.90) and Stage-5 e2e: byte-identical greedy tokens tiled vs Marlin on glm-4-9b-int4 and qwen2.5-14b-instruct-int4 (asymmetric zp). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the Marlin int4 M>1 path to cover the gate/up SwiGLU MLP fusion so the MLP no longer falls back to the portable tiled two-GEMM at M>1. This is the bulk of prefill/verify cost and the remaining gap toward zero M>1 tiled-fallback nodes (the capture segments->1 unlock). - marlin_gemm.rs: add `ensure_scratch` size-keyed scratch pool (slot-keyed, FIFO cap 256, rejects cold-miss during capture) so warm replays allocate nothing and stay capture-safe. - matmul_nbits.rs: `try_launch_marlin_gemm_rmsnorm` now uses the pooled scratch (returns Option<bool> warm flag); add `try_launch_marlin_gate_up_prefill` (paired gate/up Marlin GEMMs + shared SiluMul, optional rmsnorm prologue) and wire it into `run_f16_gate_up_swiglu` M>1 branch. - Tests: gate_up SwiGLU M>1 Marlin-vs-tiled parity + capture-safety (plain / rmsnorm / decomposed-silu). worst_abs 0.0001-0.008 vs tiled, warm replays capture-safe & byte-identical. Refs #957, #960. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a tracer-driven coverage audit to the M>1 e2e test that runs a
prefill-heavy generation with ONNX_GENAI_MARLIN_M_GT_1=1, tallies the
per-op kernel_variant annotations, and asserts NO MatMulNBits node fell
back to a tiled M>1 kernel (gemm_f16_tiled{,_rmsnorm},
gate_up_swiglu{,_rmsnorm}_prefill). Zero tiled M>1 fallbacks is the
prerequisite for the capture segments->1 collapse.
Measured (H200, GPU 7):
- glm-4-9b-int4 : 240 gemm_marlin_int4, 0 tiled M>1
- qwen2.5-14b : 240 gemm_marlin_int4 + 48 gate_up_swiglu_marlin_prefill
+ 1 gemm_marlin_int4_rmsnorm = 289 Marlin M>1, 0 tiled
Greedy token streams remain byte-identical tiled-vs-Marlin on both.
Refs #957, #960.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
P2 "widen the win": adds a split-K variant of the Marlin int4 GEMM to attack the occupancy-bound small-M regime, where the direct kernel launches only ~1-1.5 waves of blocks (N=5120 => ~160 blocks on 132 SMs) and stalls at ~3% weight-DRAM. - matmul_nbits_marlin_gemm_f16_splitk: partitions the K/group range over grid.z, each z accumulates its groups' frag*scale into fp32 and writes partials[split_k, M, N] (no bias). - matmul_nbits_marlin_splitk_reduce: sums the z-slices in FIXED order (deterministic => run-to-run identical => capture-stable) then applies fold_bias -> fp16. - choose_split_k: conservative heuristic; only splits when the base grid is under ~2 waves (so it NEVER regresses the already-occupied M>=32 / large-N cases), caps each split at >=2 groups and factor <= 8. - launch_marlin_gemm_splitk: static-grid gemm + reduce pair, capture-safe once partials is pre-allocated (scratch pool). Numerics: split-K reorders fp32 partials vs the single-block kernel, so NOT byte-identical to direct, but passes the f64-oracle parity guard (tol 2e-2) incl. asymmetric-zp, and is deterministic (new tests marlin_splitk_parity_vs_f64_oracle, marlin_splitk_is_deterministic). Measured (H200 GPU 3, median us, split-K vs direct): N=5120 M=1 : 90.0 -> 32.0 us 2.81x (weight 3.0% -> 8.5% peak) N=5120 M=2 : 90.2 -> 32.1 us 2.81x N=5120 M=8 : 105.5 -> 42.9 us 2.46x N=13824 M=1 : 210.3 -> 88.2 us 2.38x (split_k=2) N=13824 M=2 : 127.3 -> 81.3 us 1.57x (split_k=8) Correctly regresses (heuristic returns 1, stays on direct) at M>=32 and large-N M>=8, where base occupancy already suffices. Standalone/opt-in only; not yet wired into the op dispatch. Direct kernel remains the default (byte-identical greedy-token path unchanged). Refs #957, #960. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Routes the plain and rmsnorm-prologue Marlin M>1 GEMMs through split-K when ONNX_GENAI_MARLIN_SPLITK=1 AND choose_split_k elects to split (base grid under ~2 waves). Partials come from the capture-safe scratch pool (slot 4, distinct from rmsnorm slot 0 / gate_up slot 1), so warm replays stay allocation-free; the reduce kernel applies the same fold_bias epilogue. Default OFF => the byte-identical direct kernel remains the greedy-token path. New helper Kernel::maybe_launch_marlin_splitk centralizes the direct-vs-split-K choice and propagates the pooled-scratch warm flag into the existing capture-safety return contract. gate_up SwiGLU stays on the direct kernel for now (its fused SiluMul epilogue would need a split-K reduce variant); documented as remaining work. Validated (H200 GPU 3): - f64-oracle parity + determinism (marlin_splitk_* lib tests) pass, incl. asymmetric-zp, tol 2e-2. - e2e greedy tokens BYTE-IDENTICAL vs tiled with SPLITK=1 on both glm-4-9b-int4 and qwen2.5-14b-int4 (zp) M>1 prefill. - SM80 guard + tiled fallback unchanged; split-K itself is SM80-gated. Refs #957, #960. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The last two MatMulNBits-adjacent capture barriers at query-width > 1: GroupQueryAttention (x40) and SkipSimplifiedLayerNormalization (x80) declared KernelCaptureUnsupported for any M>1 shape, keeping the speculative-verify (M=K) forward segmented even after Marlin removed all 240 MatMulNBits seams. Both were conservative gates, not real hazards. SkipSimplifiedLayerNormalization: - The launch is already a static grid (groups_u,1,1) with no mid-kernel sync / host read-back / alloc-free on the hot path, and the shape-keyed SkipBroadcastMetadataCache already rejects any shape change (incl. num_groups) mid-capture — the same pre-warm cold-miss safety valve the Marlin repack cache uses. The `num_groups == 1` capture latch was purely conservative. Relax it to advertise capture support for a fixed, pre-warmed M=K shape (bf16 staging path still demotes on arena growth). GroupQueryAttention: - Broaden the capture signature to admit a fp16/bf16 q_seq>1 fused-flash verify/prefill shape, not just q_seq==1 Phase2a decode. The fused flash kernel is already capture-safe by construction (static grid, device total_lengths/past_lengths, no host read-back, skips synchronize during capture); the on-device gqa_prepare_metadata kernel + capture_error_ptr latch replace the host-side seqlens/position validation, and the capture-safe branch uses present_capacity (masking the tail on-device) exactly like the M=1 decode path. Backend is resolved against the fixed present_capacity so warm and captured runs agree deterministically; f32 M>1 (reference-scores, host-sized) stays on the eager fallback. Gates: greedy tokens byte-identical on glm-4-9b-int4 (heavy SkipSimplifiedLayerNorm) M>1 prefill; GQA + normalization lib unit tests pass; fmt + lib clippy + native-backend clippy clean. Capture support is still declared only for a warmed signature (pre-warm the M=K shape before capture, same safety valve as the Marlin repack/scratch pools). This is Stage 2 of the merged spec-capture design (#957): "SkipSimplified LayerNorm = signature relaxation, GQA = pre-warmed fixed-M flash workspace." With Marlin M>1 + these two, no hot decode/verify node declares KernelCaptureUnsupported at M>1. Refs #957, #960. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ard-marlin) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
…afe) Merged Deckard's c842b75 (GroupQueryAttention + SkipSimplifiedLayerNorm M>1 capture support). leverb-phase0 Part D INC0 (glm, M=8, pre-warmed): - segments 120 -> 1: whole M=8 verify now captures as ONE replayable graph. Only residual seam = MatMul[KernelCaptureUnsupported]x1 (lone dense MatMul). - B* unchanged by capture-safety alone (5.10x vs marlin-only 4.99x): the captured M=8 wall is compute-bound, not segmentation-bound (120->1 segments moved the wall ~0: 50.7->51.8ms). - Split-K (exploratory, ALSO capture-safe + byte-identical) is the lever that moves B*: 5.10x -> 2.69x (M=8 wall 51.8->27.3ms) -- right at the <=2 GO line. - Numerics: Marlin greedy tokens byte-identical to tiled with AND without split-K (24/24), no NaN under capture. Zero-init KV tail assumption holds. Path to speculative-capture GO = capture-safety (done, segments=1) + split-K on the M=K verify. Decision drop updated with progression table + commands. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
…confirm Refreshed M-scaling + prefill sweep at c842b75 with split-K ON, and re-ran the decisive capture probe clean for reproducibility: - Capture re-probe reconfirmed: canonical segments=1 / MatMul x1 seam / B*=5.09x; split-K segments=1 / B*=2.71x. Reproduces prior 5.10/2.69 within noise. - Warmup coverage confirmed: Part-D INC0 warms via a FULL M=K forward, so every fixed-shape cache (Marlin repack+scratch, GQA meta, LN meta) is populated in one pass; capture_alloc=(0,0) + segments=1 prove no cold-miss valve fired. - Final tail slope (ms/row) BEFORE -> Marlin -> +split-K: glm 2.21 -> 0.93 -> 0.822 (-63%); qwen 3.26 -> 1.10 -> 1.147 (-65%). - Prefill nuance: canonical Marlin edges split-K at large-M prefill (qwen 291-300 vs 267-277); split-K's win is small-M M=K verify. Recommend canonical Marlin for prefill, split-K auto-on for the speculative verify. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… M>1 Two changes that close the residual gap to the speculative-capture GO gate after the GQA + SkipSimplifiedLayerNorm capture-safety landed (segments 120->1). 1. Split-K is now ON by default within the already-opt-in Marlin M>1 path (ONNX_GENAI_MARLIN_M_GT_1=1); opt out with ONNX_GENAI_MARLIN_SPLITK=0. This is the measured B* lever: Sebastian's Increment-0 re-probe on glm-4-9b drops capture B* 5.10x -> 2.69x at M=8 with split-K on. choose_split_k still elects a split only for small-M / low-wave shapes, so large-M prefill stays on the byte-identical direct kernel; greedy tokens remain byte-identical (argmax stable over the deterministic, within-f64-tol split reduction). 2. The plain dense MatMul (lm_head logits projection) is the last node that declared KernelCaptureUnsupported at M>1 (it fell to cuBLASLt's per-call heuristic path). Add a cached-plan fast path (blas::CaptureGemmPlan + DenseGemmPlan) for the plain 2-D (batch==1) M>1 GEMM: select the algorithm + persistent workspace once at warmup, then replay with no heuristic query, allocation, or synchronization. This is the M>1 analogue of F32GemvPlan and reproduces governed_gemm's arithmetic bit-for-bit at a fixed shape. Batched/ broadcast GEMMs keep the per-call path. Now advertises capture support after a warmed call, closing the last M>1 capture seam. Gates: greedy tokens byte-identical on glm-4-9b-int4 and qwen2.5-14b-int4-zp (marlin_m_gt_1_matches_tiled_on_*); fmt + lib clippy + native-backend clippy clean. SM80 guard + fallback unchanged (Marlin M>1 still opt-in). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…an census) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
…B* 2.63x Merged Deckard's 2971403/1a72c865 (lm_head cached dense-GEMM capture plan + split-K default within MARLIN_M_GT_1=1). leverb-phase0 Part D INC0, glm, M=8, GPU7 (Deckard on GPU5): - Census now TRUE ZERO unsupported nodes: seam nodes '<none: whole-graph>'. The whole M=8 verify (incl. lm_head logits projection) captures as ONE whole-subgraph replay. Last MatMul[KernelCaptureUnsupported]x1 is gone. - B* 2.71 -> 2.63x (captured M=8 wall 27.5 -> 26.8ms): folding lm_head out of its per-step eager launch+sync nudged B* down as predicted, but strict <=2 GO line not yet crossed. Residual is genuine small-M GEMM compute. - Byte-identical greedy tokens on glm-4-9b-int4 with the new default path (split-K default + lm_head plan); no NaN. Cleanest capture config to date. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The old "aim for ~2 waves of blocks, else don't split" rule systematically
under-split at the M=8 verify width: it picked split_k=2-3 where the microbench
optimum is 8, and bailed to no-split entirely for large-N projections (gate_up
N=27392) where split_k=8 still wins. Block count is a poor proxy at small M —
each block is latency-bound (low DRAM %), so oversubscribing K keeps more memory
requests in flight and lifts achieved weight-DRAM bandwidth.
Retune: split for m <= 32 (compute-bound beyond), oversubscribe ~8 waves, and
floor toward the measured small-M optimum (8 for m<=16, 4 above) even when the
base grid already spans many waves; cap at 8 (16 regresses medium-N). H200
M=8 microbench (glm/qwen decode dims), auto factor before -> after:
K=4096 N=4096 (o/q proj) sk3 2.34x -> sk8 2.93x
K=13696 N=4096 (down) sk3 2.45x -> sk8 3.18x
K=4096 N=27392 (gate_up) sk1 (none) -> sk8 1.11x
K=5120 N=13824 (qwen g/u) sk1 (none) -> sk8 1.35x
K=4096 N=256 (kv) sk8 5.30x (unchanged, already optimal)
The dominant verify cost (MLP gate_up + down) now splits; large-M prefill still
returns 1 (byte-identical direct kernel). K=5120,N=5120 (qwen square attn proj)
is bimodal (cold-clock 0.9x / clocked-up 2.5x) — a minor fraction of verify cost;
Sebastian's B* re-probe is the e2e arbiter.
Gates: split-K f64-oracle parity + determinism pass at sk=8
(marlin_splitk_parity_vs_f64_oracle covers split_k in {2,4,8}); greedy tokens
byte-identical on glm-4-9b-int4; fmt + lib clippy clean. choose_split_k only
affects m<=32, so prefill/decode paths are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
Merged Deckard's 4abe4e5/0143f235 (choose_split_k retune for M=8 verify: gate_up/down/o-q now split sk=8; prefill still sk=1 direct kernel). Re-probed glm M=8, GPU7 (Deckard on 4/6), reproduced x2: - B* 2.63 -> 2.16x / 2.17x (captured M=8 verify wall 26.8 -> 22.0ms). Gate_up MLP GEMM off the no-split slow path is the dominant win. - Still segments=1, whole-graph, ZERO unsupported nodes. Byte-identical greedy tokens on glm-4-9b-int4 (parity PASS), no NaN. - 2.16x is an honest plateau a hair above the conservative <=2 line; but <=2 is break-even-at-2-accepted-tokens, so this is a PRACTICAL GO once the draft accepts >2.16 tok/verify (trivial for an 8-wide draft). Full arc: segments 41->1; B* 8.76->4.99->5.09->2.71->2.63->2.16x, byte-identical. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The two largest MLP GEMMs (gate/up, glm K=4096 N=13696 each x40 layers)
were launched as direct launch_marlin_gemm calls in
try_launch_marlin_gate_up_prefill, bypassing split-K entirely. At the M=K
speculative-verify width these are the single biggest untapped small-M
lever. Route both through maybe_launch_marlin_splitk so choose_split_k
elects the 8-way K split at M<=16; gate/up reuse the slot-4 partials
scratch sequentially (gate fully reduces into gate_buf before up
overwrites partials), keeping stream ordering correct and capture-safe.
Fold both split-K warm flags into the returned warmth so capture-safety
accounts for the split-K scratch.
Validated on H200 (GPU6):
- marlin_gate_up_swiglu_matches_tiled_{plain,rmsnorm,decomposed}: PASS
(split-K now active at M=8; worst_abs 0.0001-0.008 << tol, warm replay
byte-identical, cold-miss safety valve intact)
- glm-4-9b-int4 + qwen2.5-14b-int4-zp e2e greedy tokens byte-identical
- fmt + lib clippy + native-backend clippy clean
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
…plateau) Merged Deckard's 3735d57/dffabf0d (gate_up SwiGLU MLP GEMM was DIRECT-launching, bypassing split-K even after the 4abe4e5 factor retune; now routes sk=8). Re-probed glm M=8, GPU7, reproduced x2: - B* 2.15x / 2.19x; captured M=8 verify wall stable ~21.9ms (ratio jitter is from the M=1 captured baseline 9.98-10.18ms, not the M=8 wall). segments=1, whole-graph, zero unsupported nodes, byte-identical (parity PASS), no NaN. - This is the small-M GEMM floor: M=8 mma.m16n8k16 is ~half-occupancy (~50% MMA waste), so split-K+occupancy is the last GEMM lever; path to universal <=2 is drafting depth, not more GEMM tuning. - Practical GO: any 8-wide draft accepting >2.16 tok/verify wins. Capture-safety fully solved. Full arc: segments 41->1 whole-graph; B* 8.76->4.99->5.09->2.71->2.63->2.16x. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…en for review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
…wen block_32 B*~4.7x Key finding: the fused gate_up SwiGLU Marlin path requires block_size==32 (matmul_nbits.rs run_f16_gate_up_swiglu). Verified via onnx: glm=block_128, qwen=block_32. - glm (block_128) is NOT eligible for the gate_up fusion -> 3735d57 is a NO-OP for glm; glm's B*=2.16x comes from the 4abe4e5 general split-K retune, not gate_up fusion. Explains the flat glm wall (22.0->21.9ms) after 3735d57. - qwen (block_32) M=8 verify re-probe: segments=1, whole-graph, zero unsupported (48 GQA + 96 LN captured -> capture-safety generalizes), but B*=4.62-4.72x (captured M=8 35.5ms / M=1 7.5ms). NOT at GO. - Honest read: captured_ratio > eager_ratio (4.7>3.8) is a DENOMINATOR effect (capture speeds the launch-bound M=1 by 34% vs M=8 by 18%), not a capture inefficiency. qwen's tuned block-32 M=1 is fast (inflates ratio) and its 14B/48-layer M=8 verify is genuinely heavier. Deckard caveat #1 (qwen attn proj cold-clock bimodal) may add. Small-M GEMM floor is model-dependent: glm practical GO (2.16x), qwen 4.7x needs more small-M work or deeper drafting. glm remains canonical gate. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… fusion) + qwen breadth Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
pushed a commit
that referenced
this pull request
Aug 14, 2026
…OT a Marlin regression Ran qwen byte-identical parity at 3735d57; first run failed (Marlin greedy stream diverged from tiled at token 19). Investigated before raising a regression: - 4 runs (split-K on x3 + off x1): Marlin output DETERMINISTIC every time (5692,374,264,1140,315). The TILED reference diverged (320,94409,...) on only 1 of 4 runs. Token 19 is a near-degenerate argmax; the tiled GEMM's own nondeterministic (atomic/reduction-order) accumulation occasionally flips it. - Conclusion: NOT a Marlin numerics regression. Marlin (fixed split-K reduction) is the MORE deterministic path; the tiled oracle flakes ~1-in-4 at this tie. Split-K on and off both produce identical deterministic Marlin tokens. - Action for Chew/Gaff: the qwen exact-token parity test is flaky (~25%) because it asserts equality vs a nondeterministic tiled reference at a near-tie. Not a blocker; harden the assertion (logit tolerance / deterministic oracle / assert Marlin self-determinism). glm's prompt doesn't hit such a tie -> glm clean. Refs #957, #960, #962 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
marked this pull request as ready for review
August 14, 2026 21:30
justinchuby
added a commit
that referenced
this pull request
Aug 14, 2026
Perf + capture-probe harness for the Marlin int4 M>1 GEMM program (#960): M=1 DRAM%, M-scaling wall, prefill sweep, e2e-vs-ORT, and the Part-D INC0 capture B* progression (glm 8.76->2.16x, segments 41->1 whole-graph). Bench-only delta rebased onto clean main (kernel already landed via #960). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
added a commit
that referenced
this pull request
Aug 14, 2026
Scribe consolidation for the Marlin int4 GEMM program landing (#960/#961/#962). - Merge 5 decision inbox drops (deckard/pris/sebastian kernel+bench+numerics + chew/gaff reviews) into one consolidated Marlin program entry; corrected per-model attribution (glm block-128 → gate_up fusion no-op; 2.16× was the general split-K retune). - Archive resolved #957/#948/#949 spec-capture + Lever B entries to decisions-archive/2026-08.md (decisions.md 45,991 → 42,104 B, under the 50 KB gate). - Orchestration logs (deckard/sebastian/pris/chew/gaff), session log, cross-agent history updates; summarized sebastian history (>8 chronicle threshold). State-only change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
justinchuby
added a commit
that referenced
this pull request
Aug 14, 2026
…ifier) (#974) The qwen e2e parity test (`marlin_m_gt_1_matches_tiled_on_qwen2_5_14b_int4`) was ~25% flaky. Root cause: it asserted exact greedy-token equality against the portable TILED GEMM reference, whose fp32 atomic reduction order is nondeterministic; at a near-degenerate argmax (token 19) the *tiled* side flips run-to-run — Marlin is the deterministic side. Test-oracle bug, not a kernel regression (kernel unchanged, frozen at the #960 landing). Fix: replace exact-equality with a cascade-robust `classify_parity()` — on first divergence at position d (identical prefix), re-run the tiled reference under the same prefix; if it too flips at d, it's a proven near-tie (pass), otherwise a real corruption (panic). Extracted as a pure GPU-free function with 5 deterministic unit tests. glm keeps its strict byte-identical assertion (no near-tie). Validation: 5 unit tests pass; qwen e2e 5/5 stable; glm e2e pass; fmt + clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
added a commit
that referenced
this pull request
Aug 14, 2026
…wen 2×2 (#975) Follow-up to the Marlin int4 M>1 capture arc (kernel #960, bench #962, numerics gate #961, flaky-parity hardening #974). Refs #957 and `docs/research/decode-remaining-levers-feasibility.md`. ## What Deckard's final ask before Chew's #960 numerics review: an explicit **qwen captured-M=8 vs eager-M=8 token-equality** data point to fill the last cell of the captured/eager × glm/qwen 2×2. The shipping speculative path uses the *eager* verify path (`native_decode/backend.rs`), and the existing leverb INC0 probe writes a constant token (timing-only), so no existing harness answered this. This adds a **test-only** parity variant that feeds real tokens and compares captured-replay vs eager warm-forward per-row argmax **and** raw logits bytes. - `native_decode/cuda.rs`: `leverb_increment0_token_parity_attempt` (gated `#[cfg(all(test, feature="cuda"))]`). - `native_decode/leverb_phase0_probe.rs`: **Part E** prints per-row eager vs capture argmax + `logits_byte_identical` + PASS/FAIL. ## Result — full 2×2 GREEN, captured == eager **byte-identical** on both models | Model (block) | M | argmax_match | byte_identical | segments | B* | |---|---|---|---|---|---| | qwen2.5-14b (block-32) | 8 | ✅ | ✅ | 1 | 4.45× | | qwen2.5-14b | 1 | ✅ | ✅ | 1 | — | | glm-4-9b (block-128, canonical) | 8 | ✅ | ✅ | 1 | 2.19× | | glm-4-9b | 1 | ✅ | ✅ | 1 | — | B* refresh matches the frozen plateau (glm ≈2.16×, qwen ≈4.45×). No shipping path touched; fmt + engine clippy clean. Reproduce commands in `.squad/decisions/inbox/sebastian-captured-eager-parity.md`.⚠️ Bench/validation follow-up — a squad member (Chew/Deckard) should confirm before merge. Co-authored-by: Sebastian <sebastian@squad.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Marlin-style fused fp16xint4 tensor-core GEMM (WIP -- draft)
Builds the primary decode-perf lever from
docs/research/decode-remaining-levers-feasibility.mdand the capture enabler from the spec-capture feasibility doc (#957): a Marlin-style
fused fp16xint4 tensor-core GEMM to replace the slow M>1 path of
com.microsoft::MatMulNBits(today a portable 16x16 CUDA-core tiled GEMM) and,conditionally, the M=1 decode GEMV. Refs #957.
What's landed (Stage 1-4)
crates/onnx-runtime-ep-cuda/src/kernels/marlin_gemm.rs(kernel +host/device repack isolated from the 475 KB
matmul_nbits.rs).mma.sync.m16n8k16kernel using raw inline PTX (no<mma.h>),adapting Marlin ideas (repacked per-lane weights, per-group scale AFTER the
tensor-core accumulate) to our ONNX-native asymmetric-zp / nibble format,
group sizes 16/32/64/128.
repack_int4_weights+ on-devicematmul_nbits_marlin_repack: bijective8-column-interleaved reorder for coalesced 64-byte/warp loads. Device repack is
byte-identical to the host reference.
device_supports_marlin()SM80 guard for the Rule 11 byte-identical fallback.Static launch grid => capture-safe by construction.
MatMulNBitsM>1 dispatch (opt-inONNX_GENAI_MARLIN_M_GT_1,default OFF, tier-scoped). Module-level repack cache repacks immutable-initializer
weights once during warmup; warm replays do no allocation and are capture-safe.
Cold-miss-during-capture is rejected. Any ineligibility / launch error falls
through byte-compatibly to the portable tiled GEMM.
capture_supportadvertises M>1 support on a warm cache -- the property thatunlocks capture-stable speculative verification at M>1.
Validation
Numerics:
marlin_parity_vs_f64_oracle(standalone) andmarlin_m_gt_1_op_parity_and_capture_safety(through the real op) pass vs an f64dequant->GEMM oracle across M in {1,2,7,16,33,64}, group in {16,32,64,128}, fp16/fp32
scales, sym+asym zero-points (abs <= 2e-2*max|out|; op worst_abs=0.013 << tol=0.67).
The op test asserts cold=NOT-capture-safe / warm=capture-safe + byte-identical replay.
Wall vs tiled (through the op),
marlin_m_gt_1_op_wall_vs_tiled, H200,K=5120 N=13824 block=128 (median us):
The M=1->M=2 cliff collapses from 6.9x (69.5->478 us tiled) to ~2x (69.5->140 us).
Marlin is 2.4-3.6x faster than the tiled GEMM across every M>1.
Honest status
NOT clear the feasibility section-3 >=40% precondition, so M=1 stays on the
existing GEMV (reported, not silently dropped).
levers (split-K / cp.async); e2e on glm-4-9b-int4 / qwen2.5-14b (with Sebastian).
Draft -- do not merge. Numerics gated by Chew; e2e/capture re-probe by Sebastian.
Update — fused RMS-norm epilogue + e2e PASS
the normalized activation into scratch (byte-identical to the tiled prologue),
then runs Marlin. Test
marlin_m_gt_1_rmsnorm_op_parity: worst_abs=0.0156 << tol=0.90.tests/marlin_m_gt_1_e2e.rs, native CUDA): tiled (flag off) vsMarlin (flag on) greedy token diff on the real models. Both byte-identical:
cp.async). Numerics gated by Chew; ORT e2e + capture re-probe by Sebastian.
Update — P1 COMPLETE: full M>1 coverage (gate_up SwiGLU fused), zero tiled fallbacks
Every hot MatMulNBits node in glm-4-9b and qwen2.5-14b now runs through Marlin at M>1.
try_launch_marlin_gate_up_prefill): pairedgate/up Marlin int4 GEMMs + the identical SiluMul epilogue the tiled path uses, with
an optional RMS-norm prologue. This is the MLP — the bulk of prefill/verify cost.
down_projis a plain tall-skinny MatMulNBits already covered bygemm_marlin_int4.ensure_scratch, size+slot-keyed, rejects cold-missduring capture) so warm replays of gate_up + rmsnorm allocate nothing. The rmsnorm
prefill was migrated onto it and is now capture-safe on warm replay too.
gemm_f16_tiled{,_rmsnorm}/gate_up_swiglu{,_rmsnorm}_prefillat M>1.Measured coverage audit (H200, Marlin M>1 enabled):
gemm_marlin_int4, 0 tiled M>1.gemm_marlin_int4+ 48gate_up_swiglu_marlin_prefillgemm_marlin_int4_rmsnorm= 289 Marlin M>1, 0 tiled.gate_up unit parity (Marlin vs tiled): worst_abs 0.0001–0.008; warm replays
capture-safe and byte-identical. No hot M>1 node remains on the tiled path — this
unblocks the Increment-0 capture re-probe (segments→1). Next: P2 split-K / cp.async +
honest M=1 precondition re-run.
P2 — widen the win: split-K (landed, opt-in) + honest M=1 re-run
split-K (
ONNX_GENAI_MARLIN_SPLITK=1, default OFF): partitions the K/grouprange over
grid.zto fill idle SMs when M is small (the occupancy-bound regime),fixed-order fp32 partial reduce ⇒ deterministic/capture-stable, pooled partials ⇒
capture-safe. Conservative
choose_split_konly splits under ~2 waves so it neverregresses the already-occupied M≥32 / large-N cases.
Measured (H200, split-K vs direct): N=5120 M=1 2.81×, M=2 2.81×, M=8 2.46×;
N=13824 M=1 2.38×, M=2 1.57×. Gates intact: f64-oracle parity + determinism tests
pass; e2e greedy tokens byte-identical vs tiled with SPLITK=1 on glm-4-9b and
qwen2.5-14b (zp); SM80 guard + fallback unchanged.
cp.async multistage: rejected (measured slower — small-M is occupancy-bound,
not latency-bound; hiding load latency only added barrier overhead).
M=1 DRAM precondition — honest re-run: still fails. Best split-K lifts M=1
Marlin weight-DRAM to only ~8.5% peak, far below the ≥40% floor ⇒ M=1 stays on
the existing GEMV. Remaining: gate_up SwiGLU M>1 stays on the direct kernel (split-K
reduce for the fused SiluMul epilogue is follow-up).
Stage 2 of spec-capture (#957): last two M>1 capture barriers removed
With Marlin clearing all 240 MatMulNBits M>1 seams, the only ops still declaring
KernelCaptureUnsupportedat query-width>1 were GroupQueryAttention (×40) andSkipSimplifiedLayerNormalization (×80). Both were conservative gates, not real
hazards; now both advertise capture support for a warmed M=K signature.
metadata cache already the pre-warm valve; relaxed the
num_groups == 1latch.q_seq>1fused-flashsignature.
flash_attention::runis already capture-safe (static grid, devicetotal_lengths/past_lengths, no host read-back); on-device metadata +capture_error_ptrreplace host validation; capture-safe branch usespresent_capacity(device-masked tail) like the M=1 path. f32 M>1 stays eager.Gates: greedy tokens byte-identical (glm-4-9b-int4 M>1 prefill); GQA + norm unit
tests pass; fmt + lib clippy + native-backend clippy clean. Capture still declared
only for a warmed signature (pre-warm M=K before capture). Sebastian re-probes for
the decisive segments→
1 / B*≤2 (speculative-capture GO).Segment census confirmed (Sebastian) + last dense seam closed
Sebastian's Increment-0 re-probe (glm-4-9b, M=8,
MARLIN_M_GT_1=1) confirmed theGQA + SkipSimplifiedLayerNorm capture-safety: segments 120 → 1, all 40 GQA +
80 SkipSimplifiedLayerNorm + 240 MatMulNBits M>1 seams gone, byte-identical, no
NaN under capture. Two follow-ups (commit
29714037):ONNX_GENAI_MARLIN_SPLITK=0). It is the measured B* lever — Sebastian's probe:B* 5.10× → 2.69× at M=8 (wall 51.8→27.3ms).
choose_split_kkeeps large-Mprefill on the byte-identical direct kernel; greedy tokens byte-identical.
lm_head/MatMul→
logits(plain fp16 denseMatMul, not MatMulNBits), which fell tocuBLASLt's per-call heuristic at M>1. Added a cached-plan fast path
(
blas::CaptureGemmPlan+DenseGemmPlan): algorithm + persistent workspaceselected once at warmup, replayed with no heuristic/alloc/sync — the M>1
analogue of
F32GemvPlan, bit-for-bit identical togoverned_gemmat a fixedshape. Batched/broadcast GEMMs keep the per-call path.
Gates: greedy tokens byte-identical on glm-4-9b-int4 and qwen2.5-14b-int4-zp; fmt
Small-M split-K retune for the M=8 verify shape
Profiled glm/qwen decode-GEMM dims at M=8 and found
choose_split_kwasunder-splitting: it picked
split_k=2-3(or1for large-N gate_up) where theoptimum is
8. Root cause — block count is a poor fill proxy at small M, whereeach block is latency-bound; deeper K-splitting raises weight-DRAM throughput.
Retuned (split for
m<=32, oversubscribe ~8 waves, floor toward the measuredsmall-M optimum, cap 8). M=8 auto factor before→after:
The dominant MLP verify cost now splits; prefill (
m>32) stays on thebyte-identical direct kernel. Gates: split-K f64-oracle parity + determinism at
sk=8; greedy tokens byte-identical on glm-4-9b-int4; fmt + lib clippy clean.
gate_up (SwiGLU MLP) routed through split-K (3735d57)
Found the residual B* gap:
try_launch_marlin_gate_up_prefilllaunched the twolargest MLP GEMMs (gate/up, glm K=4096 N=13696 each ×40 layers) as direct
launch_marlin_gemm, bypassing split-K. The update-7 retune therefore neverreached gate_up. Both projections now go through
maybe_launch_marlin_splitk(sk=8 at M=8); gate/up reuse the slot-4 partials scratch sequentially (correct
under single-stream ordering), and both split-K warm flags fold into the
returned capture warmth. Gates:
marlin_gate_up_swiglu_matches_tiled_*PASS atM=8 (split-K now active; warm replay byte-identical, cold-miss valve intact);
glm-4-9b-int4 and qwen2.5-14b-int4-zp e2e greedy tokens byte-identical; fmt +
lib clippy + native-backend clippy clean.
FINAL STATUS — frozen for merge review (supersedes the "Next"/"Draft" notes above)
Capture-safety is complete and the M>1 Marlin path is landed opt-in. This
branch is frozen for numerics (Chew) + quality (Gaff) review.
Frozen head:
a11facbc(code frozen at3735d57e; later commits aredecision-drops only). Sebastian's decisive perf A/B was re-probed on the frozen
code commit
3735d57e(glm M=8, GPU7, ×2): B*=2.15–2.19×, captured M=8verify wall stable 21.9ms, segments=1 whole-graph, zero unsupported nodes,
byte-identical greedy tokens, no cold-miss. gate_up split-K routing is thus both
the most complete config and the independently-measured one.
Capture / perf arc (glm-4-9b, M=8 speculative verify, Sebastian's Increment-0 probe):
whole-graph replay, zero
KernelCaptureUnsupportednodes.→ 2.16× (small-M split-K retune) — an honest plateau at the ≤2 practical-GO
line (residual is intrinsic M=8
mma.m16n8k16half-occupancy, not a captureblocker). Prefill ~2× (glm 218→426, qwen 154→300). Marlin-OFF == BEFORE (no
<SM80 / consumer-tier regression).
Per-model note (block size). glm-4-9b is block-128: its gate/up run as
separate MatMulNBits through the general Marlin split-K path (covered by the
retune), so the fused gate_up split-K commit (
3735d57e) is a no-op for glmand its 2.16× is the retune alone. The fused gate_up SwiGLU path requires
block_size==32, so3735d57ebenefits block-32 models (qwen2.5-14b), whereit is confirmed firing under capture (qwen census: segments=1, zero unsupported —
a fall-through would hit the non-capture-safe tiled path and fragment). qwen M=8
verify lands at B*≈4.7×: capture-safety fully generalizes (whole-graph, zero
seams, byte-identical eager parity), but the ratio is inflated by a denominator
effect (capture speeds qwen's fast block-32 M=1 GEMV 34% vs the heavier 14B M=8
verify 18%). Closing qwen's B* is a drafting-depth story, not a kernel fix.
glm-4-9b (block-128) is the canonical practical GO at 2.16×.
Env-var contract (both implemented — verified in
marlin_gemm.rs):ONNX_GENAI_MARLIN_M_GT_1— opt-in, default OFF, tier-scoped (Rule 11).marlin_m_gt_1_enabled()(1/true/on). SM80 runtime guard(
device_supports_marlin) + byte-identical fallback to the portable tiled GEMMon <SM80 / CPU EP. Two weight layouts coexist; repack is an added packaging step.
ONNX_GENAI_MARLIN_SPLITK— default ON within the opt-in M>1 path; opt out=0/false/offfor the byte-identical direct kernel.choose_split_konlyelects a split for small-M / low-wave shapes (returns 1 for large-M prefill), so
prefill/decode stay on the byte-identical direct kernel.
Coverage: zero M>1 tiled-fallback for the hot MatMulNBits nodes on glm-4-9b +
qwen2.5-14b (plain GEMM, rmsnorm-prologue, swiglu/gate_up, down_proj all fused
through Marlin). GQA + SkipSimplifiedLayerNorm made M>1 capture-safe; lone dense
lm_head MatMul made M>1 capture-safe via a cached cuBLASLt plan (
CaptureGemmPlan/
DenseGemmPlan).Gates at frozen head (H200 GPU6): f64-oracle split-K parity PASS;
marlin_gate_up_swiglu_matches_tiled_{plain,rmsnorm,decomposed}PASS (split-Kactive at M=8, warm replay byte-identical, cold-miss valve intact); glm-4-9b-int4
and qwen2.5-14b-int4-zp e2e greedy tokens byte-identical to the tiled
path;
cargo fmt --all -- --checkclean;cargo clippylib + native-backend(warnings-as-errors) clean.
Test hardening (
4803b4fc, test-only — kernel frozen at3735d57e)marlin_m_gt_1_matches_tiled_on_qwen2_5_14b_int4was ~25% flaky: it assertedexact greedy-token equality against the portable tiled GEMM, whose fp32
atomic reduction order is nondeterministic. At a near-degenerate argmax the
tiled reference flips run-to-run (Marlin is the deterministic side); greedy
decode is autoregressive so one early flip cascades. Not a Marlin bug — a test
oracle bug (glm's prompt has no such tie, so its test was already clean).
Replaced exact-equality with a cascade-robust classifier: compare one tiled vs
one Marlin stream; if they first diverge at position
d(identical prefix), are-run tiled probe that reproduces the prefix but flips at
dprovesdis anear-tie → not a regression. Extracted into a pure GPU-free
classify_parity()with deterministic unit tests for every outcome, so the branch is CI-verified
without needing to reproduce a live tie. Gates: unit tests + glm/qwen e2e pass,
fmt + clippy (lib/native-backend/cuda) clean.