#902·mlx-audio

STT load_model silently returns a randomly initialized model when checkpoint keys don't match (strict=False default)

Author: happyartsCreated Aug 21, 2026Updated Aug 25, 2026

Summary

When a checkpoint's weight keys do not match the model's module tree, the STT load_model path returns a randomly initialized model with no error and no warning. Inference then runs and produces fluent-looking garbage, so the failure surfaces far from its cause — if it surfaces at all.

This is not hypothetical: every Voxtral checkpoint quantized with the other MLX Voxtral port (mzbac/mlx.voxtral) has this key layout, and several are published on the Hub.

Reproduction (mlx-audio 0.5.0, mlx 0.32.1, macOS)

python
import mlx.nn as nn
from mlx_audio.stt.utils import load_model

m = load_model("MarkusKaemmerer/Voxtral-Mini-3B-2507-8bit-dense-encoder")
# completes silently -- nothing on stdout or stderr (verified with redirect)

q = sum(1 for _, mod in m.named_modules()
        if isinstance(mod, (nn.QuantizedLinear, nn.QuantizedEmbedding)))
print(q)   # 0  -- for a checkpoint that is 8-bit quantized on disk

The language model's parameters are untouched initializer output — e.g. language_model.model.layers.0.self_attn.q_proj.weight comes back float32 with mean ≈ 0, std ≈ 0.0104, while the file carries U32 packed weights plus scales/biases for that layer. 405 dense Linear modules, 211 of them in the language model.

That the loader can see the problem is one flag away:

python
load_model("MarkusKaemmerer/Voxtral-Mini-3B-2507-8bit-dense-encoder", strict=True)
# ValueError: Received 700 parameters not in model: embed_tokens.biases, ...

Mechanism

The checkpoint names the language model language_model.layers.0…; mlx-audio's Voxtral module tree is language_model.model.layers.0… (its LanguageModel wraps LlamaModel as self.model). From there, three silent steps compound:

  1. Voxtral's sanitize() only transposes conv weights; it does not remap the prefix.
  2. apply_quantization's get_class_predicate checks p in quantization and falls back to f"{p}.scales" in weights — both keyed on module paths, which don't match the file's paths. So no layer is converted to quantized.
  3. base_load_model calls model.load_weights(..., strict=strict) with the default strict=False, which skips every mismatched key without comment.

What I am not proposing

Flipping the strict default. Several models legitimately rely on lenient loading — e.g. whisper's sanitize() remaps and drops HF-format keys — and a hard default change would break working paths.

Proposal

After load_weights, check which of the model's parameters actually received a value, and warn when parameters were left at initialization (count plus a few example keys; possibly only above a small threshold to tolerate benign buffers). That distinguishes the benign case (extra keys in the file, ignored) from the catastrophic one (model parameters unfilled), costs one set comparison at load time, and would have turned this from a debugging session into a one-line log message.

Judging by #538 and #714, key-mismatch situations do occur in the wild, and the silent variant is the expensive one.

Aside

If there is interest, I'd be happy to send a small PR adding the language_model.language_model.model. remap to Voxtral's sanitize(), which would make the mlx-voxtral-layout checkpoints on the Hub actually load — that port is unmaintained (no release since 2025-08) and its users are natural migrants to mlx-audio. Related: #900 / #901.