Fix YaRN RoPE bugs in model builder and add parity tests - #2076
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes YaRN RoPE configuration resolution in the Python ModelBuilder to prevent incorrect rotary cos/sin cache generation (which can produce unusable outputs for YaRN-based models).
Changes:
- Fixes
original_max_position_embeddingsdetection whenrope_scalingis stored as a dict-like object. - Adds
rope_thetafallback to read fromrope_scalingwhen not present as a top-level config field. - Respects explicit YaRN
mscalefrom config and corrects NTK rescaling math to avoid double-inversion.
dbb6147 to
d786f35
Compare
d786f35 to
216066d
Compare
216066d to
0b8c74a
Compare
0b8c74a to
81b1c13
Compare
81b1c13 to
f24ee36
Compare
Ti-Tai Wang (titaiwangms)
left a comment
There was a problem hiding this comment.
Reverted all cosmetic spacing changes in base.py. Only functional YaRN fixes remain — diff is now 36 changed lines (29+, 7-) vs ~487 before. Thanks for the catch!
Squashed into single clean commit: f24ee36
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Review Summary
All four YaRN RoPE bug fixes are correct and well-tested. The fixes are minimal, targeted, and produce cos/sin caches matching the HuggingFace transformers reference.
Positives:
- Using
collections.abc.Mappinginstead ofisinstance(_, dict)correctly handlesMappingProxyType, ordered dicts, and other Mapping implementations. - The
rope_thetafallback chain covers all known config layouts. - Bug 4 (inv_freq double-inversion) fix is mathematically clean:
inv_freq / factorcorrectly gives1/(factor * pos_freqs)matching HF. - Test suite is comprehensive: 11 tests with HF reference parity, guard assertions preventing false passes, and real builder code exercised via
make_rope_init.
One suggestion on make_mscale generalization (see inline comment). Two nitpicks on test file (below).
Test nitpicks (non-blocking):
_make_builder_cos_sinusesobject.__new__(Model)and manually initializes attributes. Ifmake_rope_initstarts depending on new attributes from__init__, tests will fail with opaqueAttributeErrors. Consider adding a comment listing assumed attributes.YARN_NO_MSCALE_CONFIGdocstring says "Modeled after DeepSeek-V2 style YaRN scaling" but DeepSeek-V2 useslongrope/su-type RoPE, notyarn. The inline comment ("Synthetic YaRN config") is accurate — consider aligning the docstring.
Ti-Tai Wang (titaiwangms)
left a comment
There was a problem hiding this comment.
Thanks for the review feedback!
Tianlei Wu (@tianleiwu) — Good suggestion on the mscale_all_dim formula. We went ahead and implemented it in this PR since it was straightforward: make_mscale() now accepts config_mscale_all_dim and uses the full HF formula (get_mscale(factor, mscale) / get_mscale(factor, mscale_all_dim)) when both are provided. Added a test covering equal, different, and fallback cases. Backward compatible — existing callers default to config_mscale_all_dim=0.
kunal-vaishnavi — Confirmed: all cosmetic spacing changes have been reverted. Only functional YaRN RoPE fixes remain (36→40 changed lines in base.py). The make_mscale consolidation you suggested is also in place with config_mscale parameter.
f24ee36 to
71a0f29
Compare
|
I constantly found CI failures that are not from this PR but they are required. How did people merge their PR in genai? Or I miss something. |
Fix four bugs in the YaRN RoPE implementation in base.py: 1. Use isinstance(Mapping) instead of hasattr() for dict-based rope_scaling — hasattr always returns False for dict keys 2. Resolve rope_theta from rope_scaling when top-level attr is absent 3. Add config_mscale parameter to make_mscale() so explicit mscale values (e.g. Ministral-3-3B's mscale=1.0) override computed values 4. Fix inv_freq double-inversion: use inv_freq/factor instead of 1/(factor*inv_freq) Add 11 parity tests comparing builder cos/sin caches against HuggingFace reference implementation for two YaRN configurations (Ministral-3-3B and a DeepSeek-V2 style config). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
71a0f29 to
a959221
Compare
Add Mistral3TextModel builder for mistralai/Ministral-3-3B-Instruct-2512: - VLM text decoder with FP8 dequantization (weight * scale_inv) - Patches genai_config.json with image_token_id from HF config - Excludes embedding layer (handled by separate embedding model) - Registers Mistral3ForConditionalGeneration in builder.py dispatch Also includes from PR #2076 (base branch): - Fix 4 YaRN RoPE bugs (hasattr, theta fallback, mscale, inv_freq) - isinstance(Mapping) for robust rope_scaling checks - Consolidated make_mscale() with config_mscale parameter - 12 YaRN RoPE parity tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b2c21e0 to
a959221
Compare
Add Mistral3TextModel builder for mistralai/Ministral-3-3B-Instruct-2512: - VLM text decoder with FP8 dequantization (weight * scale_inv) - Patches genai_config.json with image_token_id from HF config - Excludes embedding layer (handled by separate embedding model) - Registers Mistral3ForConditionalGeneration in builder.py dispatch Also includes from PR #2076 (base branch): - Fix 4 YaRN RoPE bugs (hasattr, theta fallback, mscale, inv_freq) - isinstance(Mapping) for robust rope_scaling checks - Consolidated make_mscale() with config_mscale parameter - 12 YaRN RoPE parity tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add src/python/py/models/** to exclude_patterns for both RUFF and RUFF-FORMAT linters in .lintrunner.toml. This prevents the linter from introducing formatting-only changes to model builder files, keeping PR diffs focused on functional changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert lambda assignment to a proper function definition to satisfy ruff E731. Add required blank lines around nested def per ruff-format. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Mistral3TextModel builder for mistralai/Ministral-3-3B-Instruct-2512: - VLM text decoder with FP8 dequantization (weight * scale_inv) - Patches genai_config.json with image_token_id from HF config - Excludes embedding layer (handled by separate embedding model) - Registers Mistral3ForConditionalGeneration in builder.py dispatch Also includes from PR #2076 (base branch): - Fix 4 YaRN RoPE bugs (hasattr, theta fallback, mscale, inv_freq) - isinstance(Mapping) for robust rope_scaling checks - Consolidated make_mscale() with config_mscale parameter - 12 YaRN RoPE parity tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary Adds Mistral3/Pixtral VLM support to onnxruntime-genai with multi-image inference. Includes C++ image processor, PixtralVisionState for per-image vision processing, Python export support, and comprehensive tests. ## Changes ### C++ Runtime - **Mistral3 image processor** — `[IMG]`/`[IMG_BREAK]`/`[IMG_END]` token expansion based on image resolution and patch geometry, multi-image support - **PixtralVisionState** — per-image vision processing loop with bounds checks and overflow guard; slices from padded batch tensor using `image_sizes` metadata from ort-extensions `PixtralImageSizes` op - **Virtual `SetExtraInputs`** — proper polymorphic dispatch for vision state subclasses - **`IsPixtralFamily()` model type detection** — enables Pixtral-specific codepath - **`processor_config.json`** — with `PixtralImageSizes` preprocessing step - **`context_length` / `max_length` separation** — `context_length` controls KV cache allocation while `max_length` controls generation stopping, preventing premature EOS with large image token counts - **INT32 `input_ids`** — token IDs above 32767 (Pixtral `[IMG]`=128011) require int32 ### Python Export Support - Mistral3 model classes (`Mistral3Config`, `Mistral3ForConditionalGeneration`) - FP8 dtype promotion for checkpoint loading - `get_user_content()` handler for Mistral3 prompt formatting ## Multi-Image Architecture Pixtral uses dynamic image sizes (28×28 to 1540×1540) so images can't be batched in the vision encoder. `PixtralVisionState` processes each image individually by: 1. Reading `image_sizes` tensor from ort-extensions `PixtralImageSizes` op (provides per-image H×W) 2. Slicing the padded `[N, C, max_H, max_W]` batch tensor to extract each image's actual pixels 3. Running vision encoder on each image separately 4. Concatenating vision embeddings for the decoder ## Dependencies - **onnxruntime-extensions PR #1050** — `PixtralImageSizes` custom op for image size metadata - **PR #2076** — YaRN RoPE parity fixes (merged) ✅ ## Testing - 5 multi-image token expansion tests (various image sizes and counts) - Virtual dispatch verification tests for `SetExtraInputs` - YaRN RoPE parity tests (from merged #2076) - E2E verified: multi-image (fish.jpg + challenge.jpg) correctly describes both images --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Fixes four bugs in the ModelBuilder's YaRN RoPE configuration resolution that caused completely wrong cos/sin caches, producing garbage output for ALL YaRN-based models (Ministral-3-3B, GPT-OSS-20B, and any model using
beta_fast/beta_slowinrope_scaling).Also adds comprehensive parity tests for 3 YaRN configurations, consolidates mscale logic, and excludes
src/python/py/models/from lintrunner (legacy formatting conflicts).Bugs Fixed
Bug 1:
hasattron dict fororiginal_max_position_embeddingshasattr(config.rope_scaling, 'original_max_position_embeddings')always returnsFalsefor dicts. Fixed to useisinstance(Mapping)+inoperator.Bug 2:
rope_thetafallbackModels that store
rope_thetaonly inrope_scalingdict (not as a top-level config attribute) fell through to defaulttheta=10000. Added fallback chain:config.rope_theta→config.rope_embedding_base→config.rope_scaling["rope_theta"]→10000.Bug 3: YaRN
mscaleoverrideAlways computed mscale from factor via
make_mscale(), ignoring explicitmscale=1.0in config. Ministral-3-3B setsmscale=1.0but was gettingmscale≈1.277. Now respects the config value when> 0.Bug 4:
inv_freqdouble-inversionmake_inv_freq_rescaled_with_ntkcomputed1/(factor * inv_freq)which double-inverts sinceinv_freqis already1/pos_freqs, givingpos_freqs/factor(wrong). Fixed toinv_freq / factor.Review Feedback Addressed
isinstance(rope_scaling, dict)→isinstance(rope_scaling, Mapping)in 2 locationsmake_mscale(mscale, config_mscale=0)— config override is now handled centrally with backward-compatible defaultmake_mscaledefault parameter values (callers pass explicitly)mscale_all_dimdocumentation commentmake_rope_init) instead of reimplementing logic inlineTests Added
Added
test/python/test_yarn_rope_parity.pywith 16 tests covering 3 YaRN configurations:Ministral-3-3B (factor=16, theta=1M, explicit mscale=1.0):
test_ministral_3b_cos_sin_match— end-to-end parity with HF referencetest_bug_a_hasattr_on_dict— dict key access + wrong fallback guardtest_bug_b_rope_theta_fallback— theta from rope_scaling + default guardtest_bug_c_mscale_override— explicit mscale viamake_rope_init()test_mscale_fallback_when_absent—.get("mscale", 0)fallbacktest_mscale_all_dim_formula— mscale_all_dim computationtest_bug_d_inv_freq_no_double_inversion— NTK scaling vs HF referencetest_full_cache_length— parity at 2048 positionstest_mapping_isinstance_with_frozen_dict— MappingProxyType supportSynthetic YaRN (factor=40, theta=10K, no explicit mscale):
10.
test_yarn_no_mscale_cos_sin_match— parity with HF reference11.
test_yarn_no_mscale_uses_computed_value— computed mscale from factor12.
test_different_yarn_configs_produce_different_caches— configs produce distinct outputsGPT-OSS-20B (factor=4, theta=500K, top-level rope_theta, no explicit mscale):
13.
test_gptoss_20b_cos_sin_match— end-to-end parity with HF reference14.
test_gptoss_20b_top_level_rope_theta— verifies top-level theta is used15.
test_gptoss_20b_computed_mscale— computed mscale from factor=416.
test_gptoss_20b_full_cache_length— parity at 2048 positionsImpact
beta_fast/beta_slowinrope_scaling)atol=1e-5)Files Changed
.lintrunner.toml— excludesrc/python/py/models/**from lintrunner (legacy formatting)src/python/py/models/builders/base.py— 4 bug fixes +Mappingimport + mscale consolidationtest/python/test_yarn_rope_parity.py— 16 parity tests for 3 YaRN configs (new file)