Repository navigation
Conversation
A prompt chunk's sparse attention ran the scalar parts kernel: one block per (kv head, row, part), each staging its own row's selected keys. On gfx11 a new tile takes 4 query rows x the kv head's query heads as WMMA columns over the union of those rows' selected blocks, with per-row block masks, the causal end per row, fp32 online softmax, non-finite values isolated per row, and the existing parts/merge layout. Opt-in with --policy sparse=wmma; prompt chunks only, so verify windows and decode keep the scalar kernel. Registered with the HIP kernel contract against an fp64 reference. Fixes ashhart#1597
|
Thanks for this. A WMMA tile for sparse prefill is a big piece of work, and keeping it opt-in, with contract records and an fp64 test, is the right shape. The main question is exactness. The tile rounds q*scale and the softmax probabilities to 16 bits before the products, the contract spec marks it Two smaller points:
Also, #1614 edits the same lines in |
…the Choices comment
q*scale and the softmax probabilities were rounded to the cache's 16-bit type before the products. Each is now carried as three 16-bit parts whose fp32 sum is the value, and each product takes one matrix op a part with fp32 accumulation, so the 16-bit rounding of q and P is gone and the tile sits at the scalar kernel's fp32 error. Three Q fragment sets would spill at 256 threads, so a block is 512 threads with four waves splitting the head dimension of a score tile. The kernel test and the contract hold the tile to the scalar kernel's bound against fp64 and drop the 16-bit allowance. The sums still differ from the scalar kernel's in order, so output tokens can still differ.
|
Thanks for the careful review. Short version: q and P are now fp32 in the products, and the tile is at the scalar kernel's fp32 error against fp64. It still does not match the scalar kernel's bits, and output tokens still differ, so I can't make the exactness case you asked for. Details below so you can decide whether it is worth keeping as an opt-in or should be closed. What changed
Why the bits still differThe scalar kernel splits a row's keys into parts by position in its key list, walks 32-key tiles with sequential fp32 FMAs and keeps a per-row online max. The tile splits parts by cache block, walks 16-key union tiles and accumulates in the matrix cores' order. Those are different fp32 sums of the same terms, so they round differently in the last bits, and a 1e-7 attention difference is enough to flip a later bf16 rounding and then a greedy token. I don't see a way to reproduce the scalar order with matrix-core accumulation without giving up the tile. Receipts (gfx1151, bf16 cache)Max abs error, kernel test shapes:
Kernel time for a 64-row slice with 2,048 selected keys a row: scalar 8.71 ms, old 16-bit tile 0.98 ms, fp32 tile 1.18 ms. Served,
How often tokens differ from the scalar path, T=0:
Gates on this branch: So the option is now "fp32-accurate, 1.6x faster cold prefill, not bit-identical, tokens differ". If that is not enough for an opt-in under the exactness rule, I'm fine closing this; it was worth knowing where the line is. |
What this changes
An opt-in WMMA kernel for Flash Next's sparse attention on prompt chunks on gfx11 (
--policy sparse=wmma, defaultscalar): 4 query rows x the kv head's 12 query heads as 48 WMMA columns over the union of those rows' selected blocks, per-row block masks and causal ends, fp32 online softmax, the existing parts/merge layout. Only prompt chunks take it (o.prefill); MTP verify windows, lane rounds and decode keeptf_fn_sparse_parts12. Fixes #1597.This changes the prompt's attention sums (Q, K, P and V rounded to the cache's 16-bit type in WMMA fragments with fp32 accumulation, and a different key partition and order), so it needs the arithmetic gate agreed in #1597 before it could become a default; it stays opt-in here.
Receipt
7c068895plus this change,tensorfold-native 1.1.8built withzig build native-hip -Dhip-arch=gfx1151, Ubuntu archive HIP 7.1 (clang 21), AMD Ryzen AI MAX+ 395 with Radeon 8060Sgfx1151(128 GB unified memory). CheckpointTensorFold/Qwen3.8-Flash-Next-MLX-4bit-MTPrevision2b170fa6, MTP 3 drafts,--parallel 1 --context 131072 --prompt-cache-gib 0, base and candidate on the same machine.sparse=wmma:tools/bench_concurrent.py --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serialevery cellalone3/3 and 12/12 equal andserial(against"draft": false) equal, 0 failed, also withprefill_step=4096. Resumed equals fresh: a two-turn chat answered by a server with--prompt-cache-gib 6(turn 2 resumed at 10,944 and 12,224 tokens) gives the same replytoken_sha256as a fresh process with the cache off, undersparse=wmmaand under the scalar kernel; resume points sit on the prompt cache's 64-row grid, which is also the attention slice, so every 4-row tile starts at the same position in both runs. Against the scalar kernel the outputs differ (the sums change): the 16-token replies to both cold prompts are identical, the two 256-token greedy replies diverge partway.tools/bench_openai.py --tokens 256 --reps 3 --temperatures 0, median tok/s code / chat: base 49.8 / 43.8,sparse=wmma47.1 / 40.1. Decode runs the same scalar kernel in both; the replies themselves differ (prompt sums change), and with them MTP acceptance.tools/bench_cache.py --require-cold --tokens 16on public 7,377- and 29,364-token arrays, first cold requestttft_s: base 48.8 / 111.0 s;sparse=wmma40.4 / 74.6 s;sparse=wmma,prefill_step=409639.2 / 66.9 s. The kernel's own time on a 7.9K-token prompt drops from 9.81 s to 0.73 s (rocprofv3).zig build hip-kernel-contracton gfx1151:SUMMARY gfx1151 50 kernels: 50 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 126 limit problems(base: 49 kernels, the same 126 live-versus-recorded register differences for existing kernels under this machine's compiler); the new entries: non-finite (a NaN or Inf a row did not select does not reach it; a selected NaN propagates), repeat, 7 shapes within the fp64 bound (worst 0.021 of it), sums, and live limits matching their records.zig build test -Dhip=true --summary all466/466 steps, 0 failed.zig build hip-kernel-test: the new sparse prompt test passes; the run then stops at the same Nemotron test as the base build on this machine.python3 tools/zig/lean_check.py425 on base and change.git diff --checkclean.hip-kernel-contract-limitsstops on this machine's compiler attiles/dot2_tiles.hipfor gfx1100 (branch size exceeds simm16, same on the base, see HIP: dot2_tiles.hip fails to compile for gfx1100 with Ubuntu clang 21.1.8 ("branch size exceeds simm16") #1521); records for gfx1030, gfx1100 and gfx1201 come from each chip's compiled code object, and those chips keep the scalar kernel at runtime (the policy is ANDed with gfx11 WMMA support). Resume points off the 64-row grid (explicit request chunks). Other checkpoints and AMD chips; Metal and CUDA.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.SUMMARYline is in the receipt.