Skip to content

Add 2-bit (uint2) Gemma 4 MoE support to the model builder - #2482

Open
Thiago Pereira Rocha (thpereir) wants to merge 5 commits into
microsoft:mainfrom
thpereir:gemma4-2bit-upstream
Open

Add 2-bit (uint2) Gemma 4 MoE support to the model builder#2482
Thiago Pereira Rocha (thpereir) wants to merge 5 commits into
microsoft:mainfrom
thpereir:gemma4-2bit-upstream

Conversation

@thpereir

Copy link
Copy Markdown
Contributor

Summary

Adds a pre-quantized 2-bit (uint2) expert path for the Gemma 4 MoE builder (gemma-4-26B-A4B-it), consuming a fused Quark uint2 checkpoint directly into a QMoE graph.

  • builder.py: int2 precision plumbing (--precision int2, set_io_dtype/set_onnx_dtype int2 as a FLOAT carrier, supported_moe_quant_types).
  • loaders (quark/base): extend the Quark loader/repack to uint2 (group_size 64, MSB-first packing) and to the split gemma4 experts with bundled dense LoRA.
  • gemma.py: pre-quantized uint2 expert branch in make_moe_quark; emit block_size, fold router.per_expert_scale into down-proj scales, emit the shared gate/up prescale+rotation once before QMoE; omit zero-points on CUDA int2 (op reconstructs the symmetric -1.5*scale bias), keep float zero-points on CPU and interleave fc1 gate/up rows for the CPU kernel's swiglu_fusion=1 layout.

Stacking

This PR is stacked on top of the base Gemma 4 PR (#2473). Until #2473 merges, the diff here also shows the base-arch commits; the 2-bit-specific change is the top commit. Will rebase onto main once #2473 lands.

Test plan

  • test/python/models/test_gemma4moe_builder.py - added TestGemma4MoEQuarkPath: QMoE emits expert_weight_bits=2, CPU emits float per-group zero-points as QMoE inputs, CUDA omits zero-points. (10 passed)
  • ruff check clean on the test file.
  • End-to-end (offline): build fused Quark uint2 -> sanity generation -> wikitext PPL.

Generated with Claude Code

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>
@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
…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>
Build the fused Quark uint2 gemma-4-26B-A4B-it checkpoint to ONNX with a
pre-quantized expert path: consume group-wise uint2 weights/scales/float
zero-points directly, fold the router per-expert scale into the down-proj
scales, and emit the shared gate/up prescale+rotation once before QMoE.

- builder.py: add int2 precision plumbing
- quantized_model.py: extend the Quark loader/repack to uint2 (group_size 64,
  MSB-first packing) and to the split gemma4 experts with bundled dense LoRA
- gemma.py: pre-quantized uint2 expert branch in make_moe; emit block_size=64;
  omit zero_points on CUDA int2 (op reconstructs the symmetric -1.5*scale bias);
  keep float zero-points on CPU and interleave fc1 gate/up rows for the CPU
  kernel's swiglu_fusion=1 layout
@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 Gemma 4 text-model builder support with a new 2-bit (uint2) pre-quantized Quark MoE path, including loader-side repacking/requantization utilities, builder-side QMoE graph emission (with factored input rotation + optional zero-point omission on CUDA), and new unit tests validating structural/parity expectations.

Changes:

  • Add --precision int2 plumbing and quant-config mapping to drive uint2 weight/MoE quantization selection.
  • Extend Quark loader + base loader to support uint2 packing/unpacking, split-expert refusion, shared input rotations, and bundled dense LoRA adapter baking.
  • Add Gemma4/Gemma4MoE builders and tests validating RoPE cache parity, per-layer KV geometry, and QMoE uint2 behaviors (e.g., expert_weight_bits=2, CPU vs CUDA zero-point handling).

Reviewed changes

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

Show a summary per file
File Description
test/python/models/test_gemma4moe_builder.py Adds MoE-focused unit tests including the new Quark uint2 QMoE path expectations.
test/python/models/test_gemma4_builder.py Adds dense Gemma4 structural/parity unit tests (RoPE caches, KV-cache shapes, attention scale).
test/python/builder/test_qmoe_weights.py Registers Gemma4 builders in CLI-module loading for existing QMoE-related tests.
src/python/py/models/README.md Documents -p int2 usage for pre-quantized 2-bit MoE.
src/python/py/models/quantization/quant_config.py Adds uint2/int2 dtype descriptors and maps --precision int2 to weights.type=uint2 / MoE quant type.
src/python/py/models/loaders/quark.py Extends Quark loader for uint2 expert handling, unpack/repack, and 2-bit-aware packing utilities.
src/python/py/models/loaders/base.py Adds Gemma4-MoE layer/router structures, shared-input-rotation loading, LoRA adapter loading, and packed-splitting fixes for uint2.
src/python/py/models/builders/gemma.py Implements Gemma4 + Gemma4MoE builders, including QMoE emission for pre-quantized uint2 experts and per-layer geometry/RoPE behavior.
src/python/py/models/builders/base.py Threads true precision for int2, fixes MoE op-type wiring, adds factored-rotation + LoRA delta injection for quantized projections.
src/python/py/models/builders/init.py Exports Gemma4Model/Gemma4MoEModel.
src/python/py/models/builder.py Adds int2 precision option, dtype handling, supported MoE quant types, and Gemma4 architecture dispatch.
Suppressed comments (2)

src/python/py/models/loaders/base.py:1064

  • _packed_out_factor is declared as a @staticmethod, but it is only used via self._packed_out_factor(...). Making it a normal instance method avoids @staticmethod (which this codebase generally avoids outside of specific constructors) and keeps call sites unchanged.
    @staticmethod
    def _packed_out_factor(tensor, bits):

src/python/py/models/loaders/quark.py:212

  • _unpack_uint2_msb_first is introduced as a @staticmethod, but it’s only called as self._unpack_uint2_msb_first(...). Converting it to an instance method avoids @staticmethod usage while keeping call sites unchanged.
    @staticmethod
    def _unpack_uint2_msb_first(packed):

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

Comment on lines +273 to +274
# Factored online rotation (Quark rotation algo): x_rot = (x / input_prescale) @ shared_input_rotation_<in>.
# `shared_input_rotations` maps in_features -> [in, in] rotation matrix (shared across all layers).
Comment on lines +555 to +560
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 +18 to +20
# uint2/int2 pack 4 codes per byte (MSB-first); uint4/int4 pack 2 codes per byte.
_DTYPE_BITS = {"uint4": 4, "int4": 4, "uint2": 2, "int2": 2}

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