Image-Text-to-Text
Transformers
Safetensors
English
nanbeige_vlm
vlm
nanbeige
siglip
conversational
custom_code
SkyAsl commited on
Commit
d35708c
·
verified ·
1 Parent(s): a7afeba

Upload modeling_nanbeige_vlm.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. modeling_nanbeige_vlm.py +103 -75
modeling_nanbeige_vlm.py CHANGED
@@ -7,12 +7,14 @@ Usage:
7
 
8
  model = AutoModel.from_pretrained("SkyAsl/Nanbeige4.1-VLM-Base", trust_remote_code=True)
9
  tokenizer = AutoTokenizer.from_pretrained("SkyAsl/Nanbeige4.1-VLM-Base", trust_remote_code=True)
 
10
 
11
  image = Image.open("photo.jpg")
12
  result = model.describe(image)
13
  print(result)
14
  """
15
 
 
16
  import torch
17
  import torch.nn as nn
18
  import torch.nn.functional as F
@@ -22,26 +24,17 @@ from transformers import (
22
  SiglipVisionModel,
23
  SiglipImageProcessor,
24
  )
 
25
  from .configuration_nanbeige_vlm import NanbeigeVLMConfig
26
 
 
 
27
 
28
  # ---------------------------------------------------------------------------
29
  # Pooled projector (729 → 196 tokens via 2×2 avg-pool then linear)
30
  # ---------------------------------------------------------------------------
31
 
32
  class PooledProjector(nn.Module):
33
- """
34
- Reduces SigLIP's 729 patch tokens to 196 via spatial average pooling,
35
- then projects to the LLM hidden dimension.
36
-
37
- (B, 729, D_vision)
38
- → reshape (B, D_vision, 27, 27)
39
- → pad (B, D_vision, 28, 28)
40
- → avgpool (B, D_vision, 14, 14)
41
- → flatten (B, 196, D_vision)
42
- → linear (B, 196, D_llm)
43
- """
44
-
45
  def __init__(self, vision_hidden_size: int, llm_hidden_size: int):
46
  super().__init__()
47
  self.proj = nn.Sequential(
@@ -53,10 +46,10 @@ class PooledProjector(nn.Module):
53
  def forward(self, image_features: torch.Tensor) -> torch.Tensor:
54
  B, N, C = image_features.shape
55
  x = image_features.permute(0, 2, 1).reshape(B, C, 27, 27)
56
- x = F.pad(x, (0, 1, 0, 1), mode="replicate") # 27×27 → 28×28
57
- x = F.avg_pool2d(x, kernel_size=2, stride=2) # 28×28 → 14×14
58
- x = x.flatten(2).permute(0, 2, 1) # (B, 196, C)
59
- return self.proj(x) # (B, 196, D_llm)
60
 
61
 
62
  # ---------------------------------------------------------------------------
@@ -64,59 +57,102 @@ class PooledProjector(nn.Module):
64
  # ---------------------------------------------------------------------------
65
 
66
  class NanbeigeVLMModel(PreTrainedModel):
67
- """
68
- SigLIP so400m → PooledProjector (729→196 tokens) → Nanbeige4.1-3B
69
-
70
- Stage 1 pretrain: only mm_projector is trained.
71
- Vision tower and LLM are frozen.
72
- """
73
-
74
  config_class = NanbeigeVLMConfig
75
-
76
- # Tells transformers which keys belong to sub-models that are loaded
77
- # separately — prevents 'unexpected key' warnings.
78
  _no_split_modules = ["SiglipVisionModel", "NanbeigeForCausalLM"]
79
 
80
  def __init__(self, config: NanbeigeVLMConfig, image_token_id: int = None):
81
  super().__init__(config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- # ── Vision tower (frozen at Stage 1) ──────────────────────────────
84
- self.vision_tower = SiglipVisionModel.from_pretrained(
85
- config.vision_model_id, torch_dtype=torch.bfloat16, device_map=None
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  )
87
- self.vision_tower.requires_grad_(False)
88
- vision_hidden_size = self.vision_tower.config.hidden_size
89
 
90
- # ── Language model (frozen at Stage 1) ────────────────────────────
 
91
  try:
92
- self.language_model = AutoModelForCausalLM.from_pretrained(
93
  config.llm_model_id,
94
  trust_remote_code=True,
95
- torch_dtype=torch.bfloat16,
96
  attn_implementation="flash_attention_2",
97
- device_map=None
98
  )
99
  except (ImportError, ValueError):
100
- self.language_model = AutoModelForCausalLM.from_pretrained(
101
  config.llm_model_id,
102
  trust_remote_code=True,
103
- torch_dtype=torch.bfloat16,
104
- device_map=None
105
  )
106
- self.language_model.requires_grad_(False)
107
- llm_hidden_size = self.language_model.config.hidden_size
108
 
109
- # ── Projector ──────────────────────────────────────────────────────
110
- self.mm_projector = PooledProjector(
111
  vision_hidden_size, llm_hidden_size
112
- ).to(torch.bfloat16)
 
 
 
 
 
 
 
 
 
 
113
 
114
- # Set after resize_token_embeddings if needed
115
- self.image_token_id = image_token_id
116
- self.post_init()
 
 
 
 
117
 
118
  # ------------------------------------------------------------------
119
- # Core forward (used by Trainer)
120
  # ------------------------------------------------------------------
121
 
122
  def forward(self, input_ids, pixel_values, attention_mask=None, labels=None):
@@ -126,9 +162,9 @@ class NanbeigeVLMModel(PreTrainedModel):
126
  with torch.inference_mode():
127
  image_features = self.vision_tower(
128
  pixel_values=pixel_values
129
- ).last_hidden_state # (B, 729, D_vision)
130
 
131
- image_embeds = self.mm_projector(image_features) # (B, 196, D_llm)
132
  num_image_tokens = image_embeds.shape[1]
133
 
134
  with torch.inference_mode():
@@ -151,17 +187,27 @@ class NanbeigeVLMModel(PreTrainedModel):
151
 
152
  pos = positions[0].item()
153
  merged_embeds.append(
154
- torch.cat([inputs_embeds[i, :pos], image_embeds[i], inputs_embeds[i, pos+1:]], dim=0)
 
155
  )
156
  if attention_mask is not None:
157
- img_mask = torch.ones(num_image_tokens, device=attention_mask.device, dtype=attention_mask.dtype)
 
 
 
 
158
  merged_mask.append(
159
- torch.cat([attention_mask[i, :pos], img_mask, attention_mask[i, pos+1:]])
 
160
  )
161
  if labels is not None:
162
- img_labels = torch.full((num_image_tokens,), -100, device=labels.device, dtype=labels.dtype)
 
 
 
163
  merged_labels.append(
164
- torch.cat([labels[i, :pos], img_labels, labels[i, pos+1:]])
 
165
  )
166
 
167
  combined_embeds = torch.stack(merged_embeds, dim=0)
@@ -175,7 +221,7 @@ class NanbeigeVLMModel(PreTrainedModel):
175
  )
176
 
177
  # ------------------------------------------------------------------
178
- # High-level inference helpers
179
  # ------------------------------------------------------------------
180
 
181
  @torch.no_grad()
@@ -189,29 +235,11 @@ class NanbeigeVLMModel(PreTrainedModel):
189
  temperature: float = 0.7,
190
  repetition_penalty: float = 1.3,
191
  ) -> str:
192
- """
193
- Convenience method: pass a PIL image, get a text description back.
194
-
195
- Args:
196
- image: PIL.Image
197
- prompt: Instruction string
198
- tokenizer: Pass tokenizer if not set on model
199
- max_new_tokens: Max output length
200
- do_sample: True for creative outputs, False for deterministic
201
- temperature: Sampling temperature (only used if do_sample=True)
202
- repetition_penalty: Penalise repeated tokens
203
-
204
- Returns:
205
- str: generated description
206
- """
207
- assert self.image_token_id is not None, \
208
- "Set model.image_token_id before calling describe()."
209
-
210
  tok = tokenizer or getattr(self, "_tokenizer", None)
211
  assert tok is not None, \
212
  "Pass tokenizer=... to describe() or call model.set_tokenizer(tokenizer) first."
213
 
214
- device = next(self.parameters()).device
215
  processor = SiglipImageProcessor.from_pretrained(self.config.vision_model_id)
216
 
217
  pixel_values = processor(images=image, return_tensors="pt").pixel_values.to(device, dtype=torch.bfloat16)
@@ -242,5 +270,5 @@ class NanbeigeVLMModel(PreTrainedModel):
242
 
243
  def set_tokenizer(self, tokenizer):
244
  """Attach tokenizer to model so you don't have to pass it to describe()."""
245
- self._tokenizer = tokenizer
246
  self.image_token_id = tokenizer.convert_tokens_to_ids("<image>")
 
7
 
8
  model = AutoModel.from_pretrained("SkyAsl/Nanbeige4.1-VLM-Base", trust_remote_code=True)
9
  tokenizer = AutoTokenizer.from_pretrained("SkyAsl/Nanbeige4.1-VLM-Base", trust_remote_code=True)
10
+ model.set_tokenizer(tokenizer)
11
 
12
  image = Image.open("photo.jpg")
13
  result = model.describe(image)
14
  print(result)
15
  """
16
 
17
+ import os
18
  import torch
19
  import torch.nn as nn
20
  import torch.nn.functional as F
 
24
  SiglipVisionModel,
25
  SiglipImageProcessor,
26
  )
27
+ from transformers.utils import logging
28
  from .configuration_nanbeige_vlm import NanbeigeVLMConfig
29
 
30
+ logger = logging.get_logger(__name__)
31
+
32
 
33
  # ---------------------------------------------------------------------------
34
  # Pooled projector (729 → 196 tokens via 2×2 avg-pool then linear)
35
  # ---------------------------------------------------------------------------
36
 
37
  class PooledProjector(nn.Module):
 
 
 
 
 
 
 
 
 
 
 
 
38
  def __init__(self, vision_hidden_size: int, llm_hidden_size: int):
39
  super().__init__()
40
  self.proj = nn.Sequential(
 
46
  def forward(self, image_features: torch.Tensor) -> torch.Tensor:
47
  B, N, C = image_features.shape
48
  x = image_features.permute(0, 2, 1).reshape(B, C, 27, 27)
49
+ x = F.pad(x, (0, 1, 0, 1), mode="replicate")
50
+ x = F.avg_pool2d(x, kernel_size=2, stride=2)
51
+ x = x.flatten(2).permute(0, 2, 1)
52
+ return self.proj(x)
53
 
54
 
55
  # ---------------------------------------------------------------------------
 
57
  # ---------------------------------------------------------------------------
58
 
59
  class NanbeigeVLMModel(PreTrainedModel):
 
 
 
 
 
 
 
60
  config_class = NanbeigeVLMConfig
 
 
 
61
  _no_split_modules = ["SiglipVisionModel", "NanbeigeForCausalLM"]
62
 
63
  def __init__(self, config: NanbeigeVLMConfig, image_token_id: int = None):
64
  super().__init__(config)
65
+ # Sub-models are NOT loaded here — from_pretrained() handles this.
66
+ # This prevents the meta-device conflict with nested from_pretrained calls.
67
+ self.vision_tower = None
68
+ self.language_model = None
69
+ self.mm_projector = None
70
+ self.image_token_id = image_token_id
71
+
72
+ # ------------------------------------------------------------------
73
+ # Override from_pretrained to handle nested model loading correctly
74
+ # ------------------------------------------------------------------
75
+
76
+ @classmethod
77
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
78
+ """
79
+ Custom loader that:
80
+ 1. Loads sub-models (SigLIP, LLM) normally — outside any meta context.
81
+ 2. Loads only projector weights from the checkpoint.
82
+ 3. Returns a fully initialised NanbeigeVLMModel.
83
+ """
84
+ import safetensors.torch
85
+ from huggingface_hub import hf_hub_download
86
+
87
+ config = kwargs.pop("config", None)
88
+ token = kwargs.pop("token", None)
89
+ cache_dir = kwargs.pop("cache_dir", None)
90
 
91
+ if config is None:
92
+ config = NanbeigeVLMConfig.from_pretrained(
93
+ pretrained_model_name_or_path, token=token, cache_dir=cache_dir
94
+ )
95
+
96
+ torch_dtype = kwargs.pop("torch_dtype", torch.bfloat16)
97
+
98
+ # ── 1. Build empty shell ──────────────────────────────────────
99
+ model = cls(config)
100
+
101
+ # ── 2. Load SigLIP ────────────────────────────────────────────
102
+ logger.info("Loading vision tower...")
103
+ model.vision_tower = SiglipVisionModel.from_pretrained(
104
+ config.vision_model_id,
105
+ torch_dtype=torch_dtype,
106
+ device_map=None,
107
  )
108
+ model.vision_tower.requires_grad_(False)
109
+ vision_hidden_size = model.vision_tower.config.hidden_size
110
 
111
+ # ── 3. Load LLM ───────────────────────────────────────────────
112
+ logger.info("Loading language model...")
113
  try:
114
+ model.language_model = AutoModelForCausalLM.from_pretrained(
115
  config.llm_model_id,
116
  trust_remote_code=True,
117
+ torch_dtype=torch_dtype,
118
  attn_implementation="flash_attention_2",
119
+ device_map=None,
120
  )
121
  except (ImportError, ValueError):
122
+ model.language_model = AutoModelForCausalLM.from_pretrained(
123
  config.llm_model_id,
124
  trust_remote_code=True,
125
+ torch_dtype=torch_dtype,
126
+ device_map=None,
127
  )
128
+ model.language_model.requires_grad_(False)
129
+ llm_hidden_size = model.language_model.config.hidden_size
130
 
131
+ # ── 4. Build projector, load trained weights ──────────────────
132
+ model.mm_projector = PooledProjector(
133
  vision_hidden_size, llm_hidden_size
134
+ ).to(torch_dtype)
135
+
136
+ logger.info("Loading projector weights from checkpoint...")
137
+ weights_path = hf_hub_download(
138
+ repo_id=pretrained_model_name_or_path,
139
+ filename="model.safetensors",
140
+ token=token,
141
+ cache_dir=cache_dir,
142
+ )
143
+ all_weights = safetensors.torch.load_file(weights_path)
144
+ proj_weights = {k: v for k, v in all_weights.items() if "mm_projector" in k}
145
 
146
+ # Strip "mm_projector." prefix for load_state_dict
147
+ proj_weights_clean = {k.replace("mm_projector.", "", 1): v
148
+ for k, v in proj_weights.items()}
149
+ model.mm_projector.load_state_dict(proj_weights_clean)
150
+
151
+ logger.info("NanbeigeVLM loaded successfully.")
152
+ return model
153
 
154
  # ------------------------------------------------------------------
155
+ # forward (used by Trainer during Stage 2)
156
  # ------------------------------------------------------------------
157
 
158
  def forward(self, input_ids, pixel_values, attention_mask=None, labels=None):
 
162
  with torch.inference_mode():
163
  image_features = self.vision_tower(
164
  pixel_values=pixel_values
165
+ ).last_hidden_state
166
 
167
+ image_embeds = self.mm_projector(image_features)
168
  num_image_tokens = image_embeds.shape[1]
169
 
170
  with torch.inference_mode():
 
187
 
188
  pos = positions[0].item()
189
  merged_embeds.append(
190
+ torch.cat([inputs_embeds[i, :pos], image_embeds[i],
191
+ inputs_embeds[i, pos+1:]], dim=0)
192
  )
193
  if attention_mask is not None:
194
+ img_mask = torch.ones(
195
+ num_image_tokens,
196
+ device=attention_mask.device,
197
+ dtype=attention_mask.dtype,
198
+ )
199
  merged_mask.append(
200
+ torch.cat([attention_mask[i, :pos], img_mask,
201
+ attention_mask[i, pos+1:]])
202
  )
203
  if labels is not None:
204
+ img_labels = torch.full(
205
+ (num_image_tokens,), -100,
206
+ device=labels.device, dtype=labels.dtype,
207
+ )
208
  merged_labels.append(
209
+ torch.cat([labels[i, :pos], img_labels,
210
+ labels[i, pos+1:]])
211
  )
212
 
213
  combined_embeds = torch.stack(merged_embeds, dim=0)
 
221
  )
222
 
223
  # ------------------------------------------------------------------
224
+ # Inference helpers
225
  # ------------------------------------------------------------------
226
 
227
  @torch.no_grad()
 
235
  temperature: float = 0.7,
236
  repetition_penalty: float = 1.3,
237
  ) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  tok = tokenizer or getattr(self, "_tokenizer", None)
239
  assert tok is not None, \
240
  "Pass tokenizer=... to describe() or call model.set_tokenizer(tokenizer) first."
241
 
242
+ device = next(self.mm_projector.parameters()).device
243
  processor = SiglipImageProcessor.from_pretrained(self.config.vision_model_id)
244
 
245
  pixel_values = processor(images=image, return_tensors="pt").pixel_values.to(device, dtype=torch.bfloat16)
 
270
 
271
  def set_tokenizer(self, tokenizer):
272
  """Attach tokenizer to model so you don't have to pass it to describe()."""
273
+ self._tokenizer = tokenizer
274
  self.image_token_id = tokenizer.convert_tokens_to_ids("<image>")