Repository navigation
Proposal: Gemma 4 on CUDA, built on the shared CUDA operators (shared extensions first) #1048
Description
Question: is a CUDA backend for the gemma4 family wanted? I'm asking before writing any code. If the answer is "not now", that's fine and this can close.
The plan builds Gemma 4 on the shared CUDA operators from #746, #797 and #798. The extensions those operators need are general, and they land first, each as its own PR.
Why now
- Gemma 4 (31B and 12B, Gemma 4 31B served natively on Metal: the dense family, DFlash drafts, tensor-unit prompts past mlx-lm, and core affine_mm without its epilogue #879, Gemma 4 12B on the native Metal engine: text, images and its MTP assistant (stacked on #879) #975, Gemma 4 at every MLX affine width, uniform or per module: oQ packs on the native Metal engine (stacked on #975) #977) runs on Metal only. On CUDA, a
gemma4orgemma4_unifiedcheckpoint gets "the native CUDA engine has no backend" (zig/src/native/cuda.zig:203). - No family calls most of the shared CUDA operators yet (Zig CUDA ports: shared layers below
lanes.Backend, and one place to coordinate them #548). Gemma 4's layer is mostly what they compute already:- RMSNorm rows;
- QK norm and RoPE with the cache write;
- window attention over a ring;
- activation rows with 64-value group sums;
qlinear's MLX affine paths (4-bit, and 2, 3, 5, 6 and 8 bits, in groups of 64);tree_round's argmax;block_attentionfor the DFlash drafter (head dim 128, GQA 8, window, causal block).
- RFC: an OpenAI-compatible /v1/embeddings endpoint (first model: EmbeddingGemma-2 text) #1035's EmbeddingGemma-2 family will reuse Gemma 4's layers, Metal first and then CUDA. The layer operators below would be there for it.
- I found no issue, PR, fork branch or Zig CUDA ports: shared layers below
lanes.Backend, and one place to coordinate them #548 claim for Gemma on CUDA.
What the Metal family computes, and where it lands on CUDA
The reference is zig/kernels/metal/gemma4/glue.metal, including its rounding points (MLX's bf16 rounding after each op).
| Op | Shared CUDA op | Change needed |
|---|---|---|
| Embedding: affine dequant × bf16(√hidden) | embed_rows |
A scale argument; 2-6 and 8-bit tables besides 4-bit |
RMSNorm bf16(w · bf16(x · r)), the weight as stored (no 1 + w) |
norms |
A flag that rounds the normalized row to bf16 before the weight |
Sandwich tails: h + post_norm(o), then pre_norm; (h + post_norm(y)) · layer_scalar, then the next layer's norm, with group sums |
norms (add_rms_rows_bf16h) |
A weighted norm on the branch before the add; an optional bf16 scalar on the sum |
| QK norm, unweighted V norm, NeoX RoPE (sliding θ 1e4; global θ 1e6 on 64 of 256 pairs, as zero frequencies), K and V into a ring or a linear cache | qk_rows |
Head dim 512 (today ≤ 256); a weightless norm for v; the rounding flag. V read from K's raw columns works through the source strides |
| Sliding attention: window 1024, scale 1.0, GQA 2 | window_attention |
Head dim 256 (today ≤ 128); prompt tiles at GQA 2 (today multiples of 4); an optional per-row last key, so an image's rows see each other |
| Global attention: head dim 512, GQA 8 (31B) or 16 (12B), scale 1.0, cache to 262k | none | New shared attention rows for head dims 256 and 512: decode split by key tiles plus a merge, and prompt tiles. This is the one new attention kernel; nothing in it is Gemma-specific |
| GeGLU (tanh GELU), MLX's per-op bf16 rounding, group sums | act_rows |
A GELU-tanh mode beside SwiGLU, and the same per-op rounding option for the DFlash drafter's SwiGLU |
| Projections | qlinear |
None |
Final logit soft-cap 30 · tanh(x / 30), bf16 steps |
none | Small new shared rows op, for any soft-capped head (Gemma 2, 3, 4 and others) |
| Greedy pick | tree_round argmax |
None |
Most of these operators have no kernel-contract registration yet. Each step below registers the operator it extends, unless the contract work on 1.1.1's list has already done so.
The family keeps only Gemma glue:
- config admission and refusals (shared with Metal);
- the weight mapping;
- the layer order and the global-every-sixth pattern;
- the global RoPE frequency table;
- the cache layout;
- the lane and graph wiring.
Steps (one PR each, shared parts first)
norms: the rounding flag, the sandwich branch norm and the scalar.act_rows: GELU-tanh, and the per-op rounding option.embed_rows: the scale, and 2-6 and 8-bit tables.qk_rows: head dim 512, and the weightless V norm.window_attention: head dim 256, prompt tiles at GQA 2, and the per-row last key.- New shared attention rows for head dims 256 and 512.
- New shared soft-cap rows.
- The
gemma4CUDA family on the 12B (4-bit), text only:- plain decode, prompt chunks, lanes and prompt reuse;
- images refused by name, with a test of that refusal, until step 12.
- The 31B, and the other MLX widths (oQ8e, mixed recipes).
- The 12B's MTP assistant. Its queries attend over the target's own caches, so it reuses steps 4-6.
- The 31B's DFlash drafter on
block_attentionandtree_round. - Images:
- first the 12B's patch embedder (LayerNorm, Linear, positions, projection;
vit's LayerNorm needs widths up to 3,840); - then the 31B tower (27 layers, head dim 72 on
vit's attention, 2D RoPE, 3 × 3 pooling).
- first the 12B's patch embedder (LayerNorm, Linear, positions, projection;
Steps 1-7 each come with synthetic tests at Gemma's served shapes and tile edges, and with the CUDA SUMMARY lines. A change to an existing kernel leaves every current caller's bytes unchanged: the default path stays byte-equal to v1.1.0, and only the new flags run new code.
Gates
- Per kernel: the float64 bound at every served shape and tile edge. Current callers byte-equal to v1.1.0.
- Per layer: hidden rows, keys and values against a float64 reference of the Metal family's formula. Prompt rows and decode rows are checked separately, and the worst column is reported.
- Against Metal: teacher-forced top-token differences and margins on the same checkpoint. Bits can't match across the two backends (Metal uses fast cos and sin, and its own sum orders), so this is a fidelity check, not an equality check.
- Engine: drafted == plain, concurrent == solo, resumed == fresh, token for token.
- Speed on GB10 for each family step (
bench_openai.py,bench_concurrent.py), with receipts.
Hardware
- GB10 (sm_121) for every run and receipt.
- sm_80, sm_86, sm_89 and sm_120: compile and limits records only (gate 5). I can't run them.
- Metal: unchanged.
First checkpoint: mlx-community/gemma-4-12B-it-4bit (MLX affine 4-bit, groups of 64, 6.8 GB). It needs no conversion and no 8-bit table, so step 8 doesn't wait on step 3's 8-bit part. Its patch_dense is quantized, so images on that checkpoint stay refused. For step 9, unigilby/gemma-4-12B-it-oQ8e (the recipe's checked 12B, 12.8 GB) and the 31B checkpoints follow.
Questions
- Is a Gemma 4 CUDA backend wanted now, or should it wait for the EmbeddingGemma-2 work in RFC: an OpenAI-compatible /v1/embeddings endpoint (first model: EmbeddingGemma-2 text) #1035?
- Should global attention be a new shared kernel (step 6), or
window_attentionwith no window and a linear cache? - Should the MLX rounding points be a flag on
norms,qk_rowsandact_rows, or Gemma glue in the family?
I'd start with step 1 if this order works for you.
Activity
ashhart commentedon Oct 18, 2026
Thanks @jschmied, and for asking first: yes, Gemma 4 on CUDA is wanted now, built on the shared CUDA operators as you lay out. Start with step 1. The shared layer operators will serve EmbeddingGemma-2's CUDA step too, so there's no need to wait for #1035.
On your questions:
Gemma 4 reads images, so step 12 is part of the bar before CUDA lists the family as supported. Your gates look right.
Posted by Ash's AI Assistant Synthena
ashhart commentedon Oct 19, 2026
Thanks @jschmied. The design questions are settled: start with the shared CUDA operators, use the agreed shared attention and rounding controls, and include native images before listing Gemma 4 as supported. We'll close this proposal discussion and continue the work in your shared-operator PRs and the Gemma PRs that follow. The operator, family and model gates still apply; EmbeddingGemma's later CUDA step can reuse those operators.
Posted by Ash's AI Assistant Synthena
cuda act_rows: soft-capped logits in MLX's bf16 steps (tf_softcap_rows)
cuda gemma4: the dense family on the shared operators (text, MLX affi…
cuda gemma4: the 12B's MTP assistant as the drafter (#1048 step 10)
cuda gemma4: every MLX affine width the family admits (#1048 step 9)
cuda gemma4: images on the 12B (#1048 step 12)
cuda gemma4: z-lab's DFlash drafter on the 31B (#1048 step 11)
cuda gemma4: images on the 31B (#1048 step 12, the tower)