| --- |
| license: gemma |
| base_model: google/gemma-4-12B |
| library_name: flax |
| pipeline_tag: image-text-to-text |
| tags: |
| - jax |
| - flax |
| - nnx |
| - gemma |
| - gemma4 |
| - port |
| - multimodal |
| - encoder-free |
| --- |
| |
| # gemma4-jax — Flax NNX port of `google/gemma-4-12B` (full multimodal) |
|
|
| A from-scratch, faithful **Flax NNX** implementation of Google's Gemma 4 12B |
| "Unified" model — the **text decoder** *and* the encoder-free **vision + audio** |
| embedders — plus a HuggingFace `safetensors → NNX` weight converter. |
|
|
| - 💻 **Code / GitHub:** https://github.com/mlnomadpy/gemma4-jax |
| - 🧬 **Base model:** [`google/gemma-4-12B`](https://huggingface.co/google/gemma-4-12B) |
|
|
| This repository hosts the **port (code)**. It does **not** redistribute the |
| weights — at load time it reads the official `google/gemma-4-12B` safetensors |
| directly (the tensor names match; `nnx.Linear` kernels are just transposed). You |
| must accept the Gemma terms on the base-model page to download the weights. |
|
|
| ## Why |
|
|
| Gemma 4 12B is not (yet) in the official `google-deepmind/gemma` JAX library. |
| This port makes the architecture an open, editable NNX module — so the global |
| attention layers, the encoder-free projectors, and the RoPE/norm details are a |
| clean swap point for research (e.g. linear-attention surgery on the 8 global |
| layers). |
|
|
| ## What's implemented — verified exact to the parameter |
|
|
| | Component | State | |
| |---|---| |
| | Text decoder (48 layers, dual sliding/global attention) | ✅ | |
| | Dual attention: sliding GQA (hd 256, 8 KV) / global MQA (hd 512, 1 KV) | ✅ | |
| | `attention_k_eq_v` (V reuses pre-norm K on full layers, no RoPE on V) | ✅ | |
| | Per-head QK-norm, sandwich norm, `layer_scalar`, embed scaling, logit softcap | ✅ | |
| | Proportional / partial RoPE (zeroed-tail inv_freq) + default RoPE | ✅ | |
| | **Vision** (encoder-free: raw 48×48×3 patches → LN→Dense→LN→+2D-posemb→norm→proj) | ✅ | |
| | **Audio** (encoder-free: raw 640-sample frames → RMSNorm→proj) | ✅ | |
| | **Multimodal splice** (soft-token scatter + bidirectional-vision mask) | ✅ | |
| | `safetensors → NNX` converter (text + vision + audio) | ✅ | |
| | KV cache for fast decode | ❌ reference loop recomputes prefix | |
| |
| **Param count matches the published checkpoint exactly:** text |
| `11,907,350,320` + multimodal `52,379,904` = **`11,959,730,224`** (0 diff). |
| The multimodal converter is verified against the real safetensors; text and |
| multimodal smoke tests pass (forward, causality drift 0, softcap bounds, splice). |
| |
| ## Non-obvious details baked in (vs. Gemma 2/3) |
| |
| - RMSNorm is **plain `x·w`** (not `(1+w)`); eps *inside* the rsqrt; fp32 internals. |
| - Attention **`scaling = 1.0`** — magnitude set by per-head `q_norm`, not `1/√d`. |
| - **`k_eq_v`**: global layers have no `v_proj`; V reuses the K projection output |
| (pre-norm, pre-RoPE) + a scale-free `v_norm`, and V is **not** rotated. |
| - **Proportional RoPE** on global layers: full-length inv_freq with only the |
| first 64 frequencies nonzero (NoPE tail), base 1e6. |
| - Vision is **encoder-free**: no SigLIP; raw merged pixel patches project |
| straight into the 3840-d decoder space. Audio likewise — no mel/conformer. |
| |
| ## Usage |
| |
| ```bash |
| pip install jax flax safetensors huggingface_hub tokenizers |
| git clone https://github.com/mlnomadpy/gemma4-jax && cd gemma4-jax |
| ``` |
| |
| ```python |
| import jax.numpy as jnp |
| from gemma4_jax.convert import unified_from_safetensors |
| from gemma4_jax.config import IMAGE_TOKEN_ID |
|
|
| # point at the official google/gemma-4-12B model.safetensors (accept terms first) |
| uni = unified_from_safetensors("path/to/model.safetensors") |
|
|
| # text |
| logits = uni.logits(input_ids) # [B, S, vocab] |
| |
| # vision: pixel_values [B,P,6912], image_position_ids [B,P,2] |
| soft = uni.get_image_features(pixel_values, image_position_ids) # [B,P,3840] |
| h = uni(input_ids, pixel_values=pixel_values, image_position_ids=image_position_ids) |
|
|
| # audio: input_features [B,T,640] |
| h = uni(input_ids, input_features=input_features) |
| ``` |
| |
| The HF image processor (patchify + 3×3 pool + position ids) and audio feature |
| extractor are not ported — feed pre-patchified `pixel_values` / pre-framed |
| `input_features` exactly as the HF processors emit them. |
| |
| ## License |
| |
| This **code** is a clean-room reimplementation. The **weights** it loads are |
| Google's Gemma 4, governed by the [Gemma Terms of Use](https://ai.google.dev/gemma/terms); |
| your use of the weights is subject to those terms and the Gemma Prohibited Use |
| Policy. See the [base model](https://huggingface.co/google/gemma-4-12B). |
| |