Repository navigation
Conversation
…32 probabilities window_rows.cu: decode by 256-key chunks and a merge in chunk order, prompt tiles over 128-dim slices; Window.p32 keeps P to 2^-16 as two bf16 halves (Metal's rounding points), promptRows takes each row's last key. window_attention.cu and its callers are unchanged. core/attention_range.zig: float64 at the rounding points and the range fp32 allows.
|
Thanks @jschmied for putting the new arithmetic in its own - Synthema, Ash's AI Assistant. |
|
Thanks for carrying it. Please limit the float64 re-run to the affected row and head, as the description says. There is no reason to keep the whole block: the block-wide OR was a shortcut, and it makes a finite row's bits depend on where the chunk boundary falls, which breaks resumed == fresh across different cuts. The neighbouring-NaN device test you describe, alone, beside and split across chunks with bf16 P and P32, is the right check for it. Not in the middle of a change here or on #1124. |
…tract list, its RMS test run, contract entries at more GQA ratios - The contract's host-test step moves from cuda.zig, 607 lines with the PR, to cuda_row_contracts.zig's list, which builds the same test; cuda.zig is main's again. - contract_window_rows.zig references window_rows_check.zig in a test block, so its RMS test runs (nothing compiled it, and a wrong expectation passed). - Contract entries at 12/4, 20/4, 32/4 and 32/2 heads (GQA 3, 5, 8, 16), each for decode and for prompt rows with P as bf16 and P32, since the API takes any ratio up to 16 and the entries covered 2; plus the decode-merge and P32 window-edge controls at GQA 5.
…im 512, the 17-row decode count back - Contract entries at head dim 512 with 10 query heads over 2 (GQA 5): decode, prompt rows with P32 and with P as bf16, and the 512 merge control, so each key head's second block of four heads holds one (window_rows512.cuh's partial group) and the odd-ratio prompt mapping runs; the device tests gain the same decode and prompt cases. - decode1 runs 1, 3, 17 and c.rows rows again (those under c.rows), so the 256 decode cases check 17 rows as #1123 did, and the 512 cases with 4 or 17 rows keep their counts.
|
Thanks @jschmied. - Synthema, Ash's AI Assistant. |
|
This shipped in TensorFold 1.1.4: https://github.com/ashhart/TensorFold/releases/tag/v1.1.4 Thank you @jschmied for the contribution. - Synthema, Ash's AI Assistant. |
What this changes
Includes
core/bf16_round.zigwith #1056's sign fix (an underflow to zero keeps its sign, checked by raw bits), so the steps share one identical file.Step 5 of #1048:
window_attentionserves head dim 256 with any number of query heads per key head (up to 16, so GQA 2 included), a per-row last key, and Metal's rounding points as an option. Nothing calls it yet.It is one operator, as agreed on #1048.
WindowAttentionkeeps its API, anddecodeandprompttake head dim 256 through the sameWindow. The new arithmetic lives in a new image,window_rows.cu, sowindow_attention.cu, its fatbin and every current launch stay as they are. The only caller today is Kolibri-1 (head dim 128); its kernels, arguments and timings are unchanged (receipt below).Decode: 256-key chunks at absolute key positions, one block for each (row, key head, chunk), then a merge in chunk order. A chunk scores its keys in fp32: each lane runs an fma chain over 8 dims, then a xor tree over the lanes. It takes the chunk's max, computes
expf(s - m)and their sum, and theno = sum_j p_j v_j: up to 8 key runs, each an fma chain in key order, summed in run order. The merge computeso = sum_c a_c o_c / sum_c a_c l_cwitha_c = expf(m_c - M), as an fma chain over the chunks in order. Probabilities stay fp32 throughout, as in Metal's decode. The split depends only on key positions, so a row's bits don't depend on the batch, the slot or the cache length (chunks with none of its keys are skipped exactly).Prompt: tensor-core tiles of 16 rows of one query head, with 2 warps per row band. Each warp takes a 128-dim slice: its share of
q . k, summed in slice order, then its 128 output columns. The keys come in 32-key tiles at absolute positions, each folded intooby one fma. A row's bits don't depend on how the prompt is chunked.Window.p32(off by default): P entersP x Vas two bf16 halves,bf16(P)andbf16(P - bf16(P)). That is fp32's probabilities to 2^-16, asgemma4/prompt_attention.metalkeeps them ("bf16 moved an 8k prompt's logits off mlx-lm"). With it off, P is one bf16, aswindow_attention.cu's prompt kernel rounds it.promptRows(..., hi): row i sees keys[p0 + i - win + 1, hi[i]](int32,p0 + i <= hi[i] < p0 + n), so an image's rows see each other, asprompt_attention.metal'sHIgives them. Without it (null, and inprompt), every row's last key is its own position.Scale is a parameter (Gemma uses 1.0). Window 0 runs over a linear cache (
ringits length) in both forms.A NaN or infinite output re-runs its head in float64 (attention_ref's arithmetic, off the common path), so non-finite outputs match float64's class.
core/attention_range.zigcomputes a head in float64 at Metal's rounding points (only the output's), plus the range each form's fp32 steps allow around it:|acc| + sum |a b|);expf(2 ulps) and__expf(2 + 1.173|x|ulps);oandl, and P's representation.The checker rounds both ends of the range to bf16 and compares in value order, -0 below +0. A one-value range must match bit for bit, NaN only matches NaN, and infinities are compared by their bits (cuda norms: bf16 residual tails with a branch norm, a layer scalar and MLX's rounded rows #1056's checker rules).
Gemma 4 uses
p32withhion its sliding layers (12B 16/8 heads, 31B 32/16; window 1024 over a ring of 1536).Receipt
Environment: TensorFold
d1018b31(1.1.1) plus this commit; one DGX Spark (GB10, sm_121, CUDA 13.0, nvcc 13.0.88). No checkpoint needed. Shapes come fromfamilies/gemma4/config.zig. Inputs followmlx-community/gemma-4-12B-it-4bit's norm weights, which are constant within a layer: q rows at RMS 0.95 to 1.05, k at 0.06 to 0.13, v at unit RMS. Every few rows one key equals a query head at k's RMS, a score near 30, so softmax is peaked as well as flat.Exactness,
tf-cuda-test window-rows:Decode speed, new shapes (
tf-cuda-test window-bench gemma, GPU µs, median of 25, at positions 1k / 8k / 32k / 128k):One 12B row reads 8 MiB of keys and values: 34 µs is about 245 GB/s.
Prompt speed, new shapes (512-row chunk ending at the context):
P32 costs 13 to 14 %.
Current callers' speed: unchanged.
window_attention's only caller is Kolibri-1. Its served shapes were timed with the same bench onmain(with the bench file added) and on this branch, ABBA x2 (A B B A A B B A), GPU µs, median of each side's 4 runs:tf_window_decode_tc, 1 rowwattn_kernel<128,8,4,8>, 2,048 rows at 0The same kernels from the same bytes run in both, so the differences are run-to-run spread. The +1.1 % row's runs overlap: main 561.5 to 571.6, branch 562.3 to 576.1.
Kernel contract (
docs/kernel-contract.md),tf-cuda-test kernel-contract(built-Dnvcc, ReleaseFast) on the GB10:SUMMARY 88 kernels: 88 pass gates 1-4, 0 fail, 0 known findings; 13 controls: 13 red as planted, 0 missed; 1533 limit problems(main:
SUMMARY 85 kernels: 85 pass gates 1-4, 0 fail, 0 known findings; 7 controls: 7 red as planted, 0 missed; 1533 limit problems).zig/tests/cuda/contract_window_rows.zig), at Gemma 4 12B's sliding shapes: decode (16 and 64 rows to position 40,000, one row at 1,100; key tiles' edges), P32 prompt with image rows, and bf16-P prompt (512 rows at p0 0 and 5,000, 96 rows at 1,300; row and key tiles' edges).sums.f16 = 100); bf16 sums miss by 77x (decode), 9.4x (P32) and 0.6x (bf16 P, whose own bound is P's 2^-8), so the spec says why (sums.bf16 = 0).exp(m_c - M), the merge rounding toward zero, P32 prompt rows whose window starts one key early, P32 prompt rows that ignore an image's last key, and P rounded to bf16 under the P32 range.tf_wrows_part_256,tf_wrows_merge_256,tf_wrows_prompt_256and_p32; 48 to 255 registers, the prompt rows at 80 KiB dynamic shared memory, opt-in).tf-cuda-test kernel-contract-limitsover fatbins for sm_80, 86, 89, 120 and 121 finds all records current. The run over every image reports 877 problems, asmain's does.SUMMARYline aremain's own (records 1.1.1 lists without a compiled usage yet); this commit adds none.Default path unchanged:
window_attention.fatbinfor sm_80, 86, 89, 120 and 121 is byte-identical tomain's.cuobjdump -sassof its five kernels matches on all five SMs (25 of 25), as dotree_attention,block_attention,prefill_attentionandnemotron_attention(470 of 470) and the existing controls (35 of 35).tf-cuda-test windowpasses as onmain.Tests run:
zig build testfrom an empty cache: 213/213 steps, 2,964 of 2,978 tests passed, 14 skipped (main: 211/211 steps, 2,959 of 2,973 tests passed, 14 skipped).tf-cuda-test window-rowsandwindowpass.zig fmt --checkis clean.lean_check.py: 444 findings, as onmain.Not run, and why: Metal (no Metal change). sm_80, 86, 89 and 120 only through their recorded compiles (gate 5); the tensor-core sum model behind the prompt range was checked on sm_121 only. No model calls it yet (step 8 of Proposal: Gemma 4 on CUDA, built on the shared CUDA operators (shared extensions first) #1048).
Overlap with #798
#798 rewrites
window_attention.cu: it moves the split decode towindow_split.cuhand adds the NaN and infinity re-runs (attn_odd.cuh). This PR doesn't touch that file. Its new kernels are inwindow_rows.cu, with their own float64 re-run. A test merge onto a scratch merge of #746 and #798 (ond46d17a7) leaveswindow_attention.cu,window_attention.zigandwindow_split.cuhclean. One conflict is new:registry.zig's record line, where #798's records end and this PR appends its file; it resolves by keeping both.build/cuda.zigandcontract_tests.zigalready conflict between that scratch and 1.1.1. The remaining shared lines are the registration lists:contract_tests.zig,registry.zig's record line,build/cuda.zig's host tests and the end ofcontract_controls.cu.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (No serving path changes; a row's bits are its own at any batch, slot, cache length or prompt chunking.)SUMMARYline is in the receipt.