Skip to content

Commit 8fa912e

Browse files
justinchubyCopilot
andcommitted
Address Phi-3.5 vision review feedback
Use the public components API for CLIP, broaden the CLIP vision config contract, and filter unused CLIP layer weights when Phi-3.5-Vision exports an intermediate feature layer. Resolve cached Phi-3.5 projector shards via the HuggingFace cache index instead of requiring a full local snapshot, and remove the stale positive image-token constant. Add assertions covering skipped vision tower weights. Signed-off-by: justinchuby <justinchuby@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b9f2538 commit 8fa912e

4 files changed

Lines changed: 89 additions & 33 deletions

File tree

src/mobius/models/_phi3_vision_projector.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import dataclasses
2929
import glob
30+
import json
3031
import os
3132

3233
import numpy as np
@@ -42,6 +43,8 @@
4243
_PROJECTION_FIRST_BIAS_KEY = _PROJECTOR_PREFIX + "img_projection.0.bias"
4344
_PROJECTION_SECOND_WEIGHT_KEY = _PROJECTOR_PREFIX + "img_projection.2.weight"
4445
_PROJECTION_SECOND_BIAS_KEY = _PROJECTOR_PREFIX + "img_projection.2.bias"
46+
_WEIGHT_INDEX_NAME = "model.safetensors.index.json"
47+
_SINGLE_WEIGHT_NAME = "model.safetensors"
4548

4649
# CLIP ViT-L/14-336 produces a 24x24 patch grid (576 patches).
4750
_PATCH_GRID_SIDE = 24
@@ -75,18 +78,46 @@ class Phi3VisionProjectorWeights:
7578
projection_second_bias: np.ndarray
7679

7780

78-
def _resolve_checkpoint_directory(model_id_or_directory: str) -> str:
79-
"""Return a local directory that contains the checkpoint safetensors.
81+
def _resolve_checkpoint_shard_paths(model_id_or_directory: str) -> list[str]:
82+
"""Return local safetensors shard paths for a checkpoint.
8083
8184
Accepts either a local path or a HuggingFace hub model id (which is
8285
resolved via the local cache; the weights must already be downloaded).
8386
"""
8487
if os.path.isdir(model_id_or_directory):
85-
return model_id_or_directory
88+
return sorted(glob.glob(os.path.join(model_id_or_directory, "*.safetensors")))
8689

87-
from huggingface_hub import snapshot_download
90+
from huggingface_hub import hf_hub_download
91+
from huggingface_hub.errors import EntryNotFoundError, LocalEntryNotFoundError
8892

89-
return snapshot_download(model_id_or_directory, local_files_only=True)
93+
try:
94+
index_path = hf_hub_download(
95+
repo_id=model_id_or_directory,
96+
filename=_WEIGHT_INDEX_NAME,
97+
local_files_only=True,
98+
)
99+
with open(index_path, encoding="utf-8") as f:
100+
index = json.load(f)
101+
filenames = sorted(set(index["weight_map"].values()))
102+
except (EntryNotFoundError, LocalEntryNotFoundError):
103+
filenames = [_SINGLE_WEIGHT_NAME]
104+
105+
shard_paths: list[str] = []
106+
for filename in filenames:
107+
try:
108+
shard_paths.append(
109+
hf_hub_download(
110+
repo_id=model_id_or_directory,
111+
filename=filename,
112+
local_files_only=True,
113+
)
114+
)
115+
except (EntryNotFoundError, LocalEntryNotFoundError) as exc:
116+
raise FileNotFoundError(
117+
f"Could not find cached safetensors shard {filename!r} for "
118+
f"{model_id_or_directory!r}. Build/load the model weights first."
119+
) from exc
120+
return shard_paths
90121

91122

92123
def load_phi3_vision_projector_weights(
@@ -111,11 +142,10 @@ def load_phi3_vision_projector_weights(
111142
"""
112143
from safetensors import safe_open
113144

114-
directory = _resolve_checkpoint_directory(model_id_or_directory)
115-
shard_paths = sorted(glob.glob(os.path.join(directory, "*.safetensors")))
145+
shard_paths = _resolve_checkpoint_shard_paths(model_id_or_directory)
116146
if not shard_paths:
117147
raise FileNotFoundError(
118-
f"No .safetensors shards found in checkpoint directory: {directory}"
148+
f"No .safetensors shards found for checkpoint: {model_id_or_directory}"
119149
)
120150

121151
wanted_keys = {
@@ -136,7 +166,8 @@ def load_phi3_vision_projector_weights(
136166
missing = wanted_keys - collected.keys()
137167
if missing:
138168
raise KeyError(
139-
f"Missing Phi-3.5-Vision projector weights in {directory}: {sorted(missing)}"
169+
"Missing Phi-3.5-Vision projector weights in "
170+
f"{model_id_or_directory}: {sorted(missing)}"
140171
)
141172

142173
return Phi3VisionProjectorWeights(

src/mobius/models/clip.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,38 @@
55

66
from __future__ import annotations
77

8-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, Protocol
99

1010
import torch
1111
from onnxscript import OpBuilder, nn
1212

1313
from mobius._configs import ArchitectureConfig
14-
from mobius.components import FCMLP
15-
from mobius.components._common import INT64_MAX, Embedding, LayerNorm
16-
from mobius.components._conv import Conv2d, Conv2dNoBias
17-
from mobius.components._encoder import EncoderAttention
14+
from mobius.components import (
15+
FCMLP,
16+
INT64_MAX,
17+
Conv2d,
18+
Conv2dNoBias,
19+
Embedding,
20+
EncoderAttention,
21+
LayerNorm,
22+
)
1823

1924
if TYPE_CHECKING:
2025
import onnx_ir as ir
2126

2227

28+
class _CLIPVisionConfig(Protocol):
29+
hidden_size: int
30+
intermediate_size: int
31+
num_hidden_layers: int
32+
num_attention_heads: int
33+
image_size: int
34+
patch_size: int
35+
num_channels: int
36+
rms_norm_eps: float
37+
hidden_act: str | None
38+
39+
2340
class ClipVisionConfigView:
2441
"""Adapter exposing a :class:`VisionConfig` under CLIP's field names.
2542
@@ -47,7 +64,7 @@ def __init__(self, vision_config, *, default_hidden_act: str = "quick_gelu"):
4764
class _CLIPVisionEmbeddings(nn.Module):
4865
"""CLIP vision embeddings: Conv2d patch + CLS token + position embeddings."""
4966

50-
def __init__(self, config: ArchitectureConfig):
67+
def __init__(self, config: _CLIPVisionConfig):
5168
super().__init__()
5269
hidden_size = config.hidden_size
5370
patch_size = config.patch_size
@@ -117,7 +134,7 @@ def forward(self, op: OpBuilder, x: ir.Value):
117134
class _CLIPVisionEncoderLayer(nn.Module):
118135
"""CLIP vision encoder layer: pre-norm with LayerNorm."""
119136

120-
def __init__(self, config: ArchitectureConfig):
137+
def __init__(self, config: _CLIPVisionConfig):
121138
super().__init__()
122139
self.self_attn = EncoderAttention(config.hidden_size, config.num_attention_heads)
123140
self.layer_norm1 = LayerNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -195,7 +212,7 @@ class CLIPVisionModel(nn.Module):
195212

196213
def __init__(
197214
self,
198-
config: ArchitectureConfig,
215+
config: _CLIPVisionConfig,
199216
*,
200217
feature_layer: int | None = None,
201218
drop_class_token: bool = False,
@@ -250,9 +267,17 @@ def preprocess_weights(
250267
self, state_dict: dict[str, torch.Tensor]
251268
) -> dict[str, torch.Tensor]:
252269
new_state_dict = {}
270+
num_encoder_layers = len(self.encoder)
253271
for name, tensor in state_dict.items():
254272
new_name = _rename_clip_vision_weight(name)
255273
if new_name is not None:
274+
if self.post_layernorm is None and new_name.startswith("post_layernorm."):
275+
continue
276+
if new_name.startswith("encoder."):
277+
parts = new_name.split(".", 2)
278+
if len(parts) >= 3 and parts[1].isdigit():
279+
if int(parts[1]) >= num_encoder_layers:
280+
continue
256281
new_state_dict[new_name] = tensor
257282
return new_state_dict
258283

@@ -326,7 +351,7 @@ def _rename_clip_vision_weight(name: str) -> str | None:
326351
class _SigLIPVisionEmbeddings(nn.Module):
327352
"""SigLIP vision embeddings: Conv2d patch + position embeddings (no CLS token)."""
328353

329-
def __init__(self, config: ArchitectureConfig):
354+
def __init__(self, config: _CLIPVisionConfig):
330355
super().__init__()
331356
hidden_size = config.hidden_size
332357
patch_size = config.patch_size

src/mobius/models/phi3_v.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050
from __future__ import annotations
5151

52-
from typing import TYPE_CHECKING, cast
52+
from typing import TYPE_CHECKING
5353

5454
import torch
5555
from onnxscript import nn
@@ -71,11 +71,6 @@
7171
if TYPE_CHECKING:
7272
import onnx_ir as ir
7373

74-
# Phi-3-Vision: <|image|> token id (kept for reference; the HF processor marks
75-
# image slots with negative placeholder ids, not this positive id — see
76-
# ``_Phi3VEmbeddingModel.forward``).
77-
_IMAGE_TOKEN_ID = 32044
78-
7974
# Upper bound (magnitude) for negative image placeholder ids, mirroring
8075
# ``modeling_phi3_v.MAX_INPUT_ID = int(1e9)``. Image positions satisfy
8176
# ``-_MAX_INPUT_ID < input_ids < 0``.
@@ -201,7 +196,7 @@ def __init__(self, config: ArchitectureConfig):
201196
assert config.vision is not None, "Phi3-V requires a vision config"
202197
clip_config = ClipVisionConfigView(config.vision)
203198
self.vision_tower = CLIPVisionModel(
204-
cast(ArchitectureConfig, clip_config),
199+
clip_config,
205200
feature_layer=config.vision.feature_layer,
206201
drop_class_token=True,
207202
)
@@ -223,11 +218,14 @@ def preprocess_weights(
223218
The ``img_projection`` and ``sub_GN``/``glb_GN`` tensors are dropped
224219
(host-side HD transform).
225220
"""
226-
renamed: dict[str, torch.Tensor] = {}
221+
clip_state_dict: dict[str, torch.Tensor] = {}
227222
for key, value in state_dict.items():
228-
new_key = _rename_phi3v_vision_weight(key)
229-
if new_key is not None:
230-
renamed[new_key] = value
223+
if key.startswith(_VISION_TOWER_PREFIX):
224+
clip_state_dict[key[len(_VISION_TOWER_PREFIX) :]] = value
225+
renamed = {
226+
"vision_tower." + key: value
227+
for key, value in self.vision_tower.preprocess_weights(clip_state_dict).items()
228+
}
231229
return renamed
232230

233231

@@ -351,11 +349,11 @@ def preprocess_weights(
351349
"""
352350
renamed: dict[str, torch.Tensor] = {}
353351

352+
for key, value in self.vision_encoder.preprocess_weights(state_dict).items():
353+
renamed["vision_encoder." + key] = value
354+
354355
for key, value in state_dict.items():
355-
vision_key = _rename_phi3v_vision_weight(key)
356-
if vision_key is not None:
357-
renamed["vision_encoder." + vision_key] = value
358-
elif key.startswith("model.vision_embed_tokens."):
356+
if key.startswith("model.vision_embed_tokens."):
359357
# img_projection / sub_GN / glb_GN and any other vision-embed
360358
# state is host-side (HD transform) — skip it.
361359
pass

src/mobius/models/phi3_v_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ def test_feature_layer_maps_only_needed_layers(self):
127127
assert "vision_tower.embeddings.class_embedding" in renamed
128128
assert "vision_tower.embeddings.patch_embedding.projection.weight" in renamed
129129
assert "vision_tower.encoder.0.mlp.up_proj.weight" in renamed
130+
assert "vision_tower.encoder.2.mlp.up_proj.weight" not in renamed
131+
assert "vision_tower.post_layernorm.weight" not in renamed
130132
# Projector + separator are host-side.
131133
assert not any("projection.0" in k or "sub_GN" in k for k in renamed)
132134
# Only vision_tower.* names are produced.

0 commit comments

Comments
 (0)