Skip to content

Support GraniteMoeHybrid dense variant (granite-4.0-1b) with L4/L5 tests - #440

Merged
justinchuby merged 2 commits into
mainfrom
feat/granite-4.0-1b
Jul 30, 2026
Merged

Support GraniteMoeHybrid dense variant (granite-4.0-1b) with L4/L5 tests#440
justinchuby merged 2 commits into
mainfrom
feat/granite-4.0-1b

Conversation

@justinchuby

Copy link
Copy Markdown
Member

Summary

Adds mobius support for the dense GraniteMoeHybrid variant ibm-granite/granite-4.0-1b, the model targeted by onnxruntime-genai#2124. Unlike granite-4.0-tiny-preview, this variant is all full_attention (no Mamba), uses RoPE, and has no routed experts (num_local_experts=0) — so it runs natively in transformers and a golden can be generated in CI.

L4 (prefill argmax) and L5 (20-token greedy generation) golden tests both pass on CUDA with exact parity to the HuggingFace reference.

What was broken for this variant

  1. layer_types mis-classifiedGraniteMoeHybridConfig inherited Bamba's "attention"/"mamba" naming, mapping every full_attention layer to mamba2.
  2. MoE built unconditionallyblock_sparse_moe asserted experts exist; granite-4.0-1b has none.
  3. NoPE hardcoded — the model always passed position_embeddings=None, but granite-4.0-1b uses position_embedding_type='rope' (θ=1e7).
  4. Missing Granite multipliersembedding_multiplier, attention_multiplier, and logits_scaling were not applied (latent, since tiny-preview was skipped).

Changes

  • _configs/_base.py: rebuild layer_types from GraniteMoeHybrid's full_attention/linear_attention naming.
  • models/granitemoehybrid.py:
    • MoE optional — dense shared_mlp-only feedforward when num_local_experts == 0 (mirrors HF).
    • RoPE via initialize_rope (present for rope, None for NoPE variants), threaded to attention layers.
    • Apply embedding_multiplier, attention_multiplier (attention scale), logits_scaling.
  • scripts/generate_golden.py / _testing/torch_reference.py: all-attention hybrids trip transformers' recurrent-mask builder (has_previous_state can only be called on LinearAttention layers); both forward and generate now fall back to use_cache=False on that specific error (greedy/logits output is cache-independent).
  • Tests: new testdata/cases/causal-lm/granite-4_0-1b.yaml (L4+L5) + generated golden.

Verification

  • TestL4CheckpointVerified + TestL5GenerationE2E for text-generation/granite-4_0-1b2 passed on CUDA.
  • build_graph_test.py -k "granitemoehybrid or granite or bamba"26 passed, 6 skipped (NoPE tiny-preview path unaffected).
  • ruff format + ruff check clean.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Extend the existing GraniteMoeHybrid model to support the dense
granite-4.0-1b variant (ORT-genai #2124): all full_attention layers,
RoPE, and no routed experts. Adds working L4/L5 golden tests.

What changed and why:

- Config (_base.py): GraniteMoeHybridConfig.from_transformers now
  rebuilds layer_types from GraniteMoeHybrid's own "full_attention" /
  "linear_attention" naming instead of inheriting Bamba's
  "attention"/"mamba" mapping, which mis-classified every
  granite-4.0-1b layer as mamba2. num_local_experts, RoPE params
  (rope_theta from rope_parameters), and the Granite scaling
  multipliers are already carried through the base config.

- Model (granitemoehybrid.py):
  - MoE is now optional: block_sparse_moe is built only when
    num_local_experts > 0; otherwise the feedforward is the dense
    shared_mlp alone (mirrors HF GraniteMoeHybridDecoderLayer).
  - RoPE support: the text model builds a rotary embedding via
    initialize_rope (present when position_embedding_type='rope',
    None for NoPE variants like tiny-preview) and threads
    (cos, sin) to the attention layers.
  - Applies the previously missing Granite multipliers:
    embedding_multiplier (after embed), attention_multiplier (as the
    attention scale), and logits_scaling (final logits).

- Golden generation (generate_golden.py / torch_reference.py): all
  full_attention hybrid models trip transformers' recurrent-mask
  builder ("has_previous_state can only be called on LinearAttention
  layers"). Both the single-pass forward and greedy generate now fall
  back to use_cache=False on that specific error; greedy/logits output
  is cache-independent.

- Tests: new testdata/cases/causal-lm/granite-4_0-1b.yaml (L4+L5) with
  generated golden. L4 prefill argmax and L5 20-token generation both
  match the HuggingFace reference on CUDA.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 4ae819fd64b960

Model Sub-model Changes Status

No architecture changes detected.


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 4ae819fd64b960

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 60 60 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 68 68 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 54 54 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 62 62 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 98 98 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 60 60 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 56 56 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 62 62 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 413 413 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 166 166 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 128 128 +0.0%

No performance regressions.

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.

🟡 Not ready to approve

granitemoehybrid.py now passes position_embeddings=None for NoPE variants but keeps non-optional tuple type annotations in layer forward() signatures, which is internally inconsistent and should be corrected before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds mobius support for the dense GraniteMoeHybrid variant ibm-granite/granite-4.0-1b (all full_attention, RoPE, num_local_experts=0) and introduces L4/L5 golden coverage to validate CUDA parity against HuggingFace.

Changes:

  • Fix GraniteMoeHybridConfig.layer_types reconstruction to correctly interpret Granite’s full_attention / linear_attention naming.
  • Update GraniteMoeHybrid model implementation: make MoE optional (shared MLP-only when num_local_experts == 0), initialize/apply RoPE when configured, and apply Granite scaling multipliers (embedding/attention/logits).
  • Make golden generation / HF reference execution resilient to transformers’ hybrid-cache mask error by retrying with use_cache=False on the specific has_previous_state failure.
File summaries
File Description
testdata/golden/causal-lm/granite-4_0-1b.json Adds L4 (prefill logits/top-k) golden reference for granite-4.0-1b.
testdata/golden/causal-lm/granite-4_0-1b_generation.json Adds L5 (20-token greedy generation) golden reference for granite-4.0-1b.
testdata/cases/causal-lm/granite-4_0-1b.yaml New test case enabling L4+L5 coverage for the new checkpoint.
src/mobius/models/granitemoehybrid.py Implements dense/no-experts path, RoPE wiring, and Granite multipliers in the model.
src/mobius/_testing/torch_reference.py Retries HF forward without cache on the has_previous_state hybrid-cache error.
src/mobius/_configs/_base.py Rebuilds layer_types using Granite’s naming to avoid misclassification.
scripts/generate_golden.py Retries HF generate() with use_cache=False on the same transformers hybrid-cache error.
Review details

Comments suppressed due to low confidence (1)

src/mobius/models/granitemoehybrid.py:323

  • This forward method accepts/passes through position_embeddings=None for NoPE variants, but the type annotation is tuple (non-optional). Update the annotation to tuple | None to match Attention.forward and avoid MyPy errors at the call site.
    def forward(
        self,
        op: OpBuilder,
        hidden_states: ir.Value,
        attention_bias: ir.Value,
        position_embeddings: tuple,
        past_key_value: tuple | None,
    ):
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread src/mobius/models/granitemoehybrid.py
Address review feedback on GraniteMoeHybrid config conversion:

- NoPE regression fix: granite-4.0-tiny-preview declares
  position_embedding_type='nope' but still ships a default
  rope_parameters dict, so driving RoPE purely off initialize_rope()
  would have wrongly enabled RoPE for the NoPE variant. Now clear
  rope_type (=> initialize_rope returns None => NoPE) whenever
  position_embedding_type != 'rope'. granite-4.0-1b (='rope') is
  unaffected.

- layer_types mapping is now explicit: {full_attention, attention} ->
  full_attention, {linear_attention, mamba, mamba2} -> mamba2, and any
  unknown value raises instead of silently defaulting to mamba2.

- Widen decoder-layer forward() position_embeddings annotation to
  `tuple | None` (matches Attention.forward and the NoPE None path).

L4/L5 granite-4.0-1b golden tests still pass on CUDA; granite/bamba
unit builds unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 30, 2026 19:03
@justinchuby

Copy link
Copy Markdown
Member Author

Addressed the review feedback in d64b960:

  • NoPE regression (blocking, found via rubber-duck): granite-4.0-tiny-preview declares position_embedding_type='nope' yet still ships a default rope_parameters dict, so the initial approach of driving RoPE off initialize_rope() alone would have wrongly enabled RoPE for the NoPE variant. Config now clears rope_type (→ NoPE) unless position_embedding_type == 'rope'.
  • layer_types mapping is now explicit ({full_attention, attention}→full_attention, {linear_attention, mamba, mamba2}→mamba2) and raises on unknown values.
  • Widened position_embeddings annotation to tuple | None.

L4/L5 granite-4.0-1b golden tests still pass on CUDA.

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.

🟢 Ready to approve

The changes are cohesive (config + model + golden/reference infra) and are validated by new L4/L5 goldens that exercise the newly supported dense GraniteMoeHybrid behavior.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@justinchuby
justinchuby merged commit 9973ab6 into main Jul 30, 2026
24 checks passed
@justinchuby
justinchuby deleted the feat/granite-4.0-1b branch July 30, 2026 22:46
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