Last updated: 2026-07-19

Inference Runtime

Forward Cuda Gemma

All API Sections

CUDA forward pass for the dense gemma4 transformer (Gemma 4 31B-it).

Effort 22 — completes the 5/5 catalog on the 4090. Separate from forward_cuda.zig (qwen35/qwen36 hybrid-SSM family) because gemma4 is a plain transformer with a different per-layer geometry: sliding-window attention on a period-6 pattern (5 SWA + 1 full), per-layer head dims (256 SWA / 512 full) and KV-head counts (16 SWA / 4 full), per-head Q/K RMS norm + per-head V RMS normalize, four norms per layer (pre/post attn + pre/post ffn), GeGLU FFN, a learned per-layer output scale, scaled token embeddings, and a tied LM head.

Norm convention: the gemma RMSNorm `(1 + weight)` offset is baked into the GGUF weights at conversion (confirmed: attn_q_norm ≈ 1.0234), so every gemma norm reuses the standard `rms_norm` kernel; V uses `rms_norm_noweight`.

Attention scale: gemma4 sets f_attention_scale = 1.0 (no 1/sqrt(d) scaling). Final-logit soft-cap is monotonic, so it does not change the greedy argmax and is intentionally skipped here (correctness-first bring-up).

1 exports shown

struct

ForwardGemma

#
pub const ForwardGemma = struct

No public doc comment yet.

src/compute/forward_cuda_gemma.zig:336

Methods

15

method

ForwardGemma.decodeStep

#
pub fn decodeStep(self: *ForwardGemma, token: u32, pos: u32, run_layers: bool) !u32

One greedy decode step for `token` at sequence position `pos`.

src/compute/forward_cuda_gemma.zig:770

method

ForwardGemma.prefillStep

#
pub fn prefillStep(self: *ForwardGemma, token: u32, pos: u32) !void

Prefill helper (mirrors ForwardCuda.prefillStep): run every layer to build the KV cache, but SKIP the tail rms_norm + LM head + argmax — a prompt-internal token's logits are never read.

Saves the vocab-sized head matvec on T-1 of the T prompt tokens; the MoE gemma model (small active forward, full head) benefits most. Async layer ops drained here; bit-identical generation.

src/compute/forward_cuda_gemma.zig:824

method

ForwardGemma.prefillBatched

#
pub fn prefillBatched(self: *ForwardGemma, tokens: []const u32) !u32

Batched dense-gemma prefill: process ALL T prompt tokens at once, reading each weight ONCE for all tokens via the gemm_*_tiled_v2 register-blocked GEMMs (5.9× over the per-token matvec). Returns the last token's argmax (= the first generated token), exactly like running prefillStep on tokens [0..T-1] then decodeStep on the last. ADDITIVE: builds its own token-major scratch and never touches the single-token decode path.

Phase-1 scope is the DENSE gemma-31b (n_experts==0). Phase-2 cycle 1 adds the 26b MoE: its ATTENTION block is the SAME structure as the dense model, so it shares the batched attention path (GEMM Q/K/V/O + batched causal attn + batched norm/RoPE/KV-write) — bit-identical to the per-token attentionLayer. Its routed-expert FFN is still LOOPED per token (the existing single-token moeFfnBlock, fed each token's hidden slice via an alias-swap) because the FFN is position-independent → looping it is output-identical; full batched-expert routing (group T tokens by expert) is a later cycle.

src/compute/forward_cuda_gemma.zig:861

method

ForwardGemma.decodeBatch

#
pub fn decodeBatch(self: *ForwardGemma, tokens: []const u32, positions: []const u32, slots: []const u32, out_tokens: []u32) !void

Batched DECODE step (DENSE gemma only): advance B independent sequences by ONE token each in a SINGLE B-row forward. The projections + FFN reuse the batched-prefill GEMM path — `gemmDispatch`/`ffnBlockBatched` read each weight ONCE for all B rows (the launch / weight-bandwidth amortization that makes batching a throughput win). The attention inner (per-head V/Q/K norm + RoPE + KV-write + causal softmax) LOOPS per row through the proven single-token kernels (`rms_norm_rope_qkv` / `gemma_attention`) against each sequence's OWN KV slot at its OWN position — so row b attends over slot `slots[b]`'s history [0..positions[b]] only. Fusing that per-row loop into one batched-seq attention kernel is sub-step 1c (a perf step; the looped form here is already correct for mixed positions). ADDITIVE — the production `decodeStep` / `prefillBatched` paths are untouched.

Requires `allocSlotKv(n_slots, slot_ctx)` first, with n_slots > max(slots) and slot_ctx > max(positions). Each sequence b's slot must already hold its KV history for [0..positions[b]-1] (written by prior `decodeBatch` calls — e.g. a per-token B=1 prefill into the slot). Writes b's new K/V at its slot.

tokens[b] input token for sequence b this step positions[b] sequence b's current position (slot holds [0..pos-1]) slots[b] sequence b's KV slot index out_tokens[b] greedy argmax for sequence b (caller advances pos + feeds back)

src/compute/forward_cuda_gemma.zig:1239

method

ForwardGemma.allocSlotKv

#
pub fn allocSlotKv(self: *ForwardGemma, n_slots: u32, slot_ctx: u32) !void

Allocate slot-based KV for `n_slots` concurrent sequences of up to `slot_ctx` positions each.

ADDITIVE: a fresh per-layer allocation that never aliases or touches the production single-sequence kv_k/kv_v. The batched-decode path (sub-steps 1b/1c) writes/reads it via `slotKvOffsetBytes`; the production decodeStep is unchanged. Idempotent — frees any prior slot KV first. Sub-step 1a only allocates + smoke-tests it.

src/compute/forward_cuda_gemma.zig:1650

method

ForwardGemma.slotKvOffsetBytes

#
pub fn slotKvOffsetBytes(self: *const ForwardGemma, L: u32, slot: u32, pos: u32) usize

Byte offset of sequence-slot `slot`'s K/V for position `pos` in layer `L`'s slot KV buffer: (slot*slot_ctx + pos)*kv_dim(L)*sizeof(f32).

This is the exact indexing the 1c per-sequence kv-write + slot attention kernels use.

src/compute/forward_cuda_gemma.zig:1709

method

ForwardGemma.slotKvSmoke

#
pub fn slotKvSmoke(self: *ForwardGemma) !bool

Sub-step 1a plumbing smoke: prove the slot-KV offset arithmetic round-trips and that distinct (slot,pos) pairs map to NON-overlapping device regions.

Writes a sentinel into (slot 0, pos 0) and a distinct pattern into the LAST (slot, pos) of layer 0's K cache, reads both back, and checks neither write clobbered the other. Returns true on success. Requires allocSlotKv first.

src/compute/forward_cuda_gemma.zig:1718

method

ForwardGemma.ffnLayerPub

#
pub fn ffnLayerPub(self: *ForwardGemma, L: u32) !void

FFN block dispatched exactly as decodeStep: routed MoE when this layer carries a router, dense GeGLU otherwise.

src/compute/forward_cuda_gemma.zig:2493