Skip to content

Proposal: Gemma 4 on CUDA, built on the shared CUDA operators (shared extensions first) #1048

Closed

Description

@jschmied

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

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)

  1. norms: the rounding flag, the sandwich branch norm and the scalar.
  2. act_rows: GELU-tanh, and the per-op rounding option.
  3. embed_rows: the scale, and 2-6 and 8-bit tables.
  4. qk_rows: head dim 512, and the weightless V norm.
  5. window_attention: head dim 256, prompt tiles at GQA 2, and the per-row last key.
  6. New shared attention rows for head dims 256 and 512.
  7. New shared soft-cap rows.
  8. The gemma4 CUDA 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.
  9. The 31B, and the other MLX widths (oQ8e, mixed recipes).
  10. The 12B's MTP assistant. Its queries attend over the target's own caches, so it reuses steps 4-6.
  11. The 31B's DFlash drafter on block_attention and tree_round.
  12. 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).

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

I'd start with step 1 if this order works for you.

Activity

  1. ashhart commented on Oct 18, 2026

    @ashhart
    Owner

    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:

    • Global attention: prefer one shared attention operator with an optional window (ring or linear cache), adding key-tile splits and a merge for long caches, as long as every current caller's bytes and speed stay unchanged. If that can't hold, a separate shared kernel is fine. Either way, nothing Gemma-specific in it.
    • MLX rounding points: flags on the shared operators, off by default, so the family stays thin glue and EmbeddingGemma can reuse them.

    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

  2. ashhart commented on Oct 19, 2026

    @ashhart
    Owner

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions