| """BaguettotronVLM model — self-contained for HuggingFace Hub.""" |
| from __future__ import annotations |
|
|
| import torch |
| import torch.nn as nn |
| from transformers import ( |
| AutoModel, |
| AutoModelForCausalLM, |
| AutoTokenizer, |
| LogitsProcessor, |
| PreTrainedModel, |
| StoppingCriteria, |
| ) |
| from transformers.generation.logits_process import RepetitionPenaltyLogitsProcessor |
| from transformers.modeling_outputs import CausalLMOutputWithPast |
|
|
| from .configuration_baguettotron_vlm import BaguettotronVLMConfig |
|
|
|
|
| class SafeRepetitionPenalty(LogitsProcessor): |
| """Repetition penalty that is a no-op on the first decoding step. |
| |
| generate(inputs_embeds=...) begins with an empty input_ids tensor. On MPS |
| the empty-index gather inside transformers' RepetitionPenaltyLogitsProcessor |
| zeroes the entire logits row rather than leaving it untouched (it is a |
| correct no-op on CPU and CUDA), which corrupts the first token and derails |
| the answer. There is nothing to penalise on that step anyway, so skipping it |
| removes the corruption while keeping the penalty for every later step. |
| """ |
|
|
| def __init__(self, penalty: float): |
| self.inner = RepetitionPenaltyLogitsProcessor(penalty) |
|
|
| def __call__(self, input_ids: torch.Tensor, scores: torch.Tensor) -> torch.Tensor: |
| if input_ids.shape[-1] == 0: |
| return scores |
| return self.inner(input_ids, scores) |
|
|
|
|
| class StopOnTurnEnd(StoppingCriteria): |
| """Stop when the decoded tail contains a turn marker. |
| |
| These weights never emit a stop *token*: <|im_end|> was masked out of the |
| training loss, so the model learned to spell the marker out as ordinary |
| text ("<|", "im", "_", "end", "|>") and then start a fresh turn. Passing |
| eos_token_id therefore does nothing — generation runs to max_new_tokens |
| every time, which is ~20x more tokens than the answer needs. |
| |
| Matching on decoded text rather than token ids is deliberate: how the |
| marker splits depends on what precedes it (".<|" merges into a single |
| token after a period), so an id-sequence match misses many cases. |
| """ |
|
|
| def __init__(self, tokenizer, markers=("<|im_end|>", "<|im_start|>"), window=8): |
| self.tokenizer = tokenizer |
| self.markers = markers |
| self.window = window |
|
|
| def __call__(self, input_ids: torch.Tensor, scores, **kwargs) -> bool: |
| tail = self.tokenizer.decode( |
| input_ids[0, -self.window:], skip_special_tokens=False |
| ) |
| return any(marker in tail for marker in self.markers) |
|
|
|
|
| class PixelUnshuffleProjector(nn.Module): |
| """Reduces ViT tokens 4× via PixelUnshuffle then projects to LLM dim.""" |
|
|
| def __init__(self, in_dim: int, out_dim: int, factor: int): |
| super().__init__() |
| self.factor = factor |
| self.unshuffle = nn.PixelUnshuffle(factor) |
| self.mlp = nn.Sequential( |
| nn.Linear(in_dim * factor * factor, out_dim), |
| nn.GELU(), |
| nn.Linear(out_dim, out_dim), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| B, N, D = x.shape |
| spatial = int(N ** 0.5) |
| x = x.reshape(B, spatial, spatial, D).permute(0, 3, 1, 2) |
| x = self.unshuffle(x) |
| x = x.flatten(2).transpose(1, 2) |
| return self.mlp(x) |
|
|
|
|
| class BaguettotronVLMForConditionalGeneration(PreTrainedModel): |
| """ |
| BaguettotronVLM: InternViT-300M + PixelUnshuffle projector + Baguettotron-321M. |
| |
| ~628M total parameters. The same architecture is shipped for every |
| checkpoint; only the weights and `config.chat_style` differ. |
| |
| Load with: |
| from transformers import AutoModelForImageTextToText |
| model = AutoModelForImageTextToText.from_pretrained( |
| "andreagemelli/baguettotron-vision-vqa", |
| trust_remote_code=True, |
| dtype=torch.bfloat16, |
| ) |
| """ |
|
|
| config_class = BaguettotronVLMConfig |
| _no_split_modules = ["InternVisionEncoderLayer", "LlamaDecoderLayer"] |
| |
| |
| _tied_weights_keys: list[str] = [] |
| |
| model_accepts_loss_kwargs: bool = False |
|
|
| def __init__(self, config: BaguettotronVLMConfig): |
| super().__init__(config) |
|
|
| |
| |
| |
| |
| |
| dtype = torch.get_default_dtype() |
| if dtype not in (torch.float32, torch.float16, torch.bfloat16): |
| dtype = torch.bfloat16 |
|
|
| |
| |
| |
| |
| with torch.device("cpu"): |
| self.vit = AutoModel.from_pretrained( |
| config.vit_model_id, |
| dtype=dtype, |
| low_cpu_mem_usage=True, |
| trust_remote_code=True, |
| ) |
| self.projector = PixelUnshuffleProjector( |
| in_dim=config.vit_hidden, |
| out_dim=config.llm_hidden, |
| factor=config.unshuffle_factor, |
| ) |
|
|
| tokenizer = AutoTokenizer.from_pretrained(config.llm_model_id) |
| tokenizer.add_special_tokens( |
| {"additional_special_tokens": [config.image_token, "</image>"]} |
| ) |
| raw_id = tokenizer.convert_tokens_to_ids(config.image_token) |
| self.image_token_id: int = ( |
| raw_id if isinstance(raw_id, int) else int(raw_id[0]) |
| ) |
|
|
| self.llm = AutoModelForCausalLM.from_pretrained( |
| config.llm_model_id, dtype=dtype |
| ) |
| self.llm.resize_token_embeddings(len(tokenizer), mean_resizing=False) |
| |
| |
| self.llm.lm_head.weight = nn.Parameter(self.llm.lm_head.weight.data.clone()) |
| |
| |
| |
| self.llm.config.tie_word_embeddings = False |
| self.llm._tied_weights_keys = [] |
|
|
| |
| |
| |
| |
| |
| |
|
|
| self._tokenizer = tokenizer |
|
|
| def _init_weights(self, module: nn.Module) -> None: |
| |
| |
| pass |
|
|
| |
| |
| |
|
|
| def forward( |
| self, |
| input_ids: torch.Tensor, |
| attention_mask: torch.Tensor, |
| labels: torch.Tensor | None = None, |
| pixel_values: torch.Tensor | None = None, |
| **kwargs, |
| ) -> CausalLMOutputWithPast: |
| inputs_embeds = self.llm.get_input_embeddings()(input_ids) |
|
|
| if pixel_values is not None: |
| pixel_values = pixel_values.to(dtype=self.vit.dtype, device=self.vit.device) |
| with torch.no_grad(): |
| vit_out = self.vit(pixel_values) |
| image_features = vit_out.last_hidden_state |
| if image_features.shape[1] == self.config.vit_tokens + 1: |
| image_features = image_features[:, 1:, :] |
| visual_tokens = self.projector(image_features.to(inputs_embeds.dtype)) |
| image_mask = input_ids == self.image_token_id |
| inputs_embeds[image_mask] = visual_tokens.reshape( |
| -1, self.config.llm_hidden |
| ).to(inputs_embeds.dtype) |
|
|
| return self.llm( |
| inputs_embeds=inputs_embeds, |
| attention_mask=attention_mask, |
| labels=labels, |
| return_dict=True, |
| use_cache=False, |
| ) |
|
|
| |
| |
| |
|
|
| @torch.no_grad() |
| def chat( |
| self, |
| input_ids: torch.Tensor, |
| attention_mask: torch.Tensor, |
| pixel_values: torch.Tensor | None = None, |
| max_new_tokens: int = 256, |
| repetition_penalty: float = 1.3, |
| **generate_kwargs, |
| ) -> str: |
| """Inject visual tokens, generate autoregressively, return decoded string.""" |
| inputs_embeds = self.llm.get_input_embeddings()(input_ids) |
|
|
| if pixel_values is not None: |
| pixel_values = pixel_values.to(dtype=self.vit.dtype, device=self.vit.device) |
| vit_out = self.vit(pixel_values) |
| image_features = vit_out.last_hidden_state |
| if image_features.shape[1] == self.config.vit_tokens + 1: |
| image_features = image_features[:, 1:, :] |
| visual_tokens = self.projector(image_features.to(inputs_embeds.dtype)) |
| image_mask = input_ids == self.image_token_id |
| inputs_embeds[image_mask] = visual_tokens.reshape( |
| -1, self.config.llm_hidden |
| ).to(inputs_embeds.dtype) |
|
|
| |
| |
| |
| |
| |
| |
| |
| unk_id = self._tokenizer.unk_token_id |
| candidates = ( |
| self._tokenizer.eos_token_id, |
| self._tokenizer.convert_tokens_to_ids("<|end_of_text|>"), |
| self._tokenizer.convert_tokens_to_ids("<|im_end|>"), |
| ) |
| eos_ids = list( |
| dict.fromkeys( |
| tok_id |
| for tok_id in candidates |
| if tok_id is not None and tok_id != unk_id |
| ) |
| ) |
|
|
| |
| |
| generate_kwargs.setdefault("do_sample", False) |
| generate_kwargs.setdefault( |
| "stopping_criteria", [StopOnTurnEnd(self._tokenizer)] |
| ) |
| |
| |
| if repetition_penalty != 1.0: |
| generate_kwargs.setdefault( |
| "logits_processor", [SafeRepetitionPenalty(repetition_penalty)] |
| ) |
| output_ids = self.llm.generate( |
| inputs_embeds=inputs_embeds, |
| attention_mask=attention_mask, |
| max_new_tokens=max_new_tokens, |
| eos_token_id=eos_ids, |
| **generate_kwargs, |
| ) |
| decoded = self._tokenizer.decode(output_ids[0], skip_special_tokens=False) |
| for marker in ("<|im_end|>", "<|im_start|>", "<|end_of_text|>"): |
| if marker in decoded: |
| decoded = decoded[: decoded.index(marker)] |
| return decoded.strip() |
|
|