I ran into a gap in the MLXVLM → MLXLLM factory fallback and wanted to check whether this is expected behavior or something worth filing.
Context: I maintain a Swift-native macOS agent built on MLX Swift, with a hardware-adaptive catalog of local models. I tried adding mlx-community/Qwen3.5-9B-4bit as a plain text model. Its config.json ships a full vision_config block — it looks like a multimodal config — but the actual checkpoint has no vision tower weights at all. It's genuinely text-only; the metadata just doesn't reflect that.
ModelFactoryRegistry tries MLXVLM before falling back to MLXLLM. I expected that to fail cleanly and fall through, since there's no vision encoder to load. It doesn't. MLXVLM's Qwen35Configuration declares visionConfiguration as a non-optional property mapped to vision_config. Since the key is present in the raw config, decoding succeeds — there's nothing to catch at that stage. So MLXVLM proceeds to actually load the model as a VLM, and it crashes later, inside WiredMemoryUtils.tune(), because getRopeIndex() expects real vision-encoder dimensions to compute against, and there are none. Not a clean decode-time error I could catch — a runtime crash several layers past config parsing, in the RoPE indexing math.
The workaround I ended up with is config-level surgery before the model reaches the factory at all: strip vision_config (and image_token_id / video_token_id) from the JSON first. With the key genuinely absent, Qwen35Configuration's decode throws DecodingError.keyNotFound as the fallback logic presumably intends, MLXVLM fails cleanly at that stage, and ModelFactoryRegistry falls through to MLXLLM correctly.
That works, but it feels like a workaround rather than the intended path — the factory fallback seems to assume any failure will happen at decode time, and a config key that's present but semantically empty (no real vision weights behind it) skips that safety net entirely.
Questions:
- Is there a more canonical/official way to detect whether a checkpoint actually has real vision tower weights, short of opening
model.safetensors.index.jsonand countingvision_tower.*tensor names myself? (I've done exactly that for a different, genuinely multimodal Qwen VL checkpoint — 333 real vision tensors there vs. zero here — but it feels like something the loading path should be able to tell me.) - Is the
vision_config-present-but-empty pattern something the mlx-community conversion pipeline is aware of, or is this specific to how Qwen3.5-9B-4bit happened to get converted?
Happy to share more logs/repro details if useful.