Fix generation from the published artifact; rewrite model card
Browse files- README.md +95 -51
- chat_template.jinja +5 -0
- config.json +7 -0
- generation_config.json +12 -0
- modeling_baguettotron_vlm.py +119 -26
- special_tokens_map.json +1 -28
- tokenizer.json +4 -4
- tokenizer_config.json +5 -8
README.md
CHANGED
|
@@ -16,11 +16,9 @@ tags:
|
|
| 16 |
base_model:
|
| 17 |
- OpenGVLab/InternViT-300M-448px-V2_5
|
| 18 |
- PleIAs/Baguettotron
|
| 19 |
-
datasets:
|
| 20 |
-
- liuhaotian/LLaVA-CC3M-Pretrain-595K
|
| 21 |
---
|
| 22 |
|
| 23 |
-
#
|
| 24 |
|
| 25 |
**Baguettotron-VLM** is an open, fully-reproducible, multilingual Vision-Language
|
| 26 |
Model in the **sub-1B parameter** class. It extends
|
|
@@ -29,26 +27,24 @@ text-only reasoning SLM — with visual capabilities via the
|
|
| 29 |
[InternViT-300M-448px-V2.5](https://huggingface.co/OpenGVLab/InternViT-300M-448px-V2_5)
|
| 30 |
vision encoder and a lightweight MLP projector, for a total of ~628M parameters.
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
not chasing SOTA.
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
- **
|
| 42 |
-
|
| 43 |
-
-
|
| 44 |
-
|
| 45 |
-
- **Stage 3 — reasoning SFT (flagship)**: [andreagemelli/Baguettotron-VLM](https://huggingface.co/andreagemelli/Baguettotron-VLM)
|
| 46 |
-
(reasoning traces via R1-Vision-Reasoning-Instructions, dynamic `<think>` toggle).
|
| 47 |
|
| 48 |
-
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
-
> **
|
| 52 |
|
| 53 |
## Architecture
|
| 54 |
|
|
@@ -59,32 +55,35 @@ Image (448×448)
|
|
| 59 |
→ MLP projector (2-layer, ~2.7M) → 256 tokens × 576d
|
| 60 |
→ Interleave with text tokens
|
| 61 |
→ Baguettotron (321M, Llama arch, 80L, h=576)
|
| 62 |
-
→ Text output with <think> reasoning traces
|
| 63 |
|
| 64 |
Total: ~628M parameters
|
| 65 |
```
|
| 66 |
|
| 67 |
## Usage
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
```python
|
| 70 |
import torch
|
| 71 |
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 72 |
from PIL import Image
|
| 73 |
|
| 74 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 75 |
-
"andreagemelli/
|
| 76 |
trust_remote_code=True,
|
| 77 |
-
|
| 78 |
device_map="auto",
|
| 79 |
)
|
| 80 |
processor = AutoProcessor.from_pretrained(
|
| 81 |
-
"andreagemelli/
|
| 82 |
trust_remote_code=True,
|
| 83 |
)
|
| 84 |
|
| 85 |
image = Image.open("photo.jpg").convert("RGB")
|
| 86 |
inputs = processor(
|
| 87 |
-
messages=[{"role": "user", "content": "<image>\nDescribe the image"}],
|
| 88 |
image=image,
|
| 89 |
)
|
| 90 |
inputs = {k: v.to(model.device) for k, v in inputs.items() if v is not None}
|
|
@@ -92,13 +91,61 @@ inputs = {k: v.to(model.device) for k, v in inputs.items() if v is not None}
|
|
| 92 |
print(model.chat(**inputs))
|
| 93 |
```
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
### Chat template
|
| 96 |
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
## Training
|
| 100 |
|
| 101 |
-
| |
|
| 102 |
|---|---|
|
| 103 |
| Data | LLaVA-CC3M-Pretrain-595K (595K image-caption pairs) |
|
| 104 |
| Trainable params | ~2.7M (projector only) |
|
|
@@ -107,29 +154,26 @@ Stage 1 was trained on short image captions with no `<think>` traces. The proces
|
|
| 107 |
| Learning rate | 1e-3, cosine, 250-step warmup |
|
| 108 |
| Precision | bf16 |
|
| 109 |
| Hardware | 1× H100 SXM (RunPod) |
|
| 110 |
-
| Duration | ~5h |
|
| 111 |
-
|
| 112 |
-
##
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
to
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
- **
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
- **
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
- **
|
| 129 |
-
|
| 130 |
-
("what is happening in this image?") and less reliable at OCR, counting,
|
| 131 |
-
chart reading, or multi-hop visual reasoning. Quantitative benchmark
|
| 132 |
-
numbers will be published once the evaluation harness lands.
|
| 133 |
|
| 134 |
**Contributions and suggestions are very welcome** — issues, PRs, and ideas for
|
| 135 |
better data mixes, training recipes, or evaluation setups are all appreciated.
|
|
@@ -146,7 +190,7 @@ If you use or extend Baguettotron-VLM in your research, please cite it:
|
|
| 146 |
title = {Baguettotron-VLM: An Open, Reproducible, Multilingual Sub-1B Vision-Language Model},
|
| 147 |
author = {Gemelli, Andrea},
|
| 148 |
year = {2026},
|
| 149 |
-
howpublished = {\url{https://huggingface.co/andreagemelli/
|
| 150 |
note = {Code: \url{https://github.com/andreagemelli/baguettotron-vlm}}
|
| 151 |
}
|
| 152 |
```
|
|
@@ -154,4 +198,4 @@ If you use or extend Baguettotron-VLM in your research, please cite it:
|
|
| 154 |
|
| 155 |
## License
|
| 156 |
|
| 157 |
-
Apache 2.0 — see the [GitHub repo](https://github.com/andreagemelli/baguettotron-vlm).
|
|
|
|
| 16 |
base_model:
|
| 17 |
- OpenGVLab/InternViT-300M-448px-V2_5
|
| 18 |
- PleIAs/Baguettotron
|
|
|
|
|
|
|
| 19 |
---
|
| 20 |
|
| 21 |
+
# baguettotron-internvit-alignment
|
| 22 |
|
| 23 |
**Baguettotron-VLM** is an open, fully-reproducible, multilingual Vision-Language
|
| 24 |
Model in the **sub-1B parameter** class. It extends
|
|
|
|
| 27 |
[InternViT-300M-448px-V2.5](https://huggingface.co/OpenGVLab/InternViT-300M-448px-V2_5)
|
| 28 |
vision encoder and a lightweight MLP projector, for a total of ~628M parameters.
|
| 29 |
|
| 30 |
+
Trained end-to-end in **~72h on a single H100 (~€219)**. It inherits six European
|
| 31 |
+
languages (EN, FR, DE, ES, IT, PL) from the Baguettotron backbone, and — unusually —
|
| 32 |
+
its entire LM pretraining corpus is auditable: Baguettotron was trained on
|
| 33 |
+
[SYNTH](https://huggingface.co/datasets/PleIAs/SYNTH), ~58K Wikipedia articles expanded
|
| 34 |
+
synthetically, with no scraped web.
|
|
|
|
| 35 |
|
| 36 |
+
Two checkpoints are published:
|
| 37 |
|
| 38 |
+
- [**baguettotron-vision-vqa**](https://huggingface.co/andreagemelli/baguettotron-vision-vqa)
|
| 39 |
+
— instruction-tuned on The Cauldron. **This is the model you want.**
|
| 40 |
+
- [baguettotron-internvit-alignment](https://huggingface.co/andreagemelli/baguettotron-internvit-alignment)
|
| 41 |
+
— projector-only warmup, published so the alignment stage can be reproduced.
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
Apache 2.0. This is a proof of concept and a costed recipe, not a SOTA attempt.
|
| 44 |
+
Source: [github.com/andreagemelli/baguettotron-vlm](https://github.com/andreagemelli/baguettotron-vlm).
|
| 45 |
|
| 46 |
|
| 47 |
+
> **Alignment checkpoint.** Only the MLP projector was trained (on LLaVA-CC3M-Pretrain-595K) — the ViT and the Baguettotron LLM are unmodified base weights. It writes short captions and nothing more. Published so the alignment stage can be reproduced; for actual use take [baguettotron-vision-vqa](https://huggingface.co/andreagemelli/baguettotron-vision-vqa).
|
| 48 |
|
| 49 |
## Architecture
|
| 50 |
|
|
|
|
| 55 |
→ MLP projector (2-layer, ~2.7M) → 256 tokens × 576d
|
| 56 |
→ Interleave with text tokens
|
| 57 |
→ Baguettotron (321M, Llama arch, 80L, h=576)
|
|
|
|
| 58 |
|
| 59 |
Total: ~628M parameters
|
| 60 |
```
|
| 61 |
|
| 62 |
## Usage
|
| 63 |
|
| 64 |
+
```bash
|
| 65 |
+
pip install "transformers>=4.56,<5" torch pillow timm einops accelerate
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
```python
|
| 69 |
import torch
|
| 70 |
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 71 |
from PIL import Image
|
| 72 |
|
| 73 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 74 |
+
"andreagemelli/baguettotron-internvit-alignment",
|
| 75 |
trust_remote_code=True,
|
| 76 |
+
dtype=torch.bfloat16,
|
| 77 |
device_map="auto",
|
| 78 |
)
|
| 79 |
processor = AutoProcessor.from_pretrained(
|
| 80 |
+
"andreagemelli/baguettotron-internvit-alignment",
|
| 81 |
trust_remote_code=True,
|
| 82 |
)
|
| 83 |
|
| 84 |
image = Image.open("photo.jpg").convert("RGB")
|
| 85 |
inputs = processor(
|
| 86 |
+
messages=[{"role": "user", "content": "<image>\nDescribe the image concisely."}],
|
| 87 |
image=image,
|
| 88 |
)
|
| 89 |
inputs = {k: v.to(model.device) for k, v in inputs.items() if v is not None}
|
|
|
|
| 91 |
print(model.chat(**inputs))
|
| 92 |
```
|
| 93 |
|
| 94 |
+
`chat()` returns the answer as a plain string, already trimmed at the end of the
|
| 95 |
+
assistant turn. `max_new_tokens=...` bounds the length.
|
| 96 |
+
|
| 97 |
+
### Apple Silicon and CPU
|
| 98 |
+
|
| 99 |
+
The snippet above runs unchanged on `mps` and `cpu` — keep `device_map="auto"`, or drop
|
| 100 |
+
it and call `.to("mps")` yourself. `dtype=torch.float32` works too. A short answer takes
|
| 101 |
+
~1-3s on `mps` against ~20-25s on CPU, so prefer `mps` on a Mac.
|
| 102 |
+
|
| 103 |
+
One caveat handled for you: transformers' repetition-penalty processor corrupts the
|
| 104 |
+
first decoding step on MPS, which produced a garbage first token. `chat()` disables the
|
| 105 |
+
penalty on that backend automatically.
|
| 106 |
+
|
| 107 |
+
### Example output
|
| 108 |
+
|
| 109 |
+
Four COCO images, greedy, `Describe the image concisely.` — verbatim:
|
| 110 |
+
|
| 111 |
+
| image | output |
|
| 112 |
+
|---|---|
|
| 113 |
+
| two cats on a couch | `a cat is sleeping on the couch` |
|
| 114 |
+
| close-up of a brown bear | `the bear is a good friend.` |
|
| 115 |
+
| an upside-down STOP sign | `a sign for a stop sign` |
|
| 116 |
+
| a red double-decker bus | `the bus is a red double - decoration` |
|
| 117 |
+
|
| 118 |
+
That is the honest range of this checkpoint: it locates the subject and then drifts, which is what 2.7M trained parameters buys. It does not read the text on the sign or the bus.
|
| 119 |
+
|
| 120 |
### Chat template
|
| 121 |
|
| 122 |
+
Trained on short image captions with no `<think>` traces. The processor emits a bare assistant prefix (`<|im_start|>assistant\n`) and the model completes the caption directly. Keep prompts simple ("Describe the image").
|
| 123 |
+
|
| 124 |
+
## What the evaluation found
|
| 125 |
+
|
| 126 |
+
Measured on 6 held-out [RealWorldQA](https://huggingface.co/datasets/xai-org/RealworldQA)
|
| 127 |
+
images, judged 1–5 by a vision judge, 244 of 288 cells completed. Thin, and reported as
|
| 128 |
+
such — full tables and caveats in
|
| 129 |
+
[RESULTS.md](https://github.com/andreagemelli/baguettotron-vlm/blob/main/.planning/RESULTS.md).
|
| 130 |
+
|
| 131 |
+
- **Short factual questions are its strength** (2.5–3.5), open description its weakest
|
| 132 |
+
(~1.5). That inverted our own assumption that this would be a describer.
|
| 133 |
+
- **A third checkpoint made things worse.** A reasoning-SFT stage on top of the
|
| 134 |
+
instruction-tuned model scored 1.25 against its 2.17, roughly 3 SE. The reasoning
|
| 135 |
+
traces are undertrained for imagery and mostly add fluent hallucination, so that
|
| 136 |
+
checkpoint is **not published**. It is kept as a recorded negative result.
|
| 137 |
+
- **Decoding presets are inside noise** — greedy 2.17 vs 1.88 for the other three.
|
| 138 |
+
|
| 139 |
+
### Decoding
|
| 140 |
+
|
| 141 |
+
Use **greedy**, which is the default. It is recommended for determinism, not because
|
| 142 |
+
it beat anything. Pass `do_sample=True, temperature=...` to `chat()` if you want
|
| 143 |
+
sampling anyway.
|
| 144 |
+
|
| 145 |
|
| 146 |
+
## Training
|
| 147 |
|
| 148 |
+
| | |
|
| 149 |
|---|---|
|
| 150 |
| Data | LLaVA-CC3M-Pretrain-595K (595K image-caption pairs) |
|
| 151 |
| Trainable params | ~2.7M (projector only) |
|
|
|
|
| 154 |
| Learning rate | 1e-3, cosine, 250-step warmup |
|
| 155 |
| Precision | bf16 |
|
| 156 |
| Hardware | 1× H100 SXM (RunPod) |
|
| 157 |
+
| Duration | ~5h (~€15) |
|
| 158 |
+
|
| 159 |
+
## Limitations
|
| 160 |
+
|
| 161 |
+
- **No stop token.** `<|im_end|>` was masked out of the training loss, so the model
|
| 162 |
+
never learned to emit one — it spells the turn marker out as ordinary text instead.
|
| 163 |
+
`chat()` works around this by stopping on that text (a 512-token call drops from
|
| 164 |
+
~21s to ~1.5s), but the defect is in the weights and needs a retrain to fix.
|
| 165 |
+
- **Resolution ceiling.** One 448×448 crop → 256 visual tokens puts document text at
|
| 166 |
+
roughly 2–4 px/char. OCR, charts and documents are out of reach by architecture, not
|
| 167 |
+
by budget. Neither published checkpoint reads text in an image.
|
| 168 |
+
- **Hallucinations**, especially on fine-grained or text-heavy questions.
|
| 169 |
+
- **Undertrained for VQA alignment** — ~60h of instruction tuning on one H100 is small
|
| 170 |
+
relative to the data mixture.
|
| 171 |
+
- **Multilingual capability is inherited, not verified.** The backbone covers six
|
| 172 |
+
languages; the VLM was never evaluated on non-English benchmarks.
|
| 173 |
+
- **Evaluation is thin**: 6 held-out images, one judge, one rubric, 244 judged cells.
|
| 174 |
+
Every number here is directional.
|
| 175 |
+
- **`transformers` 5.x cannot load this model.** The blocker is upstream in InternViT's
|
| 176 |
+
remote code, which predates v5's tied-weight API. Pin `transformers<5`.
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
**Contributions and suggestions are very welcome** — issues, PRs, and ideas for
|
| 179 |
better data mixes, training recipes, or evaluation setups are all appreciated.
|
|
|
|
| 190 |
title = {Baguettotron-VLM: An Open, Reproducible, Multilingual Sub-1B Vision-Language Model},
|
| 191 |
author = {Gemelli, Andrea},
|
| 192 |
year = {2026},
|
| 193 |
+
howpublished = {\url{https://huggingface.co/andreagemelli/baguettotron-vision-vqa}},
|
| 194 |
note = {Code: \url{https://github.com/andreagemelli/baguettotron-vlm}}
|
| 195 |
}
|
| 196 |
```
|
|
|
|
| 198 |
|
| 199 |
## License
|
| 200 |
|
| 201 |
+
Apache 2.0 — see the [GitHub repo](https://github.com/andreagemelli/baguettotron-vlm).
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% for m in messages %}<|im_start|>{{ m['role'] }}
|
| 2 |
+
{{ m['content'] }}<|im_end|>
|
| 3 |
+
{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant
|
| 4 |
+
<think>
|
| 5 |
+
{% endif %}
|
config.json
CHANGED
|
@@ -11,6 +11,13 @@
|
|
| 11 |
"chat_style": "base",
|
| 12 |
"stage": 1,
|
| 13 |
"torch_dtype": "bfloat16",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"auto_map": {
|
| 15 |
"AutoConfig": "configuration_baguettotron_vlm.BaguettotronVLMConfig",
|
| 16 |
"AutoModelForImageTextToText": "modeling_baguettotron_vlm.BaguettotronVLMForConditionalGeneration",
|
|
|
|
| 11 |
"chat_style": "base",
|
| 12 |
"stage": 1,
|
| 13 |
"torch_dtype": "bfloat16",
|
| 14 |
+
"bos_token_id": 1,
|
| 15 |
+
"eos_token_id": [
|
| 16 |
+
2,
|
| 17 |
+
65492
|
| 18 |
+
],
|
| 19 |
+
"pad_token_id": 3,
|
| 20 |
+
"tie_word_embeddings": false,
|
| 21 |
"auto_map": {
|
| 22 |
"AutoConfig": "configuration_baguettotron_vlm.BaguettotronVLMConfig",
|
| 23 |
"AutoModelForImageTextToText": "modeling_baguettotron_vlm.BaguettotronVLMForConditionalGeneration",
|
generation_config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": false,
|
| 3 |
+
"bos_token_id": 1,
|
| 4 |
+
"eos_token_id": [
|
| 5 |
+
2,
|
| 6 |
+
65492
|
| 7 |
+
],
|
| 8 |
+
"pad_token_id": 3,
|
| 9 |
+
"do_sample": false,
|
| 10 |
+
"max_new_tokens": 256,
|
| 11 |
+
"transformers_version": "4.57.0"
|
| 12 |
+
}
|
modeling_baguettotron_vlm.py
CHANGED
|
@@ -8,12 +8,39 @@ from transformers import (
|
|
| 8 |
AutoModelForCausalLM,
|
| 9 |
AutoTokenizer,
|
| 10 |
PreTrainedModel,
|
|
|
|
| 11 |
)
|
| 12 |
from transformers.modeling_outputs import CausalLMOutputWithPast
|
| 13 |
|
| 14 |
from .configuration_baguettotron_vlm import BaguettotronVLMConfig
|
| 15 |
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
class PixelUnshuffleProjector(nn.Module):
|
| 18 |
"""Reduces ViT tokens 4× via PixelUnshuffle then projects to LLM dim."""
|
| 19 |
|
|
@@ -55,37 +82,69 @@ class BaguettotronVLMForConditionalGeneration(PreTrainedModel):
|
|
| 55 |
|
| 56 |
config_class = BaguettotronVLMConfig
|
| 57 |
_no_split_modules = ["InternVisionEncoderLayer", "LlamaDecoderLayer"]
|
|
|
|
|
|
|
|
|
|
| 58 |
# Tell HF Trainer not to pass num_items_in_batch (loss handled internally)
|
| 59 |
model_accepts_loss_kwargs: bool = False
|
| 60 |
|
| 61 |
def __init__(self, config: BaguettotronVLMConfig):
|
| 62 |
super().__init__(config)
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
out_dim=config.llm_hidden,
|
| 73 |
-
factor=config.unshuffle_factor,
|
| 74 |
-
)
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
self.llm.resize_token_embeddings(len(tokenizer), mean_resizing=False)
|
| 87 |
-
# Break weight tying — safetensors rejects shared-storage tensors
|
|
|
|
| 88 |
self.llm.lm_head.weight = nn.Parameter(self.llm.lm_head.weight.data.clone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
self._tokenizer = tokenizer
|
| 91 |
|
|
@@ -158,17 +217,51 @@ class BaguettotronVLMForConditionalGeneration(PreTrainedModel):
|
|
| 158 |
-1, self.config.llm_hidden
|
| 159 |
).to(inputs_embeds.dtype)
|
| 160 |
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
output_ids = self.llm.generate(
|
| 163 |
inputs_embeds=inputs_embeds,
|
| 164 |
attention_mask=attention_mask,
|
| 165 |
max_new_tokens=max_new_tokens,
|
| 166 |
-
do_sample=False,
|
| 167 |
repetition_penalty=repetition_penalty,
|
| 168 |
-
eos_token_id=
|
| 169 |
**generate_kwargs,
|
| 170 |
)
|
| 171 |
decoded = self._tokenizer.decode(output_ids[0], skip_special_tokens=False)
|
| 172 |
-
|
| 173 |
-
|
|
|
|
| 174 |
return decoded.strip()
|
|
|
|
| 8 |
AutoModelForCausalLM,
|
| 9 |
AutoTokenizer,
|
| 10 |
PreTrainedModel,
|
| 11 |
+
StoppingCriteria,
|
| 12 |
)
|
| 13 |
from transformers.modeling_outputs import CausalLMOutputWithPast
|
| 14 |
|
| 15 |
from .configuration_baguettotron_vlm import BaguettotronVLMConfig
|
| 16 |
|
| 17 |
|
| 18 |
+
class StopOnTurnEnd(StoppingCriteria):
|
| 19 |
+
"""Stop when the decoded tail contains a turn marker.
|
| 20 |
+
|
| 21 |
+
These weights never emit a stop *token*: <|im_end|> was masked out of the
|
| 22 |
+
training loss, so the model learned to spell the marker out as ordinary
|
| 23 |
+
text ("<|", "im", "_", "end", "|>") and then start a fresh turn. Passing
|
| 24 |
+
eos_token_id therefore does nothing — generation runs to max_new_tokens
|
| 25 |
+
every time, which is ~20x more tokens than the answer needs.
|
| 26 |
+
|
| 27 |
+
Matching on decoded text rather than token ids is deliberate: how the
|
| 28 |
+
marker splits depends on what precedes it (".<|" merges into a single
|
| 29 |
+
token after a period), so an id-sequence match misses many cases.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, tokenizer, markers=("<|im_end|>", "<|im_start|>"), window=8):
|
| 33 |
+
self.tokenizer = tokenizer
|
| 34 |
+
self.markers = markers
|
| 35 |
+
self.window = window
|
| 36 |
+
|
| 37 |
+
def __call__(self, input_ids: torch.Tensor, scores, **kwargs) -> bool:
|
| 38 |
+
tail = self.tokenizer.decode(
|
| 39 |
+
input_ids[0, -self.window:], skip_special_tokens=False
|
| 40 |
+
)
|
| 41 |
+
return any(marker in tail for marker in self.markers)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
class PixelUnshuffleProjector(nn.Module):
|
| 45 |
"""Reduces ViT tokens 4× via PixelUnshuffle then projects to LLM dim."""
|
| 46 |
|
|
|
|
| 82 |
|
| 83 |
config_class = BaguettotronVLMConfig
|
| 84 |
_no_split_modules = ["InternVisionEncoderLayer", "LlamaDecoderLayer"]
|
| 85 |
+
# Nothing is tied in this model — see __init__. Without this, the inherited
|
| 86 |
+
# Llama tied-weight bookkeeping drops llm.lm_head.weight during loading.
|
| 87 |
+
_tied_weights_keys: list[str] = []
|
| 88 |
# Tell HF Trainer not to pass num_items_in_batch (loss handled internally)
|
| 89 |
model_accepts_loss_kwargs: bool = False
|
| 90 |
|
| 91 |
def __init__(self, config: BaguettotronVLMConfig):
|
| 92 |
super().__init__(config)
|
| 93 |
|
| 94 |
+
# Honour the dtype the caller asked for. from_pretrained(dtype=X) makes X
|
| 95 |
+
# the default dtype for the duration of __init__, and the projector picks
|
| 96 |
+
# it up automatically. Hard-coding bfloat16 for the submodules while the
|
| 97 |
+
# projector follows the default is what made dtype=torch.float32 fail with
|
| 98 |
+
# "mat1 and mat2 must have the same dtype".
|
| 99 |
+
dtype = torch.get_default_dtype()
|
| 100 |
+
if dtype not in (torch.float32, torch.float16, torch.bfloat16):
|
| 101 |
+
dtype = torch.bfloat16
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
# The explicit CPU device context shields these nested from_pretrained
|
| 104 |
+
# calls from an outer meta-device init context (accelerate's device_map,
|
| 105 |
+
# and transformers >= 5), which otherwise aborts with "You are using
|
| 106 |
+
# from_pretrained with a meta device context manager".
|
| 107 |
+
with torch.device("cpu"):
|
| 108 |
+
self.vit = AutoModel.from_pretrained(
|
| 109 |
+
config.vit_model_id,
|
| 110 |
+
dtype=dtype,
|
| 111 |
+
low_cpu_mem_usage=True,
|
| 112 |
+
trust_remote_code=True,
|
| 113 |
+
)
|
| 114 |
+
self.projector = PixelUnshuffleProjector(
|
| 115 |
+
in_dim=config.vit_hidden,
|
| 116 |
+
out_dim=config.llm_hidden,
|
| 117 |
+
factor=config.unshuffle_factor,
|
| 118 |
+
)
|
| 119 |
|
| 120 |
+
tokenizer = AutoTokenizer.from_pretrained(config.llm_model_id)
|
| 121 |
+
tokenizer.add_special_tokens(
|
| 122 |
+
{"additional_special_tokens": [config.image_token, "</image>"]}
|
| 123 |
+
)
|
| 124 |
+
raw_id = tokenizer.convert_tokens_to_ids(config.image_token)
|
| 125 |
+
self.image_token_id: int = (
|
| 126 |
+
raw_id if isinstance(raw_id, int) else int(raw_id[0])
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
self.llm = AutoModelForCausalLM.from_pretrained(
|
| 130 |
+
config.llm_model_id, dtype=dtype
|
| 131 |
+
)
|
| 132 |
self.llm.resize_token_embeddings(len(tokenizer), mean_resizing=False)
|
| 133 |
+
# Break weight tying — safetensors rejects shared-storage tensors, and
|
| 134 |
+
# training updates lm_head independently of the input embeddings.
|
| 135 |
self.llm.lm_head.weight = nn.Parameter(self.llm.lm_head.weight.data.clone())
|
| 136 |
+
# Keep it broken. Llama declares lm_head.weight as a tied key, so
|
| 137 |
+
# from_pretrained would skip it while loading and then re-tie it to
|
| 138 |
+
# embed_tokens — silently discarding the trained output head.
|
| 139 |
+
self.llm.config.tie_word_embeddings = False
|
| 140 |
+
self.llm._tied_weights_keys = []
|
| 141 |
+
|
| 142 |
+
# NOTE: transformers >= 5 cannot load this model, and the blocker is
|
| 143 |
+
# upstream: OpenGVLab's InternViT remote code predates v5's tied-weight
|
| 144 |
+
# API, so v5 aborts inside the nested AutoModel.from_pretrained above
|
| 145 |
+
# with "'InternVisionModel' object has no attribute
|
| 146 |
+
# 'all_tied_weights_keys'". Nothing can be patched from here — the model
|
| 147 |
+
# cards pin transformers<5 until InternViT is updated.
|
| 148 |
|
| 149 |
self._tokenizer = tokenizer
|
| 150 |
|
|
|
|
| 217 |
-1, self.config.llm_hidden
|
| 218 |
).to(inputs_embeds.dtype)
|
| 219 |
|
| 220 |
+
# These weights end a turn by spelling out the literal text "<|im_end|>"
|
| 221 |
+
# as ordinary tokens and then emitting <|end_of_text|> — the special
|
| 222 |
+
# <|im_end|> token is never produced. <|end_of_text|> is therefore the id
|
| 223 |
+
# that actually stops generation; the others are listed for safety.
|
| 224 |
+
# PleIAs/Baguettotron's tokenizer declares no eos_token, so eos_token_id
|
| 225 |
+
# is None there and must be filtered out — an unfiltered [None, ...] makes
|
| 226 |
+
# generate() raise "'NoneType' object cannot be interpreted as an integer".
|
| 227 |
+
unk_id = self._tokenizer.unk_token_id
|
| 228 |
+
candidates = (
|
| 229 |
+
self._tokenizer.eos_token_id,
|
| 230 |
+
self._tokenizer.convert_tokens_to_ids("<|end_of_text|>"),
|
| 231 |
+
self._tokenizer.convert_tokens_to_ids("<|im_end|>"),
|
| 232 |
+
)
|
| 233 |
+
eos_ids = list(
|
| 234 |
+
dict.fromkeys(
|
| 235 |
+
tok_id
|
| 236 |
+
for tok_id in candidates
|
| 237 |
+
if tok_id is not None and tok_id != unk_id
|
| 238 |
+
)
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# transformers' repetition-penalty processor corrupts the very first step
|
| 242 |
+
# on MPS: generate(inputs_embeds=...) starts from an empty input_ids, and
|
| 243 |
+
# the empty-index gather/scatter zeroes the entire logits row on Metal
|
| 244 |
+
# (it is a no-op on CPU and CUDA, as it should be). The result is a
|
| 245 |
+
# garbage first token that derails the whole answer.
|
| 246 |
+
if inputs_embeds.device.type == "mps" and repetition_penalty != 1.0:
|
| 247 |
+
repetition_penalty = 1.0
|
| 248 |
+
|
| 249 |
+
# do_sample defaults to greedy but can be overridden by callers
|
| 250 |
+
# (e.g. the inference sweep) without colliding on the keyword.
|
| 251 |
+
generate_kwargs.setdefault("do_sample", False)
|
| 252 |
+
generate_kwargs.setdefault(
|
| 253 |
+
"stopping_criteria", [StopOnTurnEnd(self._tokenizer)]
|
| 254 |
+
)
|
| 255 |
output_ids = self.llm.generate(
|
| 256 |
inputs_embeds=inputs_embeds,
|
| 257 |
attention_mask=attention_mask,
|
| 258 |
max_new_tokens=max_new_tokens,
|
|
|
|
| 259 |
repetition_penalty=repetition_penalty,
|
| 260 |
+
eos_token_id=eos_ids,
|
| 261 |
**generate_kwargs,
|
| 262 |
)
|
| 263 |
decoded = self._tokenizer.decode(output_ids[0], skip_special_tokens=False)
|
| 264 |
+
for marker in ("<|im_end|>", "<|im_start|>", "<|end_of_text|>"):
|
| 265 |
+
if marker in decoded:
|
| 266 |
+
decoded = decoded[: decoded.index(marker)]
|
| 267 |
return decoded.strip()
|
special_tokens_map.json
CHANGED
|
@@ -15,32 +15,5 @@
|
|
| 15 |
"single_word": false
|
| 16 |
}
|
| 17 |
],
|
| 18 |
-
"
|
| 19 |
-
"content": "<|begin_of_text|>",
|
| 20 |
-
"lstrip": false,
|
| 21 |
-
"normalized": false,
|
| 22 |
-
"rstrip": false,
|
| 23 |
-
"single_word": false
|
| 24 |
-
},
|
| 25 |
-
"eos_token": {
|
| 26 |
-
"content": "<|end_of_text|>",
|
| 27 |
-
"lstrip": false,
|
| 28 |
-
"normalized": false,
|
| 29 |
-
"rstrip": false,
|
| 30 |
-
"single_word": false
|
| 31 |
-
},
|
| 32 |
-
"pad_token": {
|
| 33 |
-
"content": "[PAD]",
|
| 34 |
-
"lstrip": false,
|
| 35 |
-
"normalized": false,
|
| 36 |
-
"rstrip": false,
|
| 37 |
-
"single_word": false
|
| 38 |
-
},
|
| 39 |
-
"unk_token": {
|
| 40 |
-
"content": "[UNK]",
|
| 41 |
-
"lstrip": false,
|
| 42 |
-
"normalized": false,
|
| 43 |
-
"rstrip": false,
|
| 44 |
-
"single_word": false
|
| 45 |
-
}
|
| 46 |
}
|
|
|
|
| 15 |
"single_word": false
|
| 16 |
}
|
| 17 |
],
|
| 18 |
+
"eos_token": "<|end_of_text|>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
}
|
tokenizer.json
CHANGED
|
@@ -50,7 +50,7 @@
|
|
| 50 |
},
|
| 51 |
{
|
| 52 |
"id": 65492,
|
| 53 |
-
"content": "<|im_end>",
|
| 54 |
"single_word": false,
|
| 55 |
"lstrip": false,
|
| 56 |
"rstrip": false,
|
|
@@ -69,11 +69,11 @@
|
|
| 69 |
{
|
| 70 |
"id": 65494,
|
| 71 |
"content": "</think>",
|
| 72 |
-
"single_word":
|
| 73 |
"lstrip": false,
|
| 74 |
"rstrip": false,
|
| 75 |
-
"normalized":
|
| 76 |
-
"special":
|
| 77 |
},
|
| 78 |
{
|
| 79 |
"id": 65495,
|
|
|
|
| 50 |
},
|
| 51 |
{
|
| 52 |
"id": 65492,
|
| 53 |
+
"content": "<|im_end|>",
|
| 54 |
"single_word": false,
|
| 55 |
"lstrip": false,
|
| 56 |
"rstrip": false,
|
|
|
|
| 69 |
{
|
| 70 |
"id": 65494,
|
| 71 |
"content": "</think>",
|
| 72 |
+
"single_word": true,
|
| 73 |
"lstrip": false,
|
| 74 |
"rstrip": false,
|
| 75 |
+
"normalized": true,
|
| 76 |
+
"special": false
|
| 77 |
},
|
| 78 |
{
|
| 79 |
"id": 65495,
|
tokenizer_config.json
CHANGED
|
@@ -41,7 +41,7 @@
|
|
| 41 |
"special": true
|
| 42 |
},
|
| 43 |
"65492": {
|
| 44 |
-
"content": "<|im_end>",
|
| 45 |
"lstrip": false,
|
| 46 |
"normalized": false,
|
| 47 |
"rstrip": false,
|
|
@@ -59,10 +59,10 @@
|
|
| 59 |
"65494": {
|
| 60 |
"content": "</think>",
|
| 61 |
"lstrip": false,
|
| 62 |
-
"normalized":
|
| 63 |
"rstrip": false,
|
| 64 |
-
"single_word":
|
| 65 |
-
"special":
|
| 66 |
},
|
| 67 |
"65495": {
|
| 68 |
"content": "source_1",
|
|
@@ -413,12 +413,9 @@
|
|
| 413 |
"<image>",
|
| 414 |
"</image>"
|
| 415 |
],
|
| 416 |
-
"bos_token": "<|begin_of_text|>",
|
| 417 |
"clean_up_tokenization_spaces": true,
|
| 418 |
"eos_token": "<|end_of_text|>",
|
| 419 |
"extra_special_tokens": {},
|
| 420 |
"model_max_length": 1000000000000000019884624838656,
|
| 421 |
-
"
|
| 422 |
-
"tokenizer_class": "PreTrainedTokenizerFast",
|
| 423 |
-
"unk_token": "[UNK]"
|
| 424 |
}
|
|
|
|
| 41 |
"special": true
|
| 42 |
},
|
| 43 |
"65492": {
|
| 44 |
+
"content": "<|im_end|>",
|
| 45 |
"lstrip": false,
|
| 46 |
"normalized": false,
|
| 47 |
"rstrip": false,
|
|
|
|
| 59 |
"65494": {
|
| 60 |
"content": "</think>",
|
| 61 |
"lstrip": false,
|
| 62 |
+
"normalized": true,
|
| 63 |
"rstrip": false,
|
| 64 |
+
"single_word": true,
|
| 65 |
+
"special": false
|
| 66 |
},
|
| 67 |
"65495": {
|
| 68 |
"content": "source_1",
|
|
|
|
| 413 |
"<image>",
|
| 414 |
"</image>"
|
| 415 |
],
|
|
|
|
| 416 |
"clean_up_tokenization_spaces": true,
|
| 417 |
"eos_token": "<|end_of_text|>",
|
| 418 |
"extra_special_tokens": {},
|
| 419 |
"model_max_length": 1000000000000000019884624838656,
|
| 420 |
+
"tokenizer_class": "PreTrainedTokenizerFast"
|
|
|
|
|
|
|
| 421 |
}
|