Skip to content

Generalize Gemma 4 MoE Quark path to plain Quark/AWQ int4 experts - #2483

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

Generalize Gemma 4 MoE Quark path to plain Quark/AWQ int4 experts#2483
Thiago Pereira Rocha (thpereir) wants to merge 6 commits into
microsoft:mainfrom
thpereir:gemma4-4bit-upstream-new

Conversation

@thpereir

Copy link
Copy Markdown
Contributor

Summary

Generalizes the Gemma 4 MoE make_moe_quark path so it also builds plain Quark/AWQ int4 experts (e.g. amd/gemma-4-26B-A4B-it-uint4-awq), not just the factored 2-bit LoRA-KD checkpoint. The previous code was hardwired for the 2-bit layout; this guards each 2-bit-specific step on the checkpoint's actual shape.

  • Input transform: emit the shared prescale+rotation only when input_prescale is present. Plain Quark/AWQ experts have none, so the MoE input feeds QMoE directly.
  • fc1 layout: gate the concat->interleaved fc1 row reorder on the 2-bit path (expert_weight_bits==2). AWQ int4 experts are already emitted interleaved by combine_and_repack_gate_up.
  • Zero-points: emit at native dtype unless float. 2-bit carries FLOAT zero-points (cast to io_dtype); int4 carries INTEGER (uint8) zero-points (emitted without cast). The CUDA zp-omit path stays 2-bit-only.

Stacking

This PR is stacked on top of the 2-bit PR (#2482), which is itself stacked on the base Gemma 4 PR (#2473). Until those merge, the diff here shows the parent commits; the int4-specific change is the top commit. Will rebase onto main as parents land.

Test plan

  • test/python/models/test_gemma4moe_builder.py - added TestGemma4MoEQuarkInt4Path: QMoE emits expert_weight_bits=4, no prescale/rotation subgraph, integer uint8 zero-points emitted without float cast, zero-points kept on CUDA int4. (14 passed total)
  • ruff check clean on the test file.
  • End-to-end (offline): build amd/gemma-4-26B-A4B-it-uint4-awq -> 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
make_moe_quark was hardwired for the factored 2-bit LoRA-KD checkpoint:
it unconditionally emitted the shared input prescale+rotation, re-interleaved
fc1 rows from concat layout, and cast zero-points to the float io_dtype. Plain
Quark/AWQ int4 checkpoints have no prescale/rotation, are already emitted
interleaved by combine_and_repack_gate_up, and carry integer (uint8) zero-points.

Guard all three on the checkpoint's actual shape:
- emit prescale+rotation only when input_prescale is present
- gate the concat->interleaved fc1 reorder on the 2-bit path
- emit zero-points at native dtype unless they are float

Verified end-to-end on amd/gemma-4-26B-A4B-it-uint4-awq (build + "Paris"
sanity + PPL 35.13 on wikitext_gpt_oss_120b).

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

This PR extends the Gemma 4 text model builder to better support Gemma4 MoE and Quark pre-quantized expert checkpoints by (a) generalizing the make_moe_quark path to handle plain Quark/AWQ int4 experts in addition to the existing 2-bit path, and (b) adding builder/loader plumbing for int2/uint2 precision and related behaviors across the Python model-builder stack.

Changes:

  • Generalize Gemma4 MoE pre-quantized Quark expert emission (input transform, fc1 reorder, zero-point dtype/omission) so the same path supports both uint2 (2-bit) and uint4/int4 (4-bit) expert layouts.
  • Add int2/uint2 precision support across quant config, CLI precision selection, and Quark loader repack/unpack utilities (including split-expert uint2 handling and LoRA adapter baking for quantized loads).
  • Add/extend unit tests for Gemma4 dense + MoE builder structure and for the new Quark int4 expert path expectations.

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/extends Gemma4MoE builder structural tests plus new Quark int4 path assertions.
test/python/models/test_gemma4_builder.py Adds dense Gemma4 builder tests (RoPE caches, per-layer KV geometry, attention scale/norm behaviors).
test/python/builder/test_qmoe_weights.py Registers Gemma4Model/Gemma4MoEModel in the QMoE weight test’s builder CLI module loader.
src/python/py/models/README.md Documents the new 2-bit (int2) pre-quantized MoE builder usage.
src/python/py/models/quantization/quant_config.py Introduces int2/uint2 descriptors and maps --precision int2 to uint2 weight/MoE quant selection.
src/python/py/models/loaders/quark.py Adds uint2 split-expert unpack/repack pipeline and generalizes packing/repack logic to 2-bit+4-bit.
src/python/py/models/loaders/base.py Adds Quark factored-rotation metadata parsing, shared rotation loading, LoRA adapter attachment for quantized loads, and packed-dimension helpers.
src/python/py/models/builders/gemma.py Adds Gemma4Model + Gemma4MoEModel builders and generalizes make_moe_quark for int4 experts.
src/python/py/models/builders/base.py Threads precision through quant config init, aligns MoE op-type attrs, and adds factored-rotation + baked LoRA emission in matmul path.
src/python/py/models/builders/init.py Exports Gemma4Model/Gemma4MoEModel from the builders package.
src/python/py/models/builder.py Adds int2 CLI precision, threads true precision through extra_options, and wires Gemma4 architectures into create_model.
Suppressed comments (1)

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

  • _packed_out_factor is added as a @staticmethod with a leading underscore in the model-builder code. Per the model-builder guidelines, avoid @staticmethod helpers (and leading-underscore names); prefer an instance method (e.g., packed_out_factor) or an existing shared utility so call sites remain easy to override/mock and follow the same conventions.
    @staticmethod
    def _packed_out_factor(tensor, bits):
        """Number of logical output channels stored per element along the packed
        (column) axis: 8//bits for uint8 packing (Quark native uint2/uint4),
        32//bits for int32 packing (AWQ/GPTQ style), and 1 for unpacked floating
        tensors (per-group float scales / float zero-points)."""
        if tensor.dtype == torch.uint8:
            return 8 // bits
        if tensor.dtype == torch.int32:
            return 32 // bits
        return 1

💡 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 +211 to +217
@staticmethod
def _unpack_uint2_msb_first(packed):
"""Unpack uint8 ``[rows, cols/4]`` (4x 2-bit codes per byte, MSB-first along
columns) into int codes ``[rows, cols]``."""
shifts = torch.tensor([6, 4, 2, 0], dtype=torch.int32, device=packed.device)
codes = (packed.to(torch.int32).unsqueeze(-1) >> shifts.view(1, 1, -1)) & 0x3
return codes.reshape(packed.shape[0], -1)
Comment on lines +830 to +845
def _load_lora_adapters(self):
"""Attach PEFT LoRA adapters (baked additively into the graph) if present.

The adapter lives at ``<input_path>/lora_adapters/adapter_model.safetensors``
with keys ``base_model.model.model.layers.{i}.{proj}.lora_A.weight`` [r, in]
and ``.lora_B.weight`` [out, r] for proj in {qkv_proj, o_proj, gate_up_proj,
down_proj}. ``lora_A`` is shared across split projections that share an input
(q/k/v share the qkv input, gate/up share the gate_up input); ``lora_B`` is
split along its output dimension. The builder emits the runtime delta
``(lora_B @ lora_A @ x) * scaling`` added to the quantized projection output.
"""
adapter_dir = os.path.join(self.input_path, "lora_adapters")
adapter_path = os.path.join(adapter_dir, "adapter_model.safetensors")
if not os.path.exists(adapter_path):
return

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