You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Support explicit head_dim on MistralBackbone (enables Magistral) (#2692)
* Support explicit head_dim on MistralBackbone (enables Magistral)
Mistral and its derivatives always had `head_dim = hidden_dim //
num_query_heads` hard-coded in `CachedMistralAttention.build`. That
assumption breaks for Magistral (mistralai/Magistral-Small-2506):
hidden_size=5120, num_attention_heads=32 -> 5120/32 = 160
head_dim in HF config -> 128
Add an optional `head_dim` argument on `MistralBackbone`,
`MistralTransformerDecoder`, and `CachedMistralAttention`. When unset,
behavior is unchanged. When provided, it overrides the hidden/heads
fallback so the Q/K/V and output einsum projections build with the
correct per-head size.
Also:
- Pass `head_dim` through the HuggingFace -> keras-hub converter and
fall back to `.get("sliding_window")` so the `null` setting on
Magistral no longer KeyErrors.
- Add a unit test on `MistralBackbone` that exercises the explicit
head_dim path with sliding_window=None.
- Add a numerical-parity test that builds a small HF Mistral with
`head_dim=12` (!= hidden/num_heads), converts it through the preset
loader, and asserts the keras-hub forward pass matches the HF
reference to within fp16 precision.
After this PR lands, users can load Magistral directly:
keras_hub.models.MistralBackbone.from_preset(
"hf://mistralai/Magistral-Small-2506"
)
Addresses the "[Contributions Welcome] Add Magistral" item in the
KerasHub roadmap (#1836) and issue #2314.
* Address review feedback on MistralBackbone head_dim
- Rename the constructor-argument attribute `_configured_head_dim` to
`_head_dim` on `CachedMistralAttention`, matching the style-guide rule
that layer attributes should share the name of the `__init__` argument
(keras-hub style guide line 542).
- Simplify `build()` to only compute `_head_dim` from `_hidden_dim`
when the caller did not supply one, instead of maintaining a second
attribute for the configured value.
- Update `get_config` to return the renamed attribute.
- Tweak the `MistralBackbone.head_dim` docstring: "not a multiple of"
was inaccurate — any value that differs from
`hidden_dim // num_query_heads` needs to be set explicitly, and `//`
matches the code path.
* Plumb head_dim through Mistral checkpoint conversion (adds Magistral preset)
Update tools/checkpoint_conversion/convert_mistral_checkpoints.py to:
* Add `magistral_small_2506_en` to PRESET_MAP.
* Extract `head_dim` once from the HF config (with fallback to
`hidden_size // num_attention_heads`) and use it in the Q/K/V/O
reshape calls instead of the hard-coded ratio. Standard Mistral
checkpoints take the fallback path and behavior is unchanged.
* Pass `head_dim` through to `MistralBackbone` via `backbone_kwargs`.
Also handle the transformers 5.x rope_parameters drift in both this
script and `keras_hub/src/utils/transformers/convert_mistral.py`:
`rope_theta` no longer lives at the top level of the HF config and is
nested inside `rope_parameters`. Without this fallback, both the
script-path conversion and `from_preset("hf://...")` raise on any
recent HF Mistral/Magistral checkpoint.
Numerics verified on synthetic standard and Magistral-shaped configs
(head_dim=12 != hidden//heads, sliding_window=None) within atol=1e-2,
matching the precision bound of the fp16 weight hops.
* Use ops.convert_to_numpy in test_explicit_head_dim_matches_hf
`np.asarray()` cannot directly convert a CUDA torch tensor returned by
the keras backbone under the PyTorch backend, breaking PyTorch-GPU CI:
TypeError: can't convert cuda:0 device type tensor to numpy.
Use Tensor.cpu() to copy the tensor to host memory first.
`keras.ops.convert_to_numpy()` is backend-agnostic and handles the
`.cpu().numpy()` hop internally for PyTorch.
* Cover explicit head_dim path with run_backbone_test
Route test_explicit_head_dim through run_backbone_test so the Magistral
style config (explicit head_dim, sliding_window=None) also gets
serialization and mixed precision coverage, then keep the head_dim
assertion on the built attention layer.
* Add magistral_small_2507_en to Mistral PRESET_MAP
* Support Tekken tokenizer conversion for Mistral
Magistral checkpoints ship a Tekken (byte-level BPE) tokenizer as
tekken.json instead of a SentencePiece tokenizer.model, so the previous
convert_tokenizer that hardcoded get_file(preset, 'tokenizer.model')
failed on them.
Add convert_tekken.py to reconstruct BPE merge rules from the Tekken
rank-ordered byte vocabulary and re-encode it with the GPT-2
byte-to-unicode mapping. MistralTokenizer now accepts vocabulary/merges
in addition to a proto: when built from Tekken data it delegates to an
internal BytePairTokenizer configured with the Tekken split pattern,
with a tf.py_function bridge for the tf.data graph path. Existing
SentencePiece presets are unchanged.
* Parse Tekken vocab with mistral_common
Use Mistral's own mistral_common library to parse tekken.json during
conversion instead of parsing the raw JSON by hand. This is the same
backend Hugging Face delegates to for Tekken, so the byte vocabulary,
special tokens, split pattern and vocab size come from a maintained
source rather than reimplemented parsing.
mistral_common is imported lazily inside convert_tokenizer and is only
needed at conversion time; the runtime tokenizer still uses
BytePairTokenizer and does not depend on it. Added as an optional dep.
* Fix Tekken tokenizer init order on torch backend
On the torch backend a keras.Layer is also a torch.nn.Module, which
rejects submodule assignment before its own __init__ runs. Initialize
the base tokenizer before assigning the internal BytePairTokenizer so
the Tekken path works on all backends.
* Make Tekken tokenizer a standalone MistralTekkenTokenizer class
Address review feedback:
- Add MistralTekkenTokenizer(BytePairTokenizer) as a public class instead
of turning MistralTokenizer into a dual-personality class. MistralTokenizer
is left untouched (SentencePiece only). convert_tokenizer returns a
MistralTekkenTokenizer directly when tekken.json is detected.
- Inline the Tekken parsing helpers into convert_mistral.py and remove the
separate convert_tekken.py, matching how convert_gemma4.py inlines its
tokenizer helpers.
- Drop mistral-common from requirements-common.txt; it is only needed at
conversion time and is already lazily imported with an ImportError guard.
* Use explicit head_dim when building the generation cache
MistralCausalLM._build_cache always derived head_dim as
hidden_dim // num_query_heads, ignoring an explicitly configured head_dim.
For Magistral (hidden_dim=5120, num_query_heads=32, head_dim=128) this built
the cache with head_dim=160 and generate() failed in dot_product_attention.
Fall back to the derived value only when head_dim is not set, matching
MistralAttention. Add a regression test with an explicit head_dim.
* Align Mistral conversion script with recent converter patterns
- Load the HF reference in float32 on CPU and precompute its logits,
then free it so only one full-precision model is in memory at a time
(qwen3_5 pattern). This also fixes the bfloat16 .numpy() crash.
- Load the KerasHub model through from_preset("hf://...") so the script
exercises the built-in transformers converter (which now handles
head_dim) instead of duplicating the weight porting logic.
- Verify parameter counts, token IDs, and logits against the HF
reference in float32 with a hard assert (atol=1e-3).
- Reload in bfloat16 and save the preset in bfloat16 (gemma4 pattern).
0 commit comments