Skip to content

Commit 3e99081

Browse files
justinchubyCopilot
andauthored
Add vlm_vision_weights helper and adopt it in VLM vision sub-models (#336)
## Summary DRY refactor: dedupe the VLM vision-tower `preprocess_weights` boilerplate. The vision sub-models of **LLaVA**, **Gemma3**, and **Mllama** hand-wrote the identical loop: filter to the vision-tower prefixes, then rename the HuggingFace vision MLP projections `mlp.fc1` → `mlp.up_proj` and `mlp.fc2` → `mlp.down_proj` to match our `FCMLP` component naming. ### Changes - **Add `vlm_vision_weights(state_dict, prefixes)`** to `_weight_utils.py`, alongside the existing `vlm_decoder_weights` / `vlm_embedding_weights` helpers. - Adopt it in gemma3, llava, mllama. The only per-model difference is the prefix tuple: gemma3/llava use `("vision_tower.", "multi_modal_projector.")`, mllama uses `("vision_model.",)`. - Add 3 unit tests (filter+rename, single prefix, no-match). Behaviour-preserving: same filtering and same renames, just centralised. ### Verification - Fast suite: **2792 passed** (baseline 2789 + 3 new tests), 43 skipped. - `ruff check` + `ruff format --check` clean. Signed-off-by: Justin Chu <11205048+justinchuby@users.noreply.github.com> Co-authored-by: Justin Chu <11205048+justinchuby@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e21158a commit 3e99081

5 files changed

Lines changed: 90 additions & 30 deletions

File tree

src/mobius/_weight_utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,39 @@ def vlm_embedding_weights(
521521
return renamed
522522

523523

524+
def vlm_vision_weights(
525+
state_dict: dict[str, torch.Tensor],
526+
prefixes: tuple[str, ...],
527+
) -> dict[str, torch.Tensor]:
528+
"""Extract vision-tower weights for a VLM vision sub-model.
529+
530+
Keeps only keys starting with one of *prefixes* and renames the vision
531+
MLP projections ``mlp.fc1`` → ``mlp.up_proj`` and ``mlp.fc2`` →
532+
``mlp.down_proj`` to match our ``FCMLP`` component naming.
533+
534+
This is the standard pattern for VLM vision sub-models (LLaVA, Gemma3,
535+
Mllama) whose HuggingFace vision encoders use ``fc1``/``fc2`` MLP names.
536+
537+
Args:
538+
state_dict: Full model state dict.
539+
prefixes: Prefixes identifying vision-tower weights to keep, e.g.
540+
``("vision_tower.", "multi_modal_projector.")`` or
541+
``("vision_model.",)``.
542+
543+
Returns:
544+
New dictionary with the kept vision weights and renamed MLP keys.
545+
"""
546+
renamed: dict[str, torch.Tensor] = {}
547+
for key, value in state_dict.items():
548+
if not key.startswith(prefixes):
549+
continue
550+
new_key = key.replace(".mlp.fc1.", ".mlp.up_proj.").replace(
551+
".mlp.fc2.", ".mlp.down_proj."
552+
)
553+
renamed[new_key] = value
554+
return renamed
555+
556+
524557
def _reshape_packed_qweight(value: torch.Tensor, blob_size: int) -> torch.Tensor:
525558
"""Transpose and reshape a packed qweight tensor for MatMulNBits.
526559

src/mobius/_weight_utils_test.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
tie_word_embeddings,
2222
vlm_decoder_weights,
2323
vlm_embedding_weights,
24+
vlm_vision_weights,
2425
)
2526

2627

@@ -396,6 +397,44 @@ def test_custom_keyword(self):
396397
assert list(result.keys()) == ["word_embedding.weight"]
397398

398399

400+
class TestVlmVisionWeights:
401+
"""Tests for vlm_vision_weights."""
402+
403+
def test_filters_and_renames(self):
404+
"""Keeps prefixed keys and renames fc1/fc2."""
405+
fc1 = torch.randn(4, 8)
406+
fc2 = torch.randn(8, 4)
407+
sd = {
408+
"vision_tower.encoder.layers.0.mlp.fc1.weight": fc1,
409+
"vision_tower.encoder.layers.0.mlp.fc2.weight": fc2,
410+
"multi_modal_projector.linear.weight": torch.randn(4),
411+
"language_model.model.layers.0.weight": torch.randn(4),
412+
}
413+
result = vlm_vision_weights(sd, ("vision_tower.", "multi_modal_projector."))
414+
assert set(result.keys()) == {
415+
"vision_tower.encoder.layers.0.mlp.up_proj.weight",
416+
"vision_tower.encoder.layers.0.mlp.down_proj.weight",
417+
"multi_modal_projector.linear.weight",
418+
}
419+
assert result["vision_tower.encoder.layers.0.mlp.up_proj.weight"].data_ptr() == (
420+
fc1.data_ptr()
421+
)
422+
423+
def test_single_prefix(self):
424+
"""Works with a single-element prefix tuple."""
425+
sd = {
426+
"vision_model.layers.0.mlp.fc1.weight": torch.randn(2),
427+
"other.weight": torch.randn(2),
428+
}
429+
result = vlm_vision_weights(sd, ("vision_model.",))
430+
assert list(result.keys()) == ["vision_model.layers.0.mlp.up_proj.weight"]
431+
432+
def test_empty_when_no_match(self):
433+
"""Returns empty dict when no key matches the prefixes."""
434+
sd = {"language_model.layers.0.weight": torch.randn(2)}
435+
assert vlm_vision_weights(sd, ("vision_tower.",)) == {}
436+
437+
399438
class TestPreprocessGptqWeights:
400439
"""Tests for GPTQ weight preprocessing.
401440

src/mobius/models/gemma3.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
from onnxscript import OpBuilder, nn
1919

2020
from mobius._configs import ArchitectureConfig
21-
from mobius._weight_utils import vlm_decoder_weights, vlm_embedding_weights
21+
from mobius._weight_utils import (
22+
vlm_decoder_weights,
23+
vlm_embedding_weights,
24+
vlm_vision_weights,
25+
)
2226
from mobius.components import (
2327
Gemma3MultiModalProjector,
2428
Linear,
@@ -93,15 +97,7 @@ def forward(self, op: OpBuilder, pixel_values: ir.Value):
9397
def preprocess_weights(
9498
self, state_dict: dict[str, torch.Tensor]
9599
) -> dict[str, torch.Tensor]:
96-
renamed: dict[str, torch.Tensor] = {}
97-
for key, value in state_dict.items():
98-
if not key.startswith(("vision_tower.", "multi_modal_projector.")):
99-
continue
100-
# VisionModel MLP uses up_proj/down_proj; HF uses fc1/fc2
101-
key = key.replace(".mlp.fc1.", ".mlp.up_proj.")
102-
key = key.replace(".mlp.fc2.", ".mlp.down_proj.")
103-
renamed[key] = value
104-
return renamed
100+
return vlm_vision_weights(state_dict, ("vision_tower.", "multi_modal_projector."))
105101

106102

107103
class _Gemma3EmbeddingModel(nn.Module):

src/mobius/models/llava.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
from onnxscript import OpBuilder, nn
2828

2929
from mobius._configs import ArchitectureConfig
30-
from mobius._weight_utils import vlm_decoder_weights, vlm_embedding_weights
30+
from mobius._weight_utils import (
31+
vlm_decoder_weights,
32+
vlm_embedding_weights,
33+
vlm_vision_weights,
34+
)
3135
from mobius.components import (
3236
Embedding,
3337
Linear,
@@ -92,15 +96,7 @@ def forward(self, op: OpBuilder, pixel_values: ir.Value):
9296
def preprocess_weights(
9397
self, state_dict: dict[str, torch.Tensor]
9498
) -> dict[str, torch.Tensor]:
95-
renamed: dict[str, torch.Tensor] = {}
96-
for key, value in state_dict.items():
97-
if not key.startswith(("vision_tower.", "multi_modal_projector.")):
98-
continue
99-
# VisionModel MLP uses up_proj/down_proj; HF uses fc1/fc2
100-
key = key.replace(".mlp.fc1.", ".mlp.up_proj.")
101-
key = key.replace(".mlp.fc2.", ".mlp.down_proj.")
102-
renamed[key] = value
103-
return renamed
99+
return vlm_vision_weights(state_dict, ("vision_tower.", "multi_modal_projector."))
104100

105101

106102
class _LLaVAEmbeddingModel(nn.Module):

src/mobius/models/mllama.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
from onnxscript import OpBuilder, nn
2424

2525
from mobius._configs import ArchitectureConfig, MllamaConfig
26-
from mobius._weight_utils import vlm_decoder_weights, vlm_embedding_weights
26+
from mobius._weight_utils import (
27+
vlm_decoder_weights,
28+
vlm_embedding_weights,
29+
vlm_vision_weights,
30+
)
2731
from mobius.components import (
2832
MLP,
2933
DecoderLayer,
@@ -317,15 +321,7 @@ def forward(self, op: OpBuilder, pixel_values: ir.Value):
317321
def preprocess_weights(
318322
self, state_dict: dict[str, torch.Tensor]
319323
) -> dict[str, torch.Tensor]:
320-
renamed: dict[str, torch.Tensor] = {}
321-
for key, value in state_dict.items():
322-
if not key.startswith("vision_model."):
323-
continue
324-
# VisionModel MLP uses up_proj/down_proj; HF uses fc1/fc2
325-
key = key.replace(".mlp.fc1.", ".mlp.up_proj.")
326-
key = key.replace(".mlp.fc2.", ".mlp.down_proj.")
327-
renamed[key] = value
328-
return renamed
324+
return vlm_vision_weights(state_dict, ("vision_model.",))
329325

330326

331327
class _MllamaEmbeddingModel(nn.Module):

0 commit comments

Comments
 (0)