Skip to content

cuda qmmf: half tiles for decode calls whose 64-column grid leaves SMs idle - #1171

Closed
jschmied wants to merge 3 commits into
ashhart:mainfrom
jschmied:fn-lane-half-tiles
Closed

jschmied wants to merge 3 commits into
ashhart:mainfrom
jschmied:fn-lane-half-tiles

Conversation

@jschmied

Copy link
Copy Markdown
Contributor

What this changes

A 16-row MXFP8 or BF16 lane call (decode rows, verify windows) whose 64-column grid holds fewer than 96 blocks
(TF_QMMF_HALF_BELOW, 0 for never) takes a 32-column tile: half a packed 64-column tile's n8 tiles, which are
contiguous in both formats' word order and in MXFP8's scale order. Each column's mma chain, its K slices and the
cluster sum are the 64-column tile's, so its bits are the same; the grid has twice the blocks. The existing instances'
loads stay behind if constexpr (BN == 64).

On Flash Next's ModelOpt checkpoint the hyper-connection down projection ([324, 10240] BF16: 6 column tiles x 8
K slices, one block an SM on a GB10) read its weights at 158 GB/s.

Receipt

  • Environment: TensorFold main d1018b31 plus this branch; one DGX Spark (GB10, sm_121, CUDA 13.0, nvcc 13.0.88);
    nvidia/Qwen3.8-Flash-Next-FP4-Local@b9ce18a6.

  • Exactness: tf-cuda-test lane-half: MXFP8 and BF16, fp32 and bf16 out, bytes equal to the 64-column tile at Flash
    Next's decode shapes (hyper-connection down with and without its 4 inject rows, the router, the shared expert's
    gate/up and down), 1 to 16 rows, the 32- and 96-column edges; fp32 outputs within their float64 bound (worst 0.011).
    Served, greedy, against main: chats and ~8k contexts byte-identical.

  • Kernel time on weights read from DRAM (copies past the L2, cycled):

    call 64-column half
    BF16 1 x 324 x 10240 (hc down) 42.0 us 36.3 us 0.86x
    BF16 1 x 320 x 10240 35.8 us 32.9 us 0.92x
    BF16 1 x 512 x 2560 (router) 16.7 us 16.1 us 0.96x
    MXFP8 1 x 640 x 2560 (shared gate/up) 12.9 us 12.3 us 0.95x
    MXFP8 1 x 2560 x 640 (shared down) 11.1 us 10.3 us 0.93x

    Wider grids gain nothing (0.97-1.02x at 160 to 320 blocks), hence the threshold.

  • Decode speed (served, --no-drafts, one stream, code / chat): 23.95 / 23.83 tok/s, main 23.58 / 23.66; four
    streams 65.9 / 65.5 aggregate, main 65.6 / 65.1.

  • Prompt speed: unchanged (prompt rows take the 64-row tiles).

  • Kernel contract, tf-cuda-test kernel-contract (built -Dnvcc) on the GB10: SUMMARY 87 kernels: 87 pass gates 1-4, 0 fail, 0 known findings; 8 controls: 8 red as planted, 0 missed; 1533 limit problems (the limit problems are the
    offline audit's on this nvcc, the same count as a main-based run; none in the new launches).

    • 2 new entries (zig/tests/cuda/contract_lane_half.zig), MXFP8 and BF16, each replacing the 64-column 16-row tile:
      gate 1 against float64's classes, gate 3 bit-equal at 15 shapes (Flash Next's decode calls and the 32-, 64- and
      K-edges), gate 4 exact (bf16 sums would miss 17,823-20,611x).
    • Control: the MXFP8 half tile at a 1.5x tensor scale posing as the replacement, red at gate 3.
    • Gate 5: 8 launches recorded from nvcc 13.0.88 cubins (sm_80 to sm_121); all 8 pass live on sm_121.
  • Tests run: full zig build test -Dnvcc=/usr/local/cuda/bin/nvcc (exit 0), tf-cuda-test lane-half and kernel-contract, zig fmt --check. Lean check (CI's lean_check.py + lean_new.py against d1018b31): 0 new findings.

Gotchas

  • Shared kernel: qmmf is the lane matmul Nemotron uses too. Any 16-row MXFP8 or BF16 call with fewer than 96 blocks in its 64-column grid now takes the half tile, with the same bits. I measured speed on Flash Next only; Nemotron's bytes are covered by the contract, not by a served run.
  • Small gain: about +1-1.5 % decode at one stream, mostly from the hyper-connection down projection.
  • Conflict with Flash Next CUDA: NVFP4 experts on prompt tiles with the decode form's sums, bit for bit #1143 in zig/src/core/contract/registry.zig: both add contract entries at the same place. Keep both lists.

Checklist

  • Every token still goes through the lane rounds.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. Measured with MTP drafts at --parallel 4: drafted == plain 8/8, concurrent == solo 8/8, and a two-turn prompt resumed from the cache (7,147 of 7,184 tokens restored) equals the same turn on a fresh server; each output equals main's.
  • No precision traded for speed. The bits do not change.
  • New and changed kernels are registered with the kernel contract, with red controls.
  • Prompt processing is unchanged (prompt rows take the 64-row tiles).
  • Platforms: CUDA on GB10 (sm_121), one rank; other SMs through recorded compiles; no Metal change.
  • Comments are one line. No measurements or history in the source (lean check 0 new).
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

…s idle

A 16-row MXFP8 or BF16 lane call whose 64-column grid holds fewer than 96 blocks
(TF_QMMF_HALF_BELOW, 0 never) takes a 32-column tile: half a packed tile's n8 tiles, contiguous
in both formats' word and scale order, so each column's mma chain, K slices and cluster sum are
the 64-column tile's and its bits the same, with twice the blocks. Flash Next's hyper-connection
down projection ([324, 10240] BF16, 6 tiles x 8 slices) reads its weights at 183 GB/s instead of
158 (36.3 vs 42.0 us cold), the router and the shared expert's MXFP8 projections 4-8 % faster.
The existing instances are untouched (their loads stay behind `if constexpr (BN == 64)`).

tf-cuda-test lane-half: MXFP8 and BF16, fp32 and bf16 out, bytes equal to the 64-column tile at
Flash Next's decode shapes and the column edges, FP64-bounded, timed on weights past the L2.
Served (FP4-Local, no drafts): 23.95 / 23.83 tok/s one stream (main 23.58 / 23.66), replies equal.
…FP8 and BF16), a 1.5x-scale control, gate 5 records
… 0 for the released 64-column launches

projection_f32_host_tests and the nemotron sm_89 host tests build a qmmf.Lane field by field; they now
set its half tiles and half_below = 0, so their launch parameters stay the 64-column tile's they check.
@ashhart

ashhart commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied for the half tiles, and for keeping each column's K-slice order and cluster sum exactly as the 64-column tile's, with the 1.5x tensor-scale control to prove the replacement check bites. We are carrying all three commits onto current main ourselves, kept as yours, resolving the registry conflict you flagged, and a separate commit of ours will set half and half_below = 0 in the two hand-built qmmf.Lane fixtures main has gained since your base (zig/src/cuda/proposal_head_test.zig:82 and zig/flashnext_proposal_host.zig:66 on main), as your third commit does for the others, and add FP32-output entries to the half-tile contract (zig/tests/cuda/contract_lane_half.zig:32 declares BF16 only), so the FP32 half kernels meet the gate 4 stress. Then it goes to the GB10 kernel contract and served-equality runs on Flash Next and the shared Nemotron lane path, and lands. You need not rebase; if you are in the middle of a change, please tell us so we do not collide.

- Synthema, Ash's AI Assistant.

ashhart added a commit that referenced this pull request Oct 25, 2026
…, fp32-out half-tile contract entries

- proposal_head_test.zig and flashnext_proposal_host.zig, added to main after the PR's base, build a qmmf.Lane from undefined; they now set half and half_below = 0, as the PR does for the other hand-built lanes, so matmulOut reads defined fields and launches the 64-column tile (both crashed on 0xaa... handles in Debug before).
- contract_lane_half.zig: MXFP8 and BF16 half-tile entries writing fp32, and a 1.5x-scale fp32 control, since the FP32 projection contract's gate-4 shape (2560 x 10240) takes the 64-column tile and no gate 4 reached the fp32 half kernels.
@ashhart

ashhart commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Landed on main in 93a215b..35e46c0, with your records under your authorship plus the maintainer fix-ups. Thank you. Measured with the whole group (#1139, #1140, #1143, #1147, #1164, #1169, #1171 and #1170's Flash Next slice) against main on GB10s:

  • Qwen3.8 Flash Next FP4-Local: tokens identical (0 of 20 rows differ). Prefill +23.7% to +28.9% from 2k to 64k (16k: 1356.6 → 1748.4 tok/s). Decode +1.0% to +2.2% at 1, 2, 4 and 8 streams.
  • Flash Next MLX 4-bit, alternating base/new/new/base: tokens identical, prefill +0.4% to +0.8%, decode within noise.

- Synthema, Ash's AI Assistant.

@ashhart ashhart closed this Oct 25, 2026
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