Skip to content

Add Gemma 4 text model builder support (dense gemma-4-12b-it + 26B-A4B MoE) - #2473

Open
Thiago Pereira Rocha (thpereir) wants to merge 4 commits into
microsoft:mainfrom
thpereir:gemma4-base-upstream
Open

Add Gemma 4 text model builder support (dense gemma-4-12b-it + 26B-A4B MoE)#2473
Thiago Pereira Rocha (thpereir) wants to merge 4 commits into
microsoft:mainfrom
thpereir:gemma4-base-upstream

Conversation

@thpereir

Copy link
Copy Markdown
Contributor

Summary

Adds model-builder support for Google's Gemma 4 text models, producing GenAI-compatible ONNX:

  • Dense gemma-4-12b-it (Gemma4UnifiedForConditionalGeneration) via a new Gemma4Model.
  • MoE gemma-4-26B-A4B-it (Gemma4ForConditionalGeneration) via a new Gemma4MoEModel, including the parallel dense-MLP-alongside-experts topology and the float (MoE) / quantized (QMoE) op-type split.

Both inherit from the existing Gemma3Model and override only the hooks that differ:

  • Per-layer-type nested RoPE parameters ({"full_attention": {...}, "sliding_attention": {...}}) instead of the flat rope_parameters["rope_type"] form — handled via a make_rope_init override.
  • Gemma4 keeps a parallel dense MLP alongside the experts, so intermediate_size is pinned to the dense size while the expert size is tracked separately as moe_intermediate_size.

Scope / relationship to prior work

This is the text builder layer. It complements the already-merged Gemma 4 C++ runtime / multimodal processor. It supersedes the non-functional scaffolding stub in #2088 (which prints "not yet end-to-end functional" and raises NotImplementedError) — here the dense and MoE text paths build end-to-end and are unit-tested. See #2062 for the architecture background.

Tests

  • test/python/models/test_gemma4_builder.py — dense Gemma4 builder.
  • test/python/models/test_gemma4moe_builder.py — MoE builder incl. moe_op_type (MoE vs QMoE) and dense/expert intermediate-size wiring.

All 16 tests pass locally.

Follow-ups (separate stacked PRs)

  • 2-bit (uint2) QMoE support.
  • 4-bit (Quark/AWQ int4) expert generalization.

Draft: opening for early review while the stacked quantization PRs are prepared.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Comment thread src/python/py/models/builders/gemma.py Fixed
Comment thread src/python/py/models/builders/gemma.py Fixed
Comment thread src/python/py/models/builders/gemma.py Fixed
Comment thread src/python/py/models/builders/gemma.py Fixed
Adds Gemma4Model builder for the text component of Gemma4Unified
(Gemma4UnifiedForConditionalGeneration). Verified against HuggingFace:
full 48-layer fp32 parity gives logit correlation 1.0 and exact top-1
match.

Handles the architecture's deviations from Gemma3:
- Per-layer head dims (sliding 256 / full 512) and KV heads (8 / 1),
  emitted as concrete per-layer KV cache shapes so the runtime's
  DefaultKeyValueCache auto-detects the heterogeneous layout.
- attention_k_eq_v: full-attention layers share the K projection as V
  (no v_proj), plus a scaleless value RMSNorm (v_norm) on all layers.
- Proportional RoPE on full layers (partial rotary on the global head
  dim with a zero-padded NoPE tail), default RoPE on sliding layers.
- Per-layer layer_scalar residual multiplier.
- No-offset RMSNorm (weight used directly, unlike Gemma1/2/3).
- Text-only weight loader that remaps the model.language_model.* prefix
  and avoids loading the vision/audio towers.

Emits model_type "gemma4_text" to match the runtime's model type list.

Co-Authored-By: Claude <noreply@anthropic.com>
Add Gemma4MoEModel subclassing the dense Gemma4Model. Every layer runs a
parallel dense MLP and a fused-QMoE expert block, combined by Add. Uses the
fused QMoE op path (gelu + swiglu_fusion=1 + normalize_routing_weights) with an
explicit router pre-projection subgraph feeding raw logits, and folds
per_expert_scale into the expert down_proj weights offline.

Co-Authored-By: Claude <noreply@anthropic.com>
…nature)

Co-Authored-By: Claude <noreply@anthropic.com>
…tub test

- make_moe_init: set both moe_attrs["op_type"] and ["moe_op_type"] to the
  resolved op. The shared emitters read "op_type" while the gemma parallel-FFN
  path reads "moe_op_type"; setting only one left quantized experts on the
  float "MoE" op, which rejects uint8 weights.
- Gemma4MoEModel.__init__: hide config.moe_intermediate_size across
  super().__init__ so the base sets self.intermediate_size to the dense size
  directly, instead of reassigning it afterward (CodeQL "overwriting attribute").
- test_qmoe_weights.py: add Gemma4Model/Gemma4MoEModel to the stubbed `builders`
  class list so the builder-CLI import test matches builder.py's imports.

Co-Authored-By: Claude <noreply@anthropic.com>
@thpereir
Thiago Pereira Rocha (thpereir) marked this pull request as ready for review August 28, 2026 14:28
Copilot AI lite review requested due to automatic review settings August 28, 2026 14:28
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Python model-builder support for Google Gemma 4 text models (dense + MoE), integrating new Gemma4 builder classes into the dispatcher and extending unit tests to validate Gemma4-specific RoPE/KV-cache geometry and MoE wiring.

Changes:

  • Introduces Gemma4Model and Gemma4MoEModel builders with Gemma4-specific RoPE cache handling, per-layer KV-cache geometry, and (for MoE) parallel dense+expert FFN construction.
  • Wires Gemma4 architectures into create_model() dispatch and exports the new builders from the package.
  • Adds focused unit tests for dense Gemma4 and Gemma4 MoE builder structure/parity, plus updates QMoE weight tests to include Gemma4 builders.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/python/models/test_gemma4moe_builder.py New unit tests covering Gemma4 MoE builder (attrs/op selection, parallel FFN combine, router path, per-expert scale folding).
test/python/models/test_gemma4_builder.py New unit tests covering dense Gemma4 builder (per-layer KV shapes, RoPE cache parity vs HF reference).
test/python/builder/test_qmoe_weights.py Extends builder CLI test allowlist to include Gemma4 builders.
src/python/py/models/builders/gemma.py Implements Gemma4Model and Gemma4MoEModel builder logic (RoPE, KV cache geometry, layer scalar, MoE FFN).
src/python/py/models/builders/base.py Ensures MoE op-type is consistently available under both moe_attrs["op_type"] and moe_attrs["moe_op_type"].
src/python/py/models/builders/init.py Exports the new Gemma4 builder classes.
src/python/py/models/builder.py Adds dispatcher branches for Gemma4 architectures and emits warnings about precision and unsupported towers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +546 to +567
def make_gemma4_rmsnorm(self, name, root_input, weight, with_scale=True):
# Standalone Gemma4 RMSNorm (SimplifiedLayerNormalization, fp32 accumulation)
# on a [B, S, H] tensor. `weight` is the HF parameter (or None if scaleless).
weight_name = f"model.{name}.weight"
if with_scale:
self.make_initializer(weight + self.layernorm_attrs["add_offset"], weight_name, to=self.io_dtype)
else:
self.make_initializer(torch.ones(self.hidden_size), weight_name, to=self.io_dtype)
# Node paths use '/' separators (initializer names keep the '.'-joined form).
ln_name = f"/model/{name.replace('.', '/')}/SimplifiedLayerNormalization"
output = f"{ln_name}/output_0"
self.make_node(
"SimplifiedLayerNormalization",
inputs=[root_input, weight_name],
outputs=[output],
name=ln_name,
epsilon=self.layernorm_attrs["epsilon"],
axis=-1,
stash_type=1,
)
self.make_value(output, self.io_dtype, shape=["batch_size", "sequence_length", self.hidden_size])
return output
Comment on lines +539 to +544
def make_layer(self, layer_id, layer):
# Stash the full layer so make_mlp can reach the router/experts/extra norms
# (the parent only passes layer.mlp to make_mlp).
self._current_layer = layer
super().make_layer(layer_id, layer)
self._current_layer = None
Comment on lines +472 to +476
moe_intermediate_size = config.moe_intermediate_size
del config.moe_intermediate_size
super().__init__(config, io_dtype, onnx_dtype, ep, cache_dir, extra_options)
config.moe_intermediate_size = moe_intermediate_size
self.moe_intermediate_size = moe_intermediate_size
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants