Skip to content

Commit 419467e

Browse files
justinchubyCopilot
andauthored
Fix L2 failures: arctic RoPE and Phi4-MM rope standardization (#230)
## Summary Fixes 8 L2 test failures (arctic ×2, phi4_multimodal ×3, phi4mm ×3). ### Arctic (TypeError: rotary_emb is None) Arctic's config JSON has `rope_theta=10000` (the default) and `rope_scaling=null`. No signal triggers `_extract_rope_config()`, so `rotary_emb` is never created — but the model uses RoPE. **Fix:** Add `arctic` to `_IMPLICIT_ROPE_DEFAULTS` with `rope_theta=10000.0`. ### Phi4-MM (AttributeError: max_position_embeddings) Newer transformers (`convert_rope_params_to_dict` → `standardize_rope_params`) accesses `self.max_position_embeddings` before it's set as an attribute on `PretrainedConfig`. This crashes even when `max_position_embeddings` is in the kwargs dict. **Fix:** Two-layer defense in `_dict_to_pretrained_config`: 1. Proactively strip rope fields from **composite** configs (those with nested text_config/thinker_config dicts) — the nested config carries its own rope_scaling 2. For **flat** configs that still crash, catch the exception, strip rope fields, reconstruct, then restore rope fields as attributes so `_extract_rope_config` can still read them --------- Signed-off-by: Justin Chu <justinchu@microsoft.com> Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4de6bb7 commit 419467e

4 files changed

Lines changed: 113 additions & 18 deletions

File tree

src/mobius/_config_resolver.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,23 +115,14 @@ def _dict_to_pretrained_config(d: dict):
115115
"""
116116
import transformers
117117

118-
# Some composite configs (e.g. Phi4-MM) have rope_scaling at the
119-
# top level without max_position_embeddings, which crashes
120-
# PretrainedConfig.__post_init__ rope standardization. Strip rope
121-
# fields proactively when the guard field is absent — the nested
122-
# text_config will carry its own rope_scaling.
123-
has_rope_fields = "rope_scaling" in d or "rope_parameters" in d
124-
has_max_pos = "max_position_embeddings" in d
125-
if has_rope_fields and not has_max_pos:
126-
logger.debug(
127-
"Stripping top-level rope fields from %s config: "
128-
"rope_scaling present without max_position_embeddings",
129-
d.get("model_type", "unknown"),
130-
)
131-
d = {k: v for k, v in d.items() if k not in ("rope_scaling", "rope_parameters")}
132-
config = transformers.PretrainedConfig(**d)
133-
# Recursively convert known nested config keys
134-
nested_keys = (
118+
# Composite configs (e.g. configs with text_config/thinker_config) may
119+
# duplicate rope_scaling at the top level. PretrainedConfig's rope
120+
# standardization (__post_init__ → standardize_rope_params) can crash
121+
# with AttributeError when self.max_position_embeddings is not yet set.
122+
# Strip top-level rope fields ONLY for composite configs — the nested
123+
# text_config will carry its own rope_scaling with correct context.
124+
# Non-composite (flat) configs must keep rope fields intact.
125+
nested_config_keys = (
135126
"thinker_config",
136127
"talker_config",
137128
"text_config",
@@ -141,7 +132,36 @@ def _dict_to_pretrained_config(d: dict):
141132
"code_predictor_config",
142133
"speaker_encoder_config",
143134
)
144-
for key in nested_keys:
135+
is_composite = any(isinstance(d.get(k), dict) for k in nested_config_keys)
136+
rope_keys = ("rope_scaling", "rope_parameters")
137+
if is_composite and any(k in d for k in rope_keys):
138+
logger.debug(
139+
"Stripping top-level rope fields from composite %s config",
140+
d.get("model_type", "unknown"),
141+
)
142+
d = {k: v for k, v in d.items() if k not in rope_keys}
143+
144+
try:
145+
config = transformers.PretrainedConfig(**d)
146+
except (AttributeError, KeyError, TypeError) as e:
147+
# Newer transformers may crash during rope standardization
148+
# (e.g. Phi4-MM longrope format where PretrainedConfig doesn't
149+
# set max_position_embeddings before accessing it). Strip rope
150+
# fields, construct the config, then restore them as attributes
151+
# so _extract_rope_config can still read them.
152+
logger.warning(
153+
"Retrying %s config without rope fields after PretrainedConfig init failure: %s",
154+
d.get("model_type", "unknown"),
155+
e,
156+
)
157+
saved_rope = {k: d[k] for k in rope_keys if k in d}
158+
d_clean = {k: v for k, v in d.items() if k not in rope_keys}
159+
config = transformers.PretrainedConfig(**d_clean)
160+
for k, v in saved_rope.items():
161+
setattr(config, k, v)
162+
163+
# Recursively convert known nested config keys
164+
for key in nested_config_keys:
145165
val = getattr(config, key, None)
146166
if isinstance(val, dict):
147167
setattr(config, key, _dict_to_pretrained_config(val))

src/mobius/_config_resolver_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,3 +475,49 @@ def test_thinker_config_nested(self):
475475
config = _dict_to_pretrained_config(d)
476476
assert config.thinker_config.model_type == "qwen3"
477477
assert config.thinker_config.text_config.model_type == "qwen3"
478+
479+
def test_composite_config_strips_rope_fields(self):
480+
"""Composite configs strip top-level rope_scaling/rope_parameters."""
481+
d = {
482+
"model_type": "composite",
483+
"rope_scaling": {"type": "longrope"},
484+
"rope_parameters": {"rope_type": "default"},
485+
"text_config": {"model_type": "inner", "hidden_size": 256},
486+
}
487+
config = _dict_to_pretrained_config(d)
488+
# Rope fields stripped from top level
489+
assert not hasattr(config, "rope_scaling") or config.rope_scaling is None
490+
# Nested config still works
491+
assert config.text_config.model_type == "inner"
492+
493+
def test_flat_config_keeps_rope_fields(self):
494+
"""Non-composite (flat) configs keep rope_scaling as attribute."""
495+
d = {
496+
"model_type": "flat",
497+
"hidden_size": 256,
498+
"max_position_embeddings": 4096,
499+
# Simple rope_scaling that won't crash PretrainedConfig
500+
"rope_theta": 10000.0,
501+
}
502+
config = _dict_to_pretrained_config(d)
503+
assert config.rope_theta == pytest.approx(10000.0)
504+
505+
def test_rope_retry_restores_fields(self):
506+
"""When PretrainedConfig init crashes on rope, fields are restored."""
507+
# Simulate a config with rope_scaling that crashes standardization
508+
# by including rope_scaling without the fields needed for
509+
# standardization (matching Phi4-MM's failure pattern).
510+
d = {
511+
"model_type": "phi4mm",
512+
"rope_scaling": {"type": "longrope", "long_factor": [1.0]},
513+
"rope_theta": 10000.0,
514+
"hidden_size": 256,
515+
"max_position_embeddings": 4096,
516+
}
517+
# This should succeed (either directly or via retry)
518+
config = _dict_to_pretrained_config(d)
519+
assert config.model_type == "phi4mm"
520+
# If the retry path ran, rope_scaling should be restored as attr
521+
rope_scaling = getattr(config, "rope_scaling", None)
522+
if rope_scaling is not None:
523+
assert rope_scaling["type"] == "longrope"

src/mobius/_configs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ def _first_not_none(*values, default=None):
323323
# TODO: migrate to a registry annotation (uses_rope: bool, rope_theta: float)
324324
# once the registry schema supports per-model capability flags.
325325
_IMPLICIT_ROPE_DEFAULTS: dict[str, float] = {
326+
# arctic: config JSON has rope_theta=10000 (default) and rope_scaling=null;
327+
# no signal triggers _extract_rope_config, but the model uses RoPE.
328+
"arctic": 10_000.0,
326329
# chatglm: config JSON has no rope_theta/rope_scaling/rotary_* attrs;
327330
# uses default rope_theta=10000.0 hardcoded in modeling code.
328331
"chatglm": 10_000.0,

src/mobius/_configs_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,3 +1050,29 @@ class FakeConfig:
10501050
# With gelu_activation=False and no other activation attr,
10511051
# hidden_act should be None (not "gelu")
10521052
assert config.hidden_act is None
1053+
1054+
1055+
class TestImplicitRopeDefaults:
1056+
"""Tests for models in _IMPLICIT_ROPE_DEFAULTS."""
1057+
1058+
def test_arctic_gets_rope_config(self):
1059+
"""Arctic (rope_theta=10000, rope_scaling=null) should get RoPE."""
1060+
1061+
class FakeConfig:
1062+
model_type = "arctic"
1063+
num_attention_heads = 8
1064+
num_key_value_heads = 8
1065+
num_hidden_layers = 2
1066+
vocab_size = 1000
1067+
hidden_size = 256
1068+
intermediate_size = 512
1069+
max_position_embeddings = 4096
1070+
head_dim = 32
1071+
hidden_act = "silu"
1072+
# Arctic has rope_theta=10000 (default) and no rope_scaling
1073+
rope_theta = 10_000.0
1074+
1075+
config = ArchitectureConfig.from_transformers(FakeConfig())
1076+
# Arctic must get RoPE via _IMPLICIT_ROPE_DEFAULTS
1077+
assert config.rope_type == "default"
1078+
assert config.rope_theta == pytest.approx(10_000.0)

0 commit comments

Comments
 (0)