- AI Usage Notice: We found this with Opus. Code is easily reproducible below.
microsoft/Phi-mini-MoE-instruct has "rope_scaling": null in its config.json. phimoe.py
reads four keys out of that field while building attention, so loading the checkpoint raises before
any weight is touched.
Versions: mlx-lm 0.31.3, mlx 0.32.2, Apple M3 Max.
Runnable repro
Part A needs no download and no checkpoint at all: ModelArgs.rope_scaling already defaults to
None, and the only code that reads it subscripts it four times, so the port contradicts its own
declared default. Part B is the checkpoint that made this visible.
pip install "mlx-lm>=0.31.3"
python repro_phimoe.py # Part A only -- instant, no download
python repro_phimoe.py --real # adds Part B (~7.6 GB)
"""Repro: mlx-lm's phimoe port cannot build a model whose `rope_scaling` is null.
Part A needs no download and no checkpoint -- `ModelArgs.rope_scaling` already defaults to
None, and the code that reads it cannot accept None, so the port contradicts its own default.
Part B is the real checkpoint (~7.6 GB) that made this visible.
pip install "mlx-lm>=0.31.3"
python repro_phimoe.py # Part A only
python repro_phimoe.py --real # adds Part B
"""
import sys
import traceback
from mlx_lm.models import phimoe
print("=" * 78)
print("A. mlx-lm's own default, no checkpoint and no download")
print("=" * 78)
args = phimoe.ModelArgs(
# Small so the failure is quick; the shape has nothing to do with the bug.
hidden_size=64,
intermediate_size=64,
num_hidden_layers=1,
num_attention_heads=4,
num_key_value_heads=4,
vocab_size=32,
num_local_experts=2,
num_experts_per_tok=1,
)
print(f"ModelArgs.rope_scaling = {args.rope_scaling!r} <- the dataclass's declared default")
try:
phimoe.Model(args)
except TypeError:
frames = traceback.extract_tb(sys.exc_info()[2])
last = frames[-1]
print(f"\n{type(sys.exc_info()[1]).__name__}: {sys.exc_info()[1]}")
print(f" raised at {last.filename.split('/')[-1]}:{last.lineno}")
print(f" line: {last.line}")
print("\nThe field defaults to None and the only code that reads it subscripts it four times.")
else:
raise SystemExit("no error -- this mlx-lm version has been fixed")
if "--real" not in sys.argv:
print("\n(pass --real to also run Part B against microsoft/Phi-mini-MoE-instruct)")
raise SystemExit(0)
print()
print("=" * 78)
print("B. the checkpoint this was found on: microsoft/Phi-mini-MoE-instruct")
print("=" * 78)
print('Its config.json carries "rope_scaling": null, so the same line is reached.')
from mlx_lm import load # noqa: E402
try:
load("microsoft/Phi-mini-MoE-instruct")
except TypeError:
frames = traceback.extract_tb(sys.exc_info()[2])
ours = [f for f in frames if "mlx_lm" in (f.filename or "")]
print(f"\n{type(sys.exc_info()[1]).__name__}: {sys.exc_info()[1]}")
print(f" raised at {ours[-1].filename.split('/')[-1]}:{ours[-1].lineno}")
print(f"\nmlx_lm frames in the traceback: {len(ours)} of {len(frames)}")
print("Nothing in the message names `rope_scaling`, the config, or the checkpoint, which is")
print("why this reads as a bug in whatever called load().")
else:
raise SystemExit("no error -- this mlx-lm version has been fixed")
Output on mlx-lm 0.31.3 / mlx 0.32.2, Apple M3 Max:
==============================================================================
A. mlx-lm's own default, no checkpoint and no download
==============================================================================
ModelArgs.rope_scaling = None <- the dataclass's declared default
TypeError: 'NoneType' object is not subscriptable
raised at phimoe.py:53
line: short_factor=args.rope_scaling["short_factor"],
The field defaults to None and the only code that reads it subscripts it four times.
==============================================================================
B. the checkpoint this was found on: microsoft/Phi-mini-MoE-instruct
==============================================================================
Its config.json carries "rope_scaling": null, so the same line is reached.
TypeError: 'NoneType' object is not subscriptable
raised at phimoe.py:53
mlx_lm frames in the traceback: 6 of 7
Cause
mlx_lm/models/phimoe.py, in Attention.__init__:
self.rope = SuScaledRoPE(
head_dim,
base=args.rope_theta,
max_position_embeddings=args.max_position_embeddings,
original_max_position_embeddings=args.original_max_position_embeddings,
short_factor=args.rope_scaling["short_factor"], # <- line 53
long_factor=args.rope_scaling["long_factor"],
short_mscale=args.rope_scaling["short_mscale"],
long_mscale=args.rope_scaling["long_mscale"],
)
rope_scaling is optional in the Phi-MoE config, and this is the only place it is read. A config
that omits it or sets it to null means plain RoPE.
The traceback is the other half of the problem: it contains only mlx-lm frames and a bare
TypeError with no field name, no config key and no checkpoint name, so it reads as a bug in
whatever called load(). It cost a release's worth of misattribution here before I read the port.
Suggested fix
Fall back to plain RoPE when the field is absent:
scaling = args.rope_scaling
if scaling is None:
self.rope = nn.RoPE(head_dim, traditional=False, base=args.rope_theta)
else:
self.rope = SuScaledRoPE(...)
A clearer failure would help even without the fallback — raising ValueError naming
rope_scaling and the checkpoint would have made this a one-minute diagnosis rather than an
afternoon.
I am happy to open a PR if that is useful.
microsoft/Phi-mini-MoE-instructhas"rope_scaling": nullin itsconfig.json.phimoe.pyreads four keys out of that field while building attention, so loading the checkpoint raises before
any weight is touched.
Versions: mlx-lm 0.31.3, mlx 0.32.2, Apple M3 Max.
Runnable repro
Part A needs no download and no checkpoint at all:
ModelArgs.rope_scalingalready defaults toNone, and the only code that reads it subscripts it four times, so the port contradicts its owndeclared default. Part B is the checkpoint that made this visible.
Output on mlx-lm 0.31.3 / mlx 0.32.2, Apple M3 Max:
Cause
mlx_lm/models/phimoe.py, inAttention.__init__:rope_scalingis optional in the Phi-MoE config, and this is the only place it is read. A configthat omits it or sets it to
nullmeans plain RoPE.The traceback is the other half of the problem: it contains only mlx-lm frames and a bare
TypeErrorwith no field name, no config key and no checkpoint name, so it reads as a bug inwhatever called
load(). It cost a release's worth of misattribution here before I read the port.Suggested fix
Fall back to plain RoPE when the field is absent:
A clearer failure would help even without the fallback — raising
ValueErrornamingrope_scalingand the checkpoint would have made this a one-minute diagnosis rather than anafternoon.
I am happy to open a PR if that is useful.