Skip to content

Commit f5c6b9d

Browse files
authored
Merge branch 'main' into dependabot/pip/requirements/lintrunner/ruff-0.15.9
2 parents 3e70c8f + d9bd58d commit f5c6b9d

16 files changed

Lines changed: 1182 additions & 21 deletions

.github/skills/multimodal-models/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ input_ids ──────► Embedding ────────────
2929
| Component | File | Purpose |
3030
|-----------|------|---------|
3131
| `VisionModel` | `components/_vision.py` | SigLIP-style patch embedding + transformer encoder |
32+
| `PixtralVisionTower` | `components/_pixtral_vision.py` | Pixtral 2D RoPE vision encoder (bidirectional attention) |
3233
| `PatchEmbedding` | `components/_vision.py` | Conv2d → positional embedding |
3334
| `Gemma3MultiModalProjector` | `components/_multimodal.py` | AvgPool2d → RMSNorm → MatMul |
3435
| `MLPMultiModalProjector` | `components/_multimodal.py` | Linear → GELU → Linear |
36+
| `Mistral3MultiModalProjector` | `components/_pixtral_vision.py` | RMSNorm → spatial merge → Linear → GELU → Linear |
3537
| `LinearMultiModalProjector` | `components/_multimodal.py` | Single Linear |
3638
| `InputMixer` | `components/_multimodal.py` | Scatter vision embeddings at image-token positions |
3739
| `VisionLanguageTask` | `tasks/__init__.py` | ONNX I/O contract with `pixel_values` input |
@@ -44,7 +46,8 @@ embedding spaces. Choose the one that matches the HuggingFace implementation:
4446
| Projector | Architecture | Models |
4547
|-----------|-------------|--------|
4648
| `Gemma3MultiModalProjector` | AvgPool2d → RMSNorm → MatMul | Gemma3 |
47-
| `MLPMultiModalProjector` | Linear → GELU → Linear | LLaVA, LLaVA-NeXT, VipLLaVA, Phi-4-MM, InternVL2, Pixtral, Molmo |
49+
| `MLPMultiModalProjector` | Linear → GELU → Linear | LLaVA, LLaVA-NeXT, VipLLaVA, Phi-4-MM, InternVL2, Molmo |
50+
| `Mistral3MultiModalProjector` | RMSNorm → spatial merge → Linear → GELU → Linear | Mistral-3, Pixtral |
4851
| `LinearMultiModalProjector` | Single Linear | PaliGemma, Qwen2-Audio, Idefics2, Florence2 |
4952

5053
### Gemma3MultiModalProjector

.github/workflows/benchmark.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,12 @@ jobs:
133133
python-version: "3.12"
134134

135135
- name: Download baseline
136-
uses: actions/download-artifact@v7
136+
uses: actions/download-artifact@v8
137137
with:
138138
name: benchmark-baseline
139139

140140
- name: Download current
141-
uses: actions/download-artifact@v7
141+
uses: actions/download-artifact@v8
142142
with:
143143
name: benchmark-current
144144

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Mistral-3 / Pixtral VLM Support
11+
12+
#### Added
13+
14+
- Support for Mistral-3 / Pixtral vision-language models (`mistral3` model type)
15+
- Pixtral vision encoder with 2D RoPE, bidirectional attention, and spatial patch merging
16+
- `Mistral3MultiModalProjector` for vision-to-text projection (RMSNorm → merge → MLP)
17+
- `PixtralVisionTower` with precomputed 2D rotary caches
18+
- Moved `mistral3` from CausalLM to VLM (LLaVA-style 3-model split: decoder, vision, embedding)
19+
- FP8 quantization config handling (skip block quantization for fp8)
20+
- Integration tests for `ministral3` (text-only) and `mistral3` (VLM)
21+
- Config extraction for `PixtralVisionConfig.norm_eps` and `rope_parameters` fallback
22+
1023
### Static Cache Support
1124

1225
#### Added

docs/design.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ Key components:
8181
| `DecoderLayer` | Pre-norm residual: LayerNorm → Attention → LayerNorm → MLP |
8282
| `MoELayer` | Mixture-of-Experts with pluggable gate |
8383
| `VisionModel` | SigLIP-style patch embedding + transformer encoder |
84-
| Projectors | `Gemma3MultiModalProjector`, `MLPMultiModalProjector`, `LinearMultiModalProjector` |
84+
| `PixtralVisionTower` | Pixtral 2D RoPE vision encoder with bidirectional attention |
85+
| Projectors | `Gemma3MultiModalProjector`, `MLPMultiModalProjector`, `Mistral3MultiModalProjector`, `LinearMultiModalProjector` |
8586
| `InputMixer` | Scatter vision embeddings into text positions |
8687
| RoPE variants | `DefaultRope`, `LinearRope`, `DynamicNTKRope`, `Llama3Rope`, `InterleavedMRope` |
8788
| `ALiBiAttention` | Attention with linear biases (Falcon, Bloom) |
@@ -229,8 +230,10 @@ the correct gate variant per layer.
229230
3. `InputMixer` — scatter vision embeddings at image-token positions
230231
4. `Gemma3CausalLMModel` — standard text decoder
231232

232-
`LLaVAModel` follows a similar pattern with MLPMultiModalProjector.
233-
Many VL models (InternVL2, Pixtral, Idefics, Molmo, etc.) reuse the LLaVA
233+
`LLaVAModel` follows a similar pattern with `MLPMultiModalProjector`.
234+
Mistral-3 / Pixtral uses `Mistral3MultiModalProjector` with a dedicated
235+
`PixtralVisionTower` (2D RoPE, bidirectional attention) instead of CLIP/SigLIP.
236+
Many other VL models (InternVL2, Idefics, Molmo, etc.) reuse the LLaVA
234237
pattern with CLIP/SigLIP vision encoder + projector + LLM.
235238

236239
Uses `VisionLanguageTask` which adds `pixel_values` to the ONNX graph inputs.
@@ -241,6 +244,7 @@ Three projector variants are available for different model families:
241244
|-----------|-------------|---------|
242245
| `Gemma3MultiModalProjector` | AvgPool2d → RMSNorm → MatMul | Gemma3 |
243246
| `MLPMultiModalProjector` | Linear → GELU → Linear | LLaVA, Phi4MM |
247+
| `Mistral3MultiModalProjector` | RMSNorm → spatial merge → Linear → GELU → Linear | Mistral-3, Pixtral |
244248
| `LinearMultiModalProjector` | Single Linear | PaliGemma |
245249

246250
### Encoder-only models (BERT, RoBERTa)

src/mobius/_configs.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ class VisionConfig:
9595
norm_eps: float = 1e-6
9696
mm_tokens_per_image: int | None = None
9797
image_token_id: int | None = None
98+
# Pixtral / Mistral-3 vision fields
99+
model_type: str | None = None
100+
head_dim: int | None = None
101+
rope_theta: float | None = None
98102
# Qwen VL-specific
99103
out_hidden_size: int | None = None
100104
in_channels: int = 3
@@ -273,7 +277,13 @@ def _extract_rope_config(config) -> RoPEConfig:
273277
_nested_rope_theta(rope_scaling, "full_attention"),
274278
default=10_000.0,
275279
),
276-
rope_scaling=(_normalize_rope_scaling(rope_scaling) or None),
280+
# Some models (e.g. Ministral-3) store YaRN config under
281+
# rope_parameters instead of rope_scaling; fall back accordingly.
282+
rope_scaling=(
283+
_normalize_rope_scaling(rope_scaling)
284+
or _normalize_rope_scaling(rope_parameters)
285+
or None
286+
),
277287
partial_rotary_factor=_first_not_none(
278288
getattr(config, "partial_rotary_factor", None),
279289
rope_scaling.get("partial_rotary_factor", None),
@@ -284,12 +294,11 @@ def _extract_rope_config(config) -> RoPEConfig:
284294
getattr(config, "rope_local_base_freq", None),
285295
_nested_rope_theta(rope_scaling, "sliding_attention"),
286296
),
287-
original_max_position_embeddings=(
288-
getattr(
289-
config,
290-
"original_max_position_embeddings",
291-
rope_scaling.get("original_max_position_embeddings", None),
292-
)
297+
original_max_position_embeddings=_first_not_none(
298+
getattr(config, "original_max_position_embeddings", None),
299+
rope_scaling.get("original_max_position_embeddings", None),
300+
# Also check rope_parameters (see rope_scaling comment above).
301+
rope_parameters.get("original_max_position_embeddings", None),
293302
),
294303
)
295304

@@ -351,10 +360,22 @@ def _extract_vision_config(config, parent_config, model_type: str) -> dict:
351360
),
352361
image_size=getattr(vc, "image_size", None),
353362
patch_size=getattr(vc, "patch_size", None),
354-
norm_eps=getattr(vc, "layer_norm_eps", 1e-6),
363+
norm_eps=_first_not_none(
364+
getattr(vc, "layer_norm_eps", None),
365+
getattr(vc, "norm_eps", None),
366+
default=1e-6,
367+
),
368+
# Pixtral / Mistral-3 vision fields
369+
model_type=getattr(vc, "model_type", None),
370+
head_dim=getattr(vc, "head_dim", None),
371+
rope_theta=getattr(vc, "rope_theta", None),
355372
# Qwen VL-specific vision fields
356373
out_hidden_size=getattr(vc, "out_hidden_size", None),
357-
in_channels=getattr(vc, "in_channels", 3),
374+
in_channels=_first_not_none(
375+
getattr(vc, "in_channels", None),
376+
getattr(vc, "num_channels", None),
377+
default=3,
378+
),
358379
spatial_merge_size=getattr(vc, "spatial_merge_size", 2),
359380
temporal_patch_size=getattr(vc, "temporal_patch_size", 2),
360381
num_position_embeddings=getattr(vc, "num_position_embeddings", None),
@@ -573,6 +594,11 @@ def from_transformers(cls, hf_config) -> QuantizationConfig | None:
573594
method = qc.get("quant_method", "none")
574595
if method == "none":
575596
return None
597+
# FP8 per-tensor quantization (float8_e4m3fn + scalar scale)
598+
# is handled by dtype casting in _assign_weight(), not by
599+
# QuantizedLinear block quantization.
600+
if method == "fp8":
601+
return None
576602
return cls(
577603
bits=qc.get("bits", 4),
578604
group_size=qc.get("group_size", 128),

src/mobius/_configs_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,15 @@ def test_from_transformers_method_none_returns_none(self):
628628
hf = type("HFConfig", (), {"quantization_config": {"quant_method": "none"}})()
629629
assert QuantizationConfig.from_transformers(hf) is None
630630

631+
def test_from_transformers_fp8_returns_none(self):
632+
"""FP8 per-tensor quantization is not block quantization; returns None."""
633+
hf = type(
634+
"HFConfig",
635+
(),
636+
{"quantization_config": {"quant_method": "fp8", "bits": 8}},
637+
)()
638+
assert QuantizationConfig.from_transformers(hf) is None
639+
631640
def test_from_transformers_to_dict_object(self):
632641
"""HF QuantizationConfig objects have a to_dict() method."""
633642
inner = type(

src/mobius/_registry.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def _create_default_registry() -> ModelRegistry:
350350
"ministral",
351351
"ministral3",
352352
"mistral",
353-
"mistral3",
353+
"nanochat",
354354
"open-llama",
355355
"openelm",
356356
"qwen2",
@@ -485,6 +485,7 @@ def _create_default_registry() -> ModelRegistry:
485485
"llava_next",
486486
"llava_next_video",
487487
"llava_onevision",
488+
"mistral3",
488489
"molmo",
489490
"paligemma",
490491
"pixtral",
@@ -734,7 +735,6 @@ def _create_default_registry() -> ModelRegistry:
734735
"helium": "kyutai/helium-1-preview-2b",
735736
"minicpm": "optimum-intel-internal-testing/tiny-random-minicpm",
736737
"minicpm3": "openbmb/MiniCPM3-4B",
737-
"mistral3": "mistralai/Mistral-Small-3.2-24B-Instruct-2506",
738738
"ministral3": "Aratako/Ministral-3-3B-Instruct-2512-BF16-TextOnly",
739739
"nanochat": "nanochat-students/nanochat-d20",
740740
"olmo3": "allenai/Olmo-3-7B-Instruct",
@@ -843,6 +843,7 @@ def _create_default_registry() -> ModelRegistry:
843843
"instructblip": "Salesforce/instructblip-flan-t5-xl",
844844
"llava_onevision": "llava-hf/llava-onevision-qwen2-0.5b-ov-hf",
845845
"molmo": "allenai/MolmoE-1B-0924",
846+
"mistral3": "mistralai/Ministral-3-3B-Instruct-2512",
846847

847848
# --- Speech ---
848849
"whisper": "openai/whisper-tiny",

src/mobius/components/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"LinearMultiModalProjector",
4747
"LoRALinear",
4848
"MLP",
49+
"Mistral3MultiModalProjector",
4950
"MLPMultiModalProjector",
5051
"Mamba2Block",
5152
"Mamba2Scan",
@@ -54,6 +55,7 @@
5455
"OffsetRMSNorm",
5556
"PatchEmbed",
5657
"PatchEmbedding",
58+
"PixtralVisionTower",
5759
"PostGatedRMSNorm",
5860
"PostNormDecoderLayer",
5961
"QFormer",
@@ -186,6 +188,10 @@
186188
LinearMultiModalProjector,
187189
MLPMultiModalProjector,
188190
)
191+
from mobius.components._pixtral_vision import (
192+
Mistral3MultiModalProjector,
193+
PixtralVisionTower,
194+
)
189195
from mobius.components._qformer import (
190196
QFormer,
191197
QFormerAttention,

0 commit comments

Comments
 (0)