Repository navigation
Conversation
…d MLX's rounded rows addRmsTail extends addRmsH for Gemma 4's sandwich tails: an optional RMS norm on the branch before the residual add, an optional bf16 scalar on the sum (rounded first), and a rounding flag that gives each norm MLX's bf16(w * bf16(x * inv)). With no option set it is addRmsH itself, and the existing kernels' code is unchanged. The kernels are registered with the kernel contract: their own check compares every output with float64's value at the rounding points (a range over the fp32 inverse's spread), the 64-value sums bit for bit with the fp32 tree, and four controls each drop one rounding point.
tf_geglu_rows computes mlx_lm's gelu_approx(gate) * up as MLX's bf16 ops (Gemma 4's MLP), and tf_swiglu_rows_round SwiGLU in tf_draft_swiglu's bf16 steps (Gemma 4's DFlash drafter), both with the 64-value group sums of the stored outputs. swiglu keeps its kernel and launch. Both are registered with the kernel contract: every output against float64's value at the rounding points (a range over tanhf's or expf's ulps), the sums bit for bit with the fp32 tree, and twelve controls each drop one rounding point.
tf_qk_rows_once carries tf_qk_rows' arithmetic to head dim 512 and can norm v by a unit RMS norm; tf_qk_rows_round adds MLX's rounding points, q and k = bf16(w * bf16(x * inv)) before RoPE, as Metal's gemma_norm_rope computes them. Shape.round and Shape.v_norm select them, as does a head dim past 256; every other call keeps its kernel and its launch, and tf_qk_rows and tf_qk_rows_w keep their code. Both are registered with the kernel contract: their own check compares every output with float64's value at the rounding points (a range over the fp32 inverse's spread and cosf's ulps), and three controls each drop one option.
bf16(-2^-135) gave +0 where bf16 rounding gives -0 (0x8000). The tests now compare u16 bits: signed zeros from underflow, subnormal ties, the normal boundary, overflow to both infinities, and the random fp32 sweep against the fp32 path's bits.
EmbedRows.affine reads MLX's affine tables at 2, 3, 4, 5, 6 or 8 bits (groups of 32, 64 or 128) and, given a bf16 scale, multiplies each dequantized bf16 value by it and rounds again: bf16(bf16(q * s + b) * scale), Gemma 4's embedding times bf16(sqrt(hidden)). With 4 bits, no scale and groups of 32 or 64 it is affine4Groups itself, and the existing kernels' code is unchanged. The 4-bit rows and the new kernel at each width are registered with the kernel contract: every output bit-equal to float64's value at the rounding points, and two controls, one without the bf16 rounding before the scale and one folding the scale into the group's scale and bias.
…or one-value ranges A range with an infinite end fell back to the float64 point's class: it accepted a zero below a finite lower end and refused the infinite end itself. Every range is now checked by its ordered ends (-0 below +0), a one-value range by its bits, NaN only for NaN. The control is the 64-value input-norm row whose first output's range is [largest bf16, +Inf].
bf16(-2^-135) gave +0 where bf16 rounding gives -0 (0x8000). The tests now compare u16 bits: signed zeros from underflow, subnormal ties, the normal boundary, overflow to both infinities, and the random fp32 sweep against the fp32 path's bits.
bf16(-2^-135) gave +0 where bf16 rounding gives -0 (0x8000). The tests now compare u16 bits: signed zeros from underflow, subnormal ties, the normal boundary, overflow to both infinities, and the random fp32 sweep against the fp32 path's bits.
bf16(-2^-135) gave +0 where bf16 rounding gives -0 (0x8000). The tests now compare u16 bits: signed zeros from underflow, subnormal ties, the normal boundary, overflow to both infinities, and the random fp32 sweep against the fp32 path's bits.
…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.
…ttention window_rows.cu at head dim 512: decode partials with K and V tiles in shared memory, a block for 4 query heads and a warp for each key run, merged in chunk order as at 256; prompt tiles as at 256. No window over a linear cache to 262144 keys, GQA 8 and 16.
# Conflicts: # docs/kernel-contract.md # zig/build/cuda.zig # zig/src/core/contract/registry.zig
# Conflicts: # docs/kernel-contract.md # zig/build/cuda.zig # zig/kernels/cuda/contract_controls.cu # zig/src/core/root.zig # zig/tests/cuda/contract_tests.zig
# Conflicts: # docs/kernel-contract.md # zig/build/cuda.zig # zig/kernels/cuda/contract_controls.cu # zig/src/core/contract/registry.zig # zig/src/core/root.zig # zig/tests/cuda/contract_tests.zig
# Conflicts: # zig/kernels/cuda/contract_controls.cu # zig/src/core/root.zig # zig/tests/cuda/main.zig
…line limit The merges put build/cuda.zig at 614 lines and the shared CUDA registry at 603. The five Gemma operator contract tests (window rows, MLX activations, embed rows, qk rows) are one loop, and embed_rows' and qk_rows' records move to gemma_cuda_records.zig beside act_rows'. Nothing a test runs changes.
Step 7 of ashhart#1048: a soft-capped head's logits, cap * tanh(x / cap), as MLX computes them in bf16 ops (Metal gemma4/glue.metal's gemma_softcap): bf16(bf16(tanh(bf16(x / cap))) * cap), the division and the product correctly rounded in fp32 and rounded to bf16 at each point, tanhf for tanh. Nothing in it is Gemma's: any final_logit_softcapping head can call ActRows.softcap(s, x, out, rows, n, cap), in place or not. It sits in act_rows.cu beside the MLX activations; the module's 30 other functions keep their SASS on sm_80, 86, 89, 120 and 121. core/act_ref.softcap is the float64 reference at those points (fp32 then bf16, as the kernel rounds) and softcapRange its ends with tanhf within 2^-21 either way. Contract: tf_softcap_rows at Gemma 4's head (1 and 16 rows of 262,144 logits, 3 of 4,096), inputs to +-80 around the cap; two controls, one dropping x / cap's rounding, one tanh's (tf_ctl_softcap), both red at the shapes gate. Records for 256 and 128 threads from nvcc 13.0 (24 registers, 25 on sm_120 and 121). GB10: SUMMARY 108 kernels: 108 pass gates 1-4, 0 fail; 37 controls: 37 red as planted, 0 missed; 1533 limit problems (the base: 107, 35, 1533); both launches pass limits live.
…ne 4-bit) Gemma 4 (gemma4_unified 12B, gemma4 31B) on the native CUDA engine, every launch a shared operator from ashhart#1048 steps 1-7: qlinear's affine-4 tiles, embed_rows, the MLX-rounded norm tails, qk_rows, window_attention (sliding ring 1536, global linear), act_rows' GeGLU and soft cap, tree_round's bf16 argmax and the keyed draws. No new kernels. Config and widths are the Metal family's files, copied because a file belongs to one module; a host test holds the copies equal but for their first lines. Prompts run in chunks of up to 512 rows cut where Metal cuts, decode windows up to 16 rows a stream and 128 a forward; prompt-cache states keep global layers' positions and each sliding layer's last 1023. Other widths, images and drafters (steps 9-12) are refused by name.
A decode forward's launches depend on its row count alone: positions and slots are read on the device. Each row count (1 to 128) is captured at its first use and replayed for every slot and position after; TENSORFOLD_GEMMA4_GRAPHS=0 keeps every forward eager. Replies are bit-identical to the eager forward (36 of 36 token hashes, solo, four streams, copy drafts, resumed prompts, greedy and seeded sampling). On a GB10, alternating arms: 1 stream +1.3 %, 4 streams +1.6 %.
|
Thank you @jschmied. The seven Gemma 4 CUDA stages landed together on main (4167b87..850c692), with your records kept as authored and maintainer children for main's contract registrations and line limits. Every prefix passes its host gates. This stage (the dense family on the shared operators) ends at 4987903. On a GB10 with mlx-community/gemma-4-12B-it-4bit: prefill 2,560 tok/s at 2k; decode 37 tok/s on one stream and about 200 tok/s at 8; every stream equals its run alone and its serial run; chat resume equals fresh, thinking on and off. A 32k needle miss on this 12B reproduces with the same reply on main's M5 Metal path, so it is the model's. - 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
Gemma 4 text on the native CUDA engine, #1048 step 8. This covers
gemma4_unified(12B) andgemma4(31B) at MLX affine 4-bit, group 64. Every launch goes through a shared operator from steps 1-7, and the family adds no kernel of its own. Step 7 (the soft cap,tf_softcap_rows) is its own commit here, d3bfe17. Decode forwards run as CUDA graphs, one per row count. Part of #1048.Stacking. This branch is
cuda-attn-global(#1124, which itself carries #1123) with the other open step PRs merged in at their current heads:cuda-norms-gemma2b6cab853aa5a07dcuda-act-gelu6472617d7065e6b5cuda-embed-gemma82f714a27cb51714cuda-qk-gemma2a86f4fa794b6d23cuda-window-gemmaa2bed92acuda-attn-global74248000Then this branch's own commits:
How it merges.
main(d1018b31) is an ancestor too. So the branch merges intomainas a fast-forward, and with each of cuda norms: bf16 residual tails with a branch norm, a layer scalar and MLX's rounded rows #1056/cuda act_rows: GeGLU and SwiGLU in MLX's bf16 steps #1068/cuda embed_rows: MLX affine tables at 2 to 8 bits and a bf16 scale #1070/cuda qk_rows: head dims to 512, a weightless V norm and MLX's rounded rows #1071/cuda window_attention: head dim 256, any GQA, a row's last key and fp32 probabilities #1123/cuda window_attention: head dim 512 over a linear cache, for global attention #1124 cleanly, since it already holds them.mainon its own (GitHub: MERGEABLE).docs/kernel-contract.md,zig/build/cuda.zig,zig/kernels/cuda/contract_controls.cu,zig/src/core/contract/registry.zig,zig/src/core/root.zig,zig/tests/cuda/contract_tests.zig,zig/tests/cuda/main.zig.mainonce the steps are in.Receipt
3686e1cd(this branch).google/gemma-4-12B-it.token_sha256over 6 prompts, each greedy and seeded-sampled (temperature 0.8, top_p 0.95, seed 1234).--no-drafts,--prompt-cache-gib 0, graphs off."draft": false, resumed == fresh, concurrent == solo, and graphs == eager.Gemma4UnifiedForCausalLM) in fp32 on the checkpoint's dequantized weights, with the embedding scale bf16(sqrt(3840)) = 62.0 as the bf16 model runs it. It was teacher-forced over 8 greedy replies: 2,282 rows, prompts up to 2,463 tokens.ignore_eosand 256 tokens per reply, nottools/bench_openai.py.SUMMARY 108 kernels: 108 pass gates 1-4, 0 fail; 37 controls: 37 red as planted, 0 missed. That is the step PRs' entries plus the soft cap and its two controls. Limit problems are unchanged frommain.zig build test -Dnvcc=…: 225/225 steps, 1248/1251 tests passed, 3 skipped, 0 failed.test-gemma4-cuda-hostcovers: the copies equal Metal's files, RoPE tables, the embedding scale, snapshot runs over the sliding ring, worded refusals, and the stream fit.zig fmt --checkis clean.tools/bench_openai.pyandtools/bench_cache.py: the numbers above come from equivalent client scripts.Gotchas
gemma4model_type opens through the same path as the 12B (same layer kinds, affine-4/64). It has not been run, so the receipt covers the 12B only.--visionis refused by name, and no docs or support list change.--drafteris refused with Gemma's own message, and draft requests getNoDraftHead. The drafted == plain gate above is over prompt-copy drafts. A drafter comes with step 10 (12B MTP assistant) and step 11 (31B DFlash).TENSORFOLD_GEMMA4_GRAPHS=0turns them off, and the bits are the same either way.families/gemma4/cuda_config.zigandcuda_widths.zigcopy Metal'sconfig.zigandwidths.zig, because a file belongs to one Zig module. Only their doc line and imports differ, and a host test fails if anything past line 4 drifts. Admission and refusals are therefore Metal's, error names included.GemmaCudaWidthNotServed). That is step 9.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.SUMMARYline is in the receipt.