Skip to content

Fix SmolLM3 inverted no_rope_layers logic and add L3/L5 tests - #233

Merged
justinchuby merged 5 commits into
mainfrom
smollm3-fix-tests
May 4, 2026
Merged

Fix SmolLM3 inverted no_rope_layers logic and add L3/L5 tests#233
justinchuby merged 5 commits into
mainfrom
smollm3-fix-tests

Conversation

@justinchuby

@justinchuby justinchuby commented May 1, 2026

Copy link
Copy Markdown
Member

Summary

Fix inverted no_rope_layers interpretation in SmolLM3, fix BF16 type mismatch in RoPE, and add missing L3/L5 tests.

Bug Fix: Inverted no_rope_layers

HuggingFace convention: no_rope_layers[i] == 1 means USE RoPE (despite the misleading name). Mobius had it backwards, causing 27/36 layers to have wrong RoPE gating.

Bug Fix: BF16 RoPE Type Mismatch

The ONNX RotaryEmbedding op requires all tensor inputs (x, cos, sin) to share the same type T. The RoPE cos/sin cache is always computed in FP32 (numpy limitation), so BF16/FP16 models had a type constraint violation: BF16 query/key + FP32 cos/sin. This cascaded through Attention -> o_proj -> residual Add causing ORT type errors.

Fix: Added a dtype parameter to BaseRope that all RoPE subclasses thread from config.dtype. BaseRope._cast_embeddings() inserts Cast ops after gathering cos/sin when the model dtype differs from FP32. For FP32 models, no Cast is added (zero overhead). Covers all RoPE variants.

Also fixes a latent bug in YarnRope where attn_scale dtype was derived from cos_cache.dtype (always FP32), fixing Ministral3/Mistral4 BF16 support.

Results

  • Prefill max diff: 6.87 -> 0.00005 (143,000x improvement)
  • L5 generation: now matches HF exactly (100% token match)
  • BF16 model: zero Add type mismatches (verified with real SmolLM3-3B weights)

Tests

  • L3: Added integration test entry for smollm3-3b
  • L5: Removed xfail, generation now passes
  • L1: Added no_rope_layers to tiny config for direct gating coverage
  • All 1185 L1 tests + 1463 unit tests pass

justinchuby and others added 2 commits May 1, 2026 21:23
SmolLM3's no_rope_layers config was interpreted backwards:
the HF convention is no_rope_layers[i] == 1 means USE RoPE
(despite the misleading name), but mobius was treating it as
skip RoPE. This caused 27/36 layers to have wrong RoPE gating,
resulting in max logit diffs of ~6.9 vs HF (vs 0.00005 after fix).

Changes:
- Fix inverted RoPE gating in SmolLM3TextModel.forward()
- Remove L5 xfail now that generation matches HF exactly
- Add L3 integration test entry for SmolLM3-3B
- Mark smollm3 as representative in L1 test configs
- Regenerate L4 golden data with updated HF reference

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Exercises per-layer RoPE gating in the unit test so the inverted
polarity bug is directly covered at L1 level (layer 0 = use RoPE,
layer 1 = skip RoPE).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 4e70d8d1565b07

Model Sub-model Changes Status
bert (feature-extraction) model 0
falcon model 0
gemma2 model 0
gemma4 (gemma4) decoder 0
gemma4 (gemma4) embedding 0
gemma4 (gemma4) vision_encoder 0
gemma4_text model 0
gpt2 model 0
llama model 0
llama (static-cache) model 0
mamba (ssm-text-generation) model 0
phi3 model 0
phi3 (static-cache) model 0
qwen model 0
qwen (static-cache) model 0
qwen2 model 0
qwen2 (static-cache) model 0
qwen2_moe model 0
qwen2_moe (static-cache) model 0
qwen3 model 0
qwen3 (static-cache) model 0
qwen3_5_moe (hybrid-text-generation) model 0
qwen3_5_text (hybrid-text-generation) model 0
qwen3_5_vl (hybrid-qwen-vl) decoder 0
qwen3_5_vl (hybrid-qwen-vl) embedding 0
qwen3_5_vl (hybrid-qwen-vl) vision_encoder 0
qwen3_moe model 0
qwen3_moe (static-cache) model 0
qwen3_next (hybrid-text-generation) model 0
t5 (seq2seq) decoder 0
t5 (seq2seq) encoder 0
whisper (speech-to-text) decoder 0
whisper (speech-to-text) encoder 0

No architecture changes detected.


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

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 4e70d8d1565b07

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 66 66 +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 53 53 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 61 61 +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 59 59 +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 61 61 +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 408 408 +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.

Pull request overview

Fixes SmolLM3’s per-layer RoPE gating to match HuggingFace’s no_rope_layers convention (where 1 means use RoPE), and updates test coverage/goldens accordingly.

Changes:

  • Correct SmolLM3 RoPE gating logic (no_rope_layers[i] == 1 now enables RoPE).
  • Expand test coverage by adding SmolLM3-3B to integration coverage and un-xfailing L5 golden generation.
  • Add no_rope_layers to the tiny SmolLM3 config and update golden outputs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/mobius/models/smollm.py Fixes SmolLM3 per-layer RoPE enable/disable logic and clarifies naming/comments.
tests/_test_configs.py Adds no_rope_layers to tiny SmolLM3 config to exercise gating in fast graph tests.
tests/integration_test.py Adds SmolLM3-3B to the integration model catalogue.
tests/e2e_golden_test.py Removes SmolLM3-3B from L5-only expected-fail list so generation is asserted.
testdata/golden/causal-lm/smollm3-3b.json Updates L4 golden logits to reflect corrected RoPE behavior.

@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.65517% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/mobius/components/_rotary_embedding.py 88.88% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

justinchuby and others added 2 commits May 1, 2026 21:49
The ONNX RotaryEmbedding op requires all tensor inputs (x, cos, sin)
to share the same type T. The RoPE cos/sin cache is computed in FP32
(numpy limitation), so when the model uses BF16 or FP16, the
RotaryEmbedding op received mixed types: BF16 query/key + FP32
cos/sin. This cascaded through Attention -> o_proj -> residual Add,
causing ORT to throw:
  Type Error: Type parameter (T) of Optype (Add) bound to different
  types (tensor(bfloat16) and tensor(float))

Fix: Add a dtype parameter to BaseRope that all RoPE subclasses
thread from config.dtype. BaseRope._cast_embeddings() inserts Cast
ops after gathering cos/sin when the model dtype differs from FP32.
For FP32 models, no Cast is added (zero overhead). Covers all RoPE
variants: DefaultRope, ProportionalRope, LinearRope, DynamicNTKRope,
Llama3Rope, LongRope, YarnRope, and MRope (chunked/interleaved).

Also fixes YarnRope's attn_scale dtype derivation to use the stored
model dtype instead of inspecting cos_cache.dtype (which stays FP32).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
@justinchuby
justinchuby requested a review from titaiwangms May 2, 2026 00:04
@justinchuby
justinchuby merged commit cefc55e into main May 4, 2026
20 of 23 checks passed
@justinchuby
justinchuby deleted the smollm3-fix-tests branch May 4, 2026 15:49
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.

2 participants