File size: 12,342 Bytes
1727faf
 
 
 
 
 
 
 
 
2c4010c
1727faf
f7e9c38
1727faf
2c4010c
1727faf
 
 
 
 
2c4010c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f7e9c38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1727faf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b66671e
 
1727faf
 
 
 
b66671e
1727faf
b66671e
1727faf
 
 
 
 
f7e9c38
 
 
1727faf
 
 
 
 
 
f7e9c38
 
 
 
 
 
 
 
1727faf
f7e9c38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1727faf
f7e9c38
 
 
 
 
 
 
 
 
 
 
 
9b486d0
f7e9c38
 
1727faf
f7e9c38
 
 
 
 
 
 
 
 
 
 
 
1727faf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b486d0
1727faf
 
 
 
 
9b486d0
1727faf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b486d0
1727faf
 
 
 
9b486d0
1727faf
 
 
 
 
f7e9c38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c4010c
 
 
 
 
 
1727faf
 
 
 
f7e9c38
1727faf
 
 
f7e9c38
 
 
1727faf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
"""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"]
    # Nothing is tied in this model β€” see __init__. Without this, the inherited
    # Llama tied-weight bookkeeping drops llm.lm_head.weight during loading.
    _tied_weights_keys: list[str] = []
    # Tell HF Trainer not to pass num_items_in_batch (loss handled internally)
    model_accepts_loss_kwargs: bool = False

    def __init__(self, config: BaguettotronVLMConfig):
        super().__init__(config)

        # Honour the dtype the caller asked for. from_pretrained(dtype=X) makes X
        # the default dtype for the duration of __init__, and the projector picks
        # it up automatically. Hard-coding bfloat16 for the submodules while the
        # projector follows the default is what made dtype=torch.float32 fail with
        # "mat1 and mat2 must have the same dtype".
        dtype = torch.get_default_dtype()
        if dtype not in (torch.float32, torch.float16, torch.bfloat16):
            dtype = torch.bfloat16

        # The explicit CPU device context shields these nested from_pretrained
        # calls from an outer meta-device init context (accelerate's device_map,
        # and transformers >= 5), which otherwise aborts with "You are using
        # from_pretrained with a meta device context manager".
        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)
        # Break weight tying β€” safetensors rejects shared-storage tensors, and
        # training updates lm_head independently of the input embeddings.
        self.llm.lm_head.weight = nn.Parameter(self.llm.lm_head.weight.data.clone())
        # Keep it broken. Llama declares lm_head.weight as a tied key, so
        # from_pretrained would skip it while loading and then re-tie it to
        # embed_tokens β€” silently discarding the trained output head.
        self.llm.config.tie_word_embeddings = False
        self.llm._tied_weights_keys = []

        # NOTE: transformers >= 5 cannot load this model, and the blocker is
        # upstream: OpenGVLab's InternViT remote code predates v5's tied-weight
        # API, so v5 aborts inside the nested AutoModel.from_pretrained above
        # with "'InternVisionModel' object has no attribute
        # 'all_tied_weights_keys'". Nothing can be patched from here β€” the model
        # cards pin transformers<5 until InternViT is updated.

        self._tokenizer = tokenizer

    def _init_weights(self, module: nn.Module) -> None:
        # Pretrained components are initialised from their respective hubs;
        # the projector weights come from the saved checkpoint β€” skip random init.
        pass

    # ------------------------------------------------------------------
    # Training interface
    # ------------------------------------------------------------------

    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,
        )

    # ------------------------------------------------------------------
    # Inference interface
    # ------------------------------------------------------------------

    @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)

        # These weights end a turn by spelling out the literal text "<|im_end|>"
        # as ordinary tokens and then emitting <|end_of_text|> β€” the special
        # <|im_end|> token is never produced. <|end_of_text|> is therefore the id
        # that actually stops generation; the others are listed for safety.
        # PleIAs/Baguettotron's tokenizer declares no eos_token, so eos_token_id
        # is None there and must be filtered out β€” an unfiltered [None, ...] makes
        # generate() raise "'NoneType' object cannot be interpreted as an integer".
        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
            )
        )

        # do_sample defaults to greedy but can be overridden by callers
        # (e.g. the inference sweep) without colliding on the keyword.
        generate_kwargs.setdefault("do_sample", False)
        generate_kwargs.setdefault(
            "stopping_criteria", [StopOnTurnEnd(self._tokenizer)]
        )
        # The penalty goes through SafeRepetitionPenalty rather than generate()'s
        # repetition_penalty kwarg, so the first decoding step is skipped.
        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()