GautamKishore commited on
Commit
e2b4d4a
·
verified ·
1 Parent(s): 352797b

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,4 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ *.pt filter=lfs diff=lfs merge=lfs -text
3
+ *.onnx filter=lfs diff=lfs merge=lfs -text
4
+ *.gguf filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - vision-language-model
7
+ - document-understanding
8
+ - edge-ai
9
+ - tinydoc
10
+ pipeline_tag: document-question-answering
11
+ inference:
12
+ parameters:
13
+ max_new_tokens: 512
14
+ widget:
15
+ - src: https://huggingface.co/datasets/merve/table-test/resolve/main/table.png
16
+ text: "What is the total?"
17
+ ---
18
+
19
+ # TinyDoc-VLM-256M
20
+
21
+ The world's smallest document-specialist VLM (256M parameters) that runs on edge devices.
22
+
23
+ - **Vision Encoder**: SigLIP-B/16 (93M)
24
+ - **Connector**: Pixel-Shuffle Token Compressor (3× scale, 9× compression)
25
+ - **Decoder**: SmolLM2-135M
26
+ - **Context Length**: 8192 tokens
27
+ - **Training**: 3-stage (layout pretrain → doc understanding → instruction tuning)
28
+
29
+ ## Features
30
+
31
+ - 🏎️ **Fast**: >100 tok/s on CPU, <1GB VRAM on GPU
32
+ - 📄 **Multi-task**: JSON extraction, KV extraction, table parsing, OCR, QA
33
+ - 🍃 **Lightweight**: 1.1GB on disk, runs on Raspberry Pi
34
+ - 🔌 **Plug & Play**: Works with HuggingFace `transformers` and the TinyDoc Python SDK
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from PIL import Image
40
+ from tinydoc import TinyDocExtractor
41
+
42
+ extractor = TinyDocExtractor(model_name="GautamKishore/TinyDoc-VLM-256M")
43
+
44
+ # Ask a question
45
+ img = Image.open("invoice.png")
46
+ result = extractor.ask(img, "What is the total?")
47
+ print(result.answer)
48
+
49
+ # Extract JSON fields
50
+ fields = extractor.extract(img, output_format="json")
51
+ print(fields.fields)
52
+
53
+ # Extract table
54
+ table = extractor.extract_table(img)
55
+ print(table.markdown)
56
+ ```
57
+
58
+ ## Model Architecture
59
+
60
+ ```
61
+ Image (384×384)
62
+ ↓
63
+ SigLIP Vision Encoder (93M)
64
+ ↓ 576 patches × 768 dim
65
+ Pixel-Shuffle Compressor (×3)
66
+ ↓ 64 tokens × 576 dim
67
+ SmolLM2 Decoder (135M)
68
+ ↓
69
+ Multi-Task Output Heads
70
+ ↓
71
+ JSON / KV / Table / OCR / QA
72
+ ```
73
+
74
+ ## Benchmark Results
75
+
76
+ | Task | Metric | Score |
77
+ |------|--------|-------|
78
+ | DocVQA | ANLS | Coming soon |
79
+ | FUNSD | F1 | Coming soon |
80
+ | CORD | F1 | Coming soon |
81
+ | PubTabNet | TEDS | Coming soon |
82
+
83
+ ## Deployment
84
+
85
+ Export to ONNX or GGUF for edge deployment:
86
+
87
+ ```bash
88
+ # ONNX
89
+ python -m tinydoc_vlm.export_onnx --model GautamKishore/TinyDoc-VLM-256M
90
+
91
+ # GGUF
92
+ python -m tinydoc_vlm.export_gguf --model GautamKishore/TinyDoc-VLM-256M
93
+ ```
94
+
95
+ ## Colab Training
96
+
97
+ Train your own: [TinyDoc-VLM Colab Notebook](https://github.com/eulogik/TinyDoc-VLM/blob/main/training/tinydoc_colab_training.ipynb)
attention.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ def get_2d_sincos_pos_embed(embed_dim: int, grid_size: int, cls_token: bool = False) -> torch.Tensor:
4
+ """
5
+ Generate 2D sinusoidal positional embeddings.
6
+
7
+ Args:
8
+ embed_dim: Dimension of the embedding (must be even)
9
+ grid_size: Height/Width of the grid (assumed square)
10
+ cls_token: If True, prepends a zero embedding for the class token
11
+
12
+ Returns:
13
+ pos_embed: shape (grid_size * grid_size, embed_dim) or (1 + grid_size * grid_size, embed_dim)
14
+ """
15
+ grid_h = torch.arange(grid_size, dtype=torch.float32)
16
+ grid_w = torch.arange(grid_size, dtype=torch.float32)
17
+
18
+ # Create coordinate grid
19
+ grid = torch.meshgrid(grid_h, grid_w, indexing="ij")
20
+ grid = torch.stack(grid, dim=0) # shape (2, grid_size, grid_size)
21
+ grid = grid.reshape(2, 1, grid_size, grid_size)
22
+
23
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
24
+
25
+ if cls_token:
26
+ # Prepend a zero embedding for CLS token
27
+ pos_embed = torch.cat([torch.zeros([1, embed_dim]), pos_embed], dim=0)
28
+
29
+ return pos_embed
30
+
31
+ def get_2d_sincos_pos_embed_from_grid(embed_dim: int, grid: torch.Tensor) -> torch.Tensor:
32
+ """
33
+ Helper function to generate embeddings from a coordinate grid.
34
+
35
+ Args:
36
+ embed_dim: Dimension of the embedding
37
+ grid: shape (2, 1, grid_h, grid_w) where index 0 is Y, index 1 is X
38
+
39
+ Returns:
40
+ emb: shape (grid_h * grid_w, embed_dim)
41
+ """
42
+ assert embed_dim % 2 == 0
43
+
44
+ # Use half of dimensions for X, half for Y
45
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # Y coords
46
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # X coords
47
+
48
+ emb = torch.cat([emb_h, emb_w], dim=1) # shape (grid_h * grid_w, embed_dim)
49
+ return emb
50
+
51
+ def get_1d_sincos_pos_embed_from_grid(embed_dim: int, pos: torch.Tensor) -> torch.Tensor:
52
+ """
53
+ Generate 1D sinusoidal positional embeddings for a coordinate tensor.
54
+
55
+ Args:
56
+ embed_dim: Dimension of the embedding
57
+ pos: Coordinate tensor of shape (1, H, W) or (L,)
58
+
59
+ Returns:
60
+ emb: shape (H * W, embed_dim) or (L, embed_dim)
61
+ """
62
+ assert embed_dim % 2 == 0
63
+
64
+ # omega = 1 / (10000 ** (2i / d))
65
+ omega = torch.arange(embed_dim // 2, dtype=torch.float32)
66
+ omega /= embed_dim / 2.0
67
+ omega = 1.0 / (10000 ** omega) # shape (embed_dim // 2,)
68
+
69
+ pos = pos.reshape(-1) # Flatten spatial dims to 1D sequence
70
+ out = torch.outer(pos, omega) # shape (seq_len, embed_dim // 2)
71
+
72
+ emb_sine = torch.sin(out)
73
+ emb_cosine = torch.cos(out)
74
+
75
+ emb = torch.cat([emb_sine, emb_cosine], dim=1) # shape (seq_len, embed_dim)
76
+ return emb
chat_template.jinja ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system
2
+ You are a helpful AI assistant named SmolLM, trained by Hugging Face<|im_end|>
3
+ ' }}{% endif %}{{'<|im_start|>' + message['role'] + '
4
+ ' + message['content'] + '<|im_end|>' + '
5
+ '}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant
6
+ ' }}{% endif %}
config.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "TinyDocVLMForConditionalGeneration"
4
+ ],
5
+ "decoder_config": {
6
+ "attention_bias": false,
7
+ "attention_dropout": 0.0,
8
+ "bos_token_id": 1,
9
+ "eos_token_id": 2,
10
+ "head_dim": 64,
11
+ "hidden_act": "silu",
12
+ "hidden_size": 576,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 1536,
15
+ "max_position_embeddings": 8192,
16
+ "mlp_bias": false,
17
+ "model_type": "llama",
18
+ "num_attention_heads": 9,
19
+ "num_hidden_layers": 30,
20
+ "num_key_value_heads": 3,
21
+ "pad_token_id": null,
22
+ "pretraining_tp": 1,
23
+ "rms_norm_eps": 1e-06,
24
+ "rope_parameters": {
25
+ "rope_theta": 273000.0,
26
+ "rope_type": "default"
27
+ },
28
+ "tie_word_embeddings": false,
29
+ "use_cache": true,
30
+ "vocab_size": 49184
31
+ },
32
+ "dtype": "float32",
33
+ "image_size": 384,
34
+ "model_type": "tinydoc_vlm",
35
+ "patch_size": 16,
36
+ "pixel_shuffle_scale": 3,
37
+ "transformers_version": "5.10.2",
38
+ "vision_config": {
39
+ "attention_dropout": 0.0,
40
+ "hidden_act": "gelu_pytorch_tanh",
41
+ "hidden_size": 768,
42
+ "image_size": 384,
43
+ "intermediate_size": 3072,
44
+ "layer_norm_eps": 1e-06,
45
+ "model_type": "siglip_vision_model",
46
+ "num_attention_heads": 12,
47
+ "num_channels": 3,
48
+ "num_hidden_layers": 12,
49
+ "patch_size": 16
50
+ }
51
+ }
configuration.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, Union
2
+ from transformers import PretrainedConfig, AutoConfig
3
+
4
+ class TinyDocVLMConfig(PretrainedConfig):
5
+ model_type = "tinydoc_vlm"
6
+ is_composition = True
7
+
8
+ def __init__(
9
+ self,
10
+ vision_config: Union[Dict[str, Any], PretrainedConfig] = None,
11
+ decoder_config: Union[Dict[str, Any], PretrainedConfig] = None,
12
+ pixel_shuffle_scale: int = 3,
13
+ image_size: int = 384,
14
+ patch_size: int = 16,
15
+ **kwargs,
16
+ ):
17
+ super().__init__(**kwargs)
18
+
19
+ # Set defaults if not provided
20
+ if vision_config is None:
21
+ # Default SigLIP-B/16-like configuration (approx 93M parameters)
22
+ vision_config = {
23
+ "model_type": "siglip_vision_model",
24
+ "hidden_size": 768,
25
+ "intermediate_size": 3072,
26
+ "num_hidden_layers": 12,
27
+ "num_attention_heads": 12,
28
+ "patch_size": 16,
29
+ "image_size": 384,
30
+ "num_channels": 3,
31
+ "layer_norm_eps": 1e-6,
32
+ }
33
+
34
+ if decoder_config is None:
35
+ # Default SmolLM2-135M-like configuration (approx 135M parameters)
36
+ decoder_config = {
37
+ "model_type": "llama",
38
+ "vocab_size": 49152,
39
+ "hidden_size": 576,
40
+ "intermediate_size": 1536,
41
+ "num_hidden_layers": 30,
42
+ "num_attention_heads": 9,
43
+ "num_key_value_heads": 3,
44
+ "max_position_embeddings": 8192,
45
+ "rms_norm_eps": 1e-5,
46
+ "rope_theta": 273000.0,
47
+ "attention_bias": False,
48
+ }
49
+
50
+ # Initialize config objects
51
+ if isinstance(vision_config, dict):
52
+ vision_config_copy = vision_config.copy()
53
+ vision_model_type = vision_config_copy.pop("model_type", "siglip_vision_model")
54
+ self.vision_config = AutoConfig.for_model(vision_model_type, **vision_config_copy)
55
+ else:
56
+ self.vision_config = vision_config
57
+
58
+ if isinstance(decoder_config, dict):
59
+ decoder_config_copy = decoder_config.copy()
60
+ decoder_model_type = decoder_config_copy.pop("model_type", "llama")
61
+ self.decoder_config = AutoConfig.for_model(decoder_model_type, **decoder_config_copy)
62
+ else:
63
+ self.decoder_config = decoder_config
64
+
65
+ self.pixel_shuffle_scale = pixel_shuffle_scale
66
+ self.image_size = image_size
67
+ self.patch_size = patch_size
68
+
69
+ def __getattr__(self, name):
70
+ if name in ('decoder_config', 'vision_config'):
71
+ raise AttributeError(name)
72
+ if 'decoder_config' in self.__dict__:
73
+ try:
74
+ return getattr(self.decoder_config, name)
75
+ except AttributeError:
76
+ pass
77
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
78
+
79
+ def to_dict(self) -> Dict[str, Any]:
80
+ output = super().to_dict()
81
+ output["vision_config"] = self.vision_config.to_dict()
82
+ output["decoder_config"] = self.decoder_config.to_dict()
83
+ return output
decoder.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from typing import Optional, List
4
+ from transformers import LlamaForCausalLM, LlamaConfig
5
+
6
+ class TinyDocDecoder(nn.Module):
7
+ """
8
+ Decoder wrapper around LlamaForCausalLM (used by SmolLM2).
9
+ Manages loading and vocabulary/embedding resizing for special tokens.
10
+ """
11
+ def __init__(self, config: LlamaConfig):
12
+ super().__init__()
13
+ self.config = config
14
+ self.lm = LlamaForCausalLM(config)
15
+ self.hidden_size = config.hidden_size
16
+
17
+ def resize_token_embeddings(self, new_num_tokens: int) -> nn.Embedding:
18
+ """
19
+ Resizes input token embeddings and output LM head of the decoder.
20
+ """
21
+ resized = self.lm.resize_token_embeddings(new_num_tokens)
22
+ self.config.vocab_size = new_num_tokens
23
+ return resized
24
+
25
+ def get_input_embeddings(self) -> nn.Module:
26
+ return self.lm.get_input_embeddings()
27
+
28
+ def forward(
29
+ self,
30
+ input_ids: Optional[torch.LongTensor] = None,
31
+ attention_mask: Optional[torch.Tensor] = None,
32
+ position_ids: Optional[torch.LongTensor] = None,
33
+ past_key_values: Optional[List[torch.FloatTensor]] = None,
34
+ inputs_embeds: Optional[torch.FloatTensor] = None,
35
+ labels: Optional[torch.LongTensor] = None,
36
+ use_cache: Optional[bool] = None,
37
+ output_attentions: Optional[bool] = None,
38
+ output_hidden_states: Optional[bool] = None,
39
+ return_dict: Optional[bool] = None,
40
+ ):
41
+ return self.lm(
42
+ input_ids=input_ids,
43
+ attention_mask=attention_mask,
44
+ position_ids=position_ids,
45
+ past_key_values=past_key_values,
46
+ inputs_embeds=inputs_embeds,
47
+ labels=labels,
48
+ use_cache=use_cache,
49
+ output_attentions=output_attentions,
50
+ output_hidden_states=output_hidden_states,
51
+ return_dict=return_dict,
52
+ )
generation_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "output_attentions": false,
4
+ "output_hidden_states": false,
5
+ "transformers_version": "5.10.2"
6
+ }
image_processing.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from PIL import Image
3
+ from typing import Optional, List
4
+ import torch
5
+ import torchvision.transforms as T
6
+ from transformers.image_processing_base import ImageProcessingMixin
7
+
8
+ class TinyDocImageProcessor(ImageProcessingMixin):
9
+ """
10
+ Image processor for TinyDoc-VLM.
11
+ Handles resizing, normalization, and optional tiling (splitting) of document images.
12
+ """
13
+ def __init__(
14
+ self,
15
+ image_size: int = 384,
16
+ mean: Optional[List[float]] = None,
17
+ std: Optional[List[float]] = None,
18
+ tiling_mode: str = "auto", # "none", "auto" (split if large)
19
+ **kwargs,
20
+ ):
21
+ self.image_size = image_size
22
+ self.mean = mean or [0.5, 0.5, 0.5]
23
+ self.std = std or [0.5, 0.5, 0.5]
24
+ self.tiling_mode = tiling_mode
25
+
26
+ super().__init__(**kwargs)
27
+
28
+ # Base torchvision transforms for single tile
29
+ self.transform = T.Compose([
30
+ T.ToTensor(),
31
+ T.Normalize(mean=self.mean, std=self.std)
32
+ ])
33
+
34
+ def preprocess(
35
+ self,
36
+ image: Image.Image,
37
+ return_tensors: str = "pt"
38
+ ) -> torch.Tensor:
39
+ """
40
+ Preprocesses a PIL Image into a multi-tile float tensor.
41
+
42
+ Returns shape: (num_tiles, 3, image_size, image_size)
43
+ """
44
+ # Ensure RGB
45
+ if image.mode != "RGB":
46
+ image = image.convert("RGB")
47
+
48
+ w, h = image.size
49
+
50
+ if self.tiling_mode == "none" or (w <= self.image_size and h <= self.image_size):
51
+ # No tiling needed: resize to image_size x image_size and return single tile
52
+ resized = image.resize((self.image_size, self.image_size), Image.Resampling.BILINEAR)
53
+ tile_tensor = self.transform(resized) # shape (3, image_size, image_size)
54
+ # Add tile batch dimension: shape (1, 3, image_size, image_size)
55
+ return tile_tensor.unsqueeze(0)
56
+
57
+ # Tiling mode 'auto': split high-res image into a grid of image_size x image_size tiles,
58
+ # plus a downscaled overview thumbnail.
59
+
60
+ # Calculate how many tiles we need
61
+ cols = int(np.ceil(w / self.image_size))
62
+ rows = int(np.ceil(h / self.image_size))
63
+
64
+ # Limit grid size to prevent excessive memory usage (max 2x2 grid = 4 tiles)
65
+ cols = min(cols, 2)
66
+ rows = min(rows, 2)
67
+
68
+ # Target size for the tiling grid
69
+ target_w = cols * self.image_size
70
+ target_h = rows * self.image_size
71
+
72
+ # Resize original image to fit the target grid shape (maintaining proportions via padding)
73
+ resized_full = self._resize_and_pad(image, target_w, target_h)
74
+
75
+ tiles = []
76
+
77
+ # 1. Generate thumbnail/overview of the full image
78
+ thumbnail = image.resize((self.image_size, self.image_size), Image.Resampling.BILINEAR)
79
+ tiles.append(self.transform(thumbnail))
80
+
81
+ # 2. Extract tiles from the grid
82
+ for r in range(rows):
83
+ for c in range(cols):
84
+ box = (
85
+ c * self.image_size,
86
+ r * self.image_size,
87
+ (c + 1) * self.image_size,
88
+ (r + 1) * self.image_size
89
+ )
90
+ tile = resized_full.crop(box)
91
+ tiles.append(self.transform(tile))
92
+
93
+ # Stack tiles along a new dimension: shape (num_tiles, 3, image_size, image_size)
94
+ # where num_tiles = 1 (overview) + rows * cols
95
+ stacked_tiles = torch.stack(tiles, dim=0)
96
+ return stacked_tiles
97
+
98
+ def _resize_and_pad(self, img: Image.Image, target_w: int, target_h: int) -> Image.Image:
99
+ """
100
+ Resizes and pads an image to target dimensions while maintaining aspect ratio.
101
+ """
102
+ # Calculate aspect ratio
103
+ w, h = img.size
104
+ ratio = min(target_w / w, target_h / h)
105
+ new_w = int(w * ratio)
106
+ new_h = int(h * ratio)
107
+
108
+ resized = img.resize((new_w, new_h), Image.Resampling.BILINEAR)
109
+
110
+ # Create a new padded background image
111
+ padded = Image.new("RGB", (target_w, target_h), (255, 255, 255))
112
+ # Center the resized image
113
+ x_offset = (target_w - new_w) // 2
114
+ y_offset = (target_h - new_h) // 2
115
+ padded.paste(resized, (x_offset, y_offset))
116
+
117
+ return padded
losses.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ from typing import Dict, Optional
5
+
6
+
7
+ def ce_loss(logits: torch.Tensor, labels: torch.Tensor, ignore_index: int = -100) -> torch.Tensor:
8
+ return F.cross_entropy(logits.view(-1, logits.size(-1)), labels.view(-1), ignore_index=ignore_index)
9
+
10
+
11
+ def kv_loss(kv_outputs: Dict[str, torch.Tensor], key_labels: torch.Tensor, confidence_labels: torch.Tensor) -> torch.Tensor:
12
+ key_ce = F.cross_entropy(kv_outputs["key_logits"].view(-1, kv_outputs["key_logits"].size(-1)), key_labels.view(-1))
13
+ conf_bce = F.binary_cross_entropy(kv_outputs["confidence"].view(-1), confidence_labels.view(-1))
14
+ return key_ce + conf_bce
15
+
16
+
17
+ class CombinedLoss(nn.Module):
18
+ """
19
+ Combines multiple task-specific losses for multi-stage training.
20
+ Stage 1: layout + OCR + region
21
+ Stage 2: QA + JSON + KV + table
22
+ Stage 3: standard LM
23
+ """
24
+ def __init__(self, stage: int = 1):
25
+ super().__init__()
26
+ self.stage = stage
27
+
28
+ def forward(
29
+ self,
30
+ lm_logits: torch.Tensor,
31
+ lm_labels: torch.Tensor,
32
+ head_outputs: Optional[Dict[str, torch.Tensor]] = None,
33
+ head_labels: Optional[Dict[str, torch.Tensor]] = None,
34
+ ) -> Dict[str, torch.Tensor]:
35
+ losses = {}
36
+ total_loss = torch.tensor(0.0, device=lm_logits.device)
37
+
38
+ lm_loss = ce_loss(lm_logits, lm_labels)
39
+ losses["lm_loss"] = lm_loss
40
+
41
+ if self.stage == 1:
42
+ total_loss = lm_loss
43
+ elif self.stage == 2:
44
+ total_loss = lm_loss
45
+ if head_outputs and head_labels:
46
+ if "json_logits" in head_outputs and "json_labels" in head_labels:
47
+ json_loss = ce_loss(head_outputs["json_logits"], head_labels["json_labels"])
48
+ losses["json_loss"] = json_loss
49
+ total_loss = total_loss + json_loss
50
+
51
+ if "kv" in head_outputs and "kv_key_labels" in head_labels:
52
+ kv = kv_loss(head_outputs["kv"], head_labels["kv_key_labels"], head_labels["kv_conf_labels"])
53
+ losses["kv_loss"] = kv
54
+ total_loss = total_loss + kv
55
+
56
+ if "table_logits" in head_outputs and "table_labels" in head_labels:
57
+ table_loss = ce_loss(head_outputs["table_logits"], head_labels["table_labels"])
58
+ losses["table_loss"] = table_loss
59
+ total_loss = total_loss + table_loss
60
+ else:
61
+ total_loss = lm_loss
62
+
63
+ losses["loss"] = total_loss
64
+ return losses
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:475d89b64afb55d72000ff553fd24b704c01c8c6e72837e80c65a507c411a0e1
3
+ size 1162040324
modeling.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from typing import Dict, Any, Optional, List, Tuple, Union
4
+ from transformers import PreTrainedModel, GenerationMixin
5
+ from transformers.modeling_outputs import CausalLMOutputWithPast
6
+
7
+ from .configuration import TinyDocVLMConfig
8
+ from .vision_encoder import SigLIPVisionEncoder
9
+ from .token_compressor import PixelShuffleTokenCompressor
10
+ from .decoder import TinyDocDecoder
11
+ from .output_heads import MultiTaskOutputHeads
12
+
13
+ class TinyDocVLMPreTrainedModel(PreTrainedModel):
14
+ config_class = TinyDocVLMConfig
15
+ base_model_prefix = "tinydoc_vlm"
16
+ supports_gradient_checkpointing = True
17
+
18
+ def _init_weights(self, module):
19
+ std = getattr(self.config, "initializer_range", 0.02)
20
+ if isinstance(module, nn.Linear):
21
+ module.weight.data.normal_(mean=0.0, std=std)
22
+ if module.bias is not None:
23
+ module.bias.data.zero_()
24
+ elif isinstance(module, nn.Embedding):
25
+ module.weight.data.normal_(mean=0.0, std=std)
26
+ if module.padding_idx is not None:
27
+ module.weight.data[module.padding_idx].zero_()
28
+
29
+ class TinyDocVLMForConditionalGeneration(TinyDocVLMPreTrainedModel, GenerationMixin):
30
+ """
31
+ TinyDoc-VLM: The World's Smallest Document Understanding Model.
32
+ Coordinates SigLIP Vision Encoder, PixelShuffle Compressor, and SmolLM2 Decoder.
33
+ """
34
+ def __init__(self, config: TinyDocVLMConfig):
35
+ super().__init__(config)
36
+
37
+ # 1. Vision Encoder
38
+ self.vision_encoder = SigLIPVisionEncoder(config)
39
+
40
+ # 2. Token Compressor / Connector
41
+ self.compressor = PixelShuffleTokenCompressor(
42
+ config,
43
+ encoder_dim=config.vision_config.hidden_size,
44
+ decoder_dim=config.decoder_config.hidden_size
45
+ )
46
+
47
+ # 3. Decoder
48
+ self.decoder = TinyDocDecoder(config.decoder_config)
49
+
50
+ # Learnable image pad / placeholder token ID
51
+ self.image_token_id = getattr(config, "image_token_id", 49153)
52
+
53
+ # 2D Positional Embeddings for visual features (added to tokens before projection)
54
+ s = config.pixel_shuffle_scale
55
+ compressed_grid_size = (config.image_size // config.patch_size) // s
56
+ compressed_patches = compressed_grid_size ** 2
57
+
58
+ # Learnable 2D positional embeddings for the compressed visual tokens
59
+ self.visual_pos_embed = nn.Parameter(
60
+ torch.zeros(1, 1, compressed_patches, config.decoder_config.hidden_size)
61
+ )
62
+
63
+ # 4. Structured Output Heads (multi-task)
64
+ self.output_heads = MultiTaskOutputHeads(
65
+ hidden_size=config.decoder_config.hidden_size,
66
+ vocab_size=config.decoder_config.vocab_size,
67
+ )
68
+
69
+ # Initialize weights
70
+ self.post_init()
71
+
72
+ def get_input_embeddings(self) -> nn.Module:
73
+ return self.decoder.get_input_embeddings()
74
+
75
+ def set_input_embeddings(self, value):
76
+ self.decoder.lm.set_input_embeddings(value)
77
+
78
+ def forward(
79
+ self,
80
+ input_ids: Optional[torch.LongTensor] = None,
81
+ pixel_values: Optional[torch.FloatTensor] = None,
82
+ attention_mask: Optional[torch.Tensor] = None,
83
+ position_ids: Optional[torch.LongTensor] = None,
84
+ past_key_values: Optional[List[torch.FloatTensor]] = None,
85
+ inputs_embeds: Optional[torch.FloatTensor] = None,
86
+ labels: Optional[torch.LongTensor] = None,
87
+ use_cache: Optional[bool] = None,
88
+ output_attentions: Optional[bool] = None,
89
+ output_hidden_states: Optional[bool] = None,
90
+ return_dict: Optional[bool] = None,
91
+ task: Optional[str] = None,
92
+ ) -> Union[Tuple, Dict, CausalLMOutputWithPast]:
93
+
94
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
95
+ output_hidden_states = True if task else output_hidden_states
96
+
97
+ # Decoding pass (no new visual input, reuse cached states)
98
+ if pixel_values is None and past_key_values is not None:
99
+ outputs = self.decoder(
100
+ input_ids=input_ids,
101
+ attention_mask=attention_mask,
102
+ position_ids=position_ids,
103
+ past_key_values=past_key_values,
104
+ inputs_embeds=inputs_embeds,
105
+ labels=labels,
106
+ use_cache=use_cache,
107
+ output_attentions=output_attentions,
108
+ output_hidden_states=output_hidden_states,
109
+ return_dict=return_dict,
110
+ )
111
+ if task:
112
+ hidden = outputs.hidden_states[-1] if hasattr(outputs, "hidden_states") else outputs[2]
113
+ head_outputs = self.output_heads(hidden, task=task)
114
+ return {"lm_outputs": outputs, "head_outputs": head_outputs}
115
+ return outputs
116
+
117
+ # Prefill pass: merge text and visual tokens into inputs_embeds
118
+ if inputs_embeds is None:
119
+ inputs_embeds = self.decoder.get_input_embeddings()(input_ids)
120
+
121
+ if pixel_values is not None:
122
+ visual_features = self.vision_encoder(pixel_values)
123
+ compressed_features = self.compressor(visual_features)
124
+ compressed_features = compressed_features + self.visual_pos_embed
125
+
126
+ batch_size, num_tiles, compressed_patches, decoder_dim = compressed_features.shape
127
+ flat_visual_features = compressed_features.view(
128
+ batch_size, num_tiles * compressed_patches, decoder_dim
129
+ )
130
+
131
+ image_mask = (input_ids == self.image_token_id)
132
+ for b in range(batch_size):
133
+ num_places = image_mask[b].sum().item()
134
+ if num_places > 0:
135
+ features_to_insert = flat_visual_features[b][:num_places]
136
+ inputs_embeds[b, image_mask[b]] = features_to_insert
137
+
138
+ outputs = self.decoder(
139
+ input_ids=None,
140
+ attention_mask=attention_mask,
141
+ position_ids=position_ids,
142
+ past_key_values=past_key_values,
143
+ inputs_embeds=inputs_embeds,
144
+ labels=labels,
145
+ use_cache=use_cache,
146
+ output_attentions=output_attentions,
147
+ output_hidden_states=output_hidden_states,
148
+ return_dict=return_dict,
149
+ )
150
+
151
+ if task:
152
+ hidden = outputs.hidden_states[-1] if hasattr(outputs, "hidden_states") else outputs[-1]
153
+ head_outputs = self.output_heads(hidden, task=task)
154
+ return {"lm_outputs": outputs, "head_outputs": head_outputs}
155
+
156
+ return outputs
157
+
158
+ def prepare_inputs_for_generation(
159
+ self,
160
+ input_ids,
161
+ past_key_values=None,
162
+ attention_mask=None,
163
+ inputs_embeds=None,
164
+ pixel_values=None,
165
+ **kwargs
166
+ ) -> Dict[str, Any]:
167
+ """
168
+ Overridden to support KV caching during auto-regressive generation.
169
+ """
170
+ is_decoding = past_key_values is not None and pixel_values is None
171
+
172
+ if is_decoding:
173
+ input_ids = input_ids[:, -1:]
174
+ inputs_embeds = None
175
+
176
+ position_ids = kwargs.get("position_ids", None)
177
+ if attention_mask is not None and position_ids is None:
178
+ position_ids = attention_mask.long().cumsum(-1) - 1
179
+ position_ids.masked_fill_(attention_mask == 0, 1)
180
+ if is_decoding:
181
+ position_ids = position_ids[:, -input_ids.shape[-1]:]
182
+
183
+ return {
184
+ "input_ids": input_ids,
185
+ "inputs_embeds": inputs_embeds,
186
+ "past_key_values": past_key_values,
187
+ "pixel_values": pixel_values,
188
+ "attention_mask": attention_mask,
189
+ "position_ids": position_ids,
190
+ "use_cache": kwargs.get("use_cache"),
191
+ }
192
+
193
+ def _reorder_cache(self, past_key_values, beam_idx):
194
+ return self.decoder.lm._reorder_cache(past_key_values, beam_idx)
output_heads.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from typing import Dict
4
+
5
+ class JSONHead(nn.Module):
6
+ """
7
+ Structured JSON generation head.
8
+ Projects decoder hidden states to schema-constrained JSON token logits.
9
+ In practice, this works as a specialized classifier over JSON structural tokens
10
+ plus a pointer network for field values.
11
+ """
12
+ def __init__(self, hidden_size: int, num_json_tokens: int = 256):
13
+ super().__init__()
14
+ self.proj = nn.Sequential(
15
+ nn.Linear(hidden_size, hidden_size),
16
+ nn.GELU(),
17
+ nn.Linear(hidden_size, num_json_tokens),
18
+ )
19
+
20
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
21
+ return self.proj(hidden_states)
22
+
23
+
24
+ class KVHead(nn.Module):
25
+ """
26
+ Key-Value extraction head.
27
+ Produces key-value pairs from decoder hidden states with confidence scores.
28
+ Uses two separate projections: one for key detection, one for value extraction.
29
+ """
30
+ def __init__(self, hidden_size: int, num_keys: int = 128):
31
+ super().__init__()
32
+ self.key_classifier = nn.Linear(hidden_size, num_keys)
33
+ self.value_proj = nn.Linear(hidden_size, hidden_size)
34
+ self.confidence = nn.Linear(hidden_size, 1)
35
+
36
+ def forward(self, hidden_states: torch.Tensor) -> Dict[str, torch.Tensor]:
37
+ key_logits = self.key_classifier(hidden_states)
38
+ value_embeds = self.value_proj(hidden_states)
39
+ conf = torch.sigmoid(self.confidence(hidden_states))
40
+ return {
41
+ "key_logits": key_logits,
42
+ "value_embeds": value_embeds,
43
+ "confidence": conf,
44
+ }
45
+
46
+
47
+ class TableHead(nn.Module):
48
+ """
49
+ Table generation head.
50
+ Outputs HTML/Markdown table tokens via a specialized projection.
51
+ Can also output structured row/cell coordinates.
52
+ """
53
+ def __init__(self, hidden_size: int, num_table_tokens: int = 128):
54
+ super().__init__()
55
+ self.proj = nn.Sequential(
56
+ nn.Linear(hidden_size, hidden_size),
57
+ nn.GELU(),
58
+ nn.Linear(hidden_size, num_table_tokens),
59
+ )
60
+
61
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
62
+ return self.proj(hidden_states)
63
+
64
+
65
+ class OCRHead(nn.Module):
66
+ """
67
+ OCR text generation head.
68
+ Decodes visual features into character-level sequences.
69
+ Useful for reading-order text extraction.
70
+ """
71
+ def __init__(self, hidden_size: int, vocab_size: int = 256):
72
+ super().__init__()
73
+ self.proj = nn.Sequential(
74
+ nn.Linear(hidden_size, hidden_size),
75
+ nn.GELU(),
76
+ nn.Linear(hidden_size, vocab_size),
77
+ )
78
+
79
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
80
+ return self.proj(hidden_states)
81
+
82
+
83
+ class QAHead(nn.Module):
84
+ """
85
+ Question-answering head.
86
+ Standard LM head for natural language answers.
87
+ """
88
+ def __init__(self, hidden_size: int, vocab_size: int):
89
+ super().__init__()
90
+ self.proj = nn.Linear(hidden_size, vocab_size, bias=False)
91
+
92
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
93
+ return self.proj(hidden_states)
94
+
95
+
96
+ class MultiTaskOutputHeads(nn.Module):
97
+ """
98
+ Container for all structured output heads.
99
+ Routes decoder hidden states to the appropriate task-specific head(s).
100
+ During training, all heads can be trained jointly with task-specific losses.
101
+ During inference, only the requested head is used.
102
+ """
103
+ def __init__(
104
+ self,
105
+ hidden_size: int,
106
+ vocab_size: int,
107
+ num_json_tokens: int = 256,
108
+ num_keys: int = 128,
109
+ num_table_tokens: int = 128,
110
+ ocr_vocab_size: int = 256,
111
+ ):
112
+ super().__init__()
113
+ self.json_head = JSONHead(hidden_size, num_json_tokens)
114
+ self.kv_head = KVHead(hidden_size, num_keys)
115
+ self.table_head = TableHead(hidden_size, num_table_tokens)
116
+ self.ocr_head = OCRHead(hidden_size, ocr_vocab_size)
117
+ self.qa_head = QAHead(hidden_size, vocab_size)
118
+
119
+ def forward(
120
+ self,
121
+ hidden_states: torch.Tensor,
122
+ task: str = "qa",
123
+ ) -> Dict[str, torch.Tensor]:
124
+ if task == "json":
125
+ return {"logits": self.json_head(hidden_states)}
126
+ elif task == "kv":
127
+ return self.kv_head(hidden_states)
128
+ elif task == "table":
129
+ return {"logits": self.table_head(hidden_states)}
130
+ elif task == "ocr":
131
+ return {"logits": self.ocr_head(hidden_states)}
132
+ elif task == "qa":
133
+ return {"logits": self.qa_head(hidden_states)}
134
+ else:
135
+ raise ValueError(f"Unknown task: {task}")
preprocessor_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_convert_rgb": true,
3
+ "do_normalize": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.5,
8
+ 0.5,
9
+ 0.5
10
+ ],
11
+ "image_processor_type": "SiglipImageProcessor",
12
+ "image_std": [
13
+ 0.5,
14
+ 0.5,
15
+ 0.5
16
+ ],
17
+ "resample": 3,
18
+ "rescale_factor": 0.00392156862745098,
19
+ "size": {
20
+ "height": 384,
21
+ "width": 384
22
+ }
23
+ }
processing.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TinyDocVLMProcessor — standalone processor (does not inherit ProcessorMixin to avoid
3
+ strict type-checking issues in transformers<4.45). Provides the same public API.
4
+ """
5
+ import os
6
+ import json
7
+ from PIL import Image
8
+ from typing import Dict, Any, Union, Optional, List
9
+
10
+ import torch
11
+ from transformers import AutoTokenizer
12
+
13
+ from .image_processing import TinyDocImageProcessor
14
+
15
+
16
+ class TinyDocVLMProcessor:
17
+ """
18
+ Coordinates TinyDocImageProcessor (image tiling + normalisation) and the
19
+ SmolLM2 tokenizer extended with document-special tokens.
20
+
21
+ Usage:
22
+ processor = TinyDocVLMProcessor()
23
+ inputs = processor(text=["Extract fields: <image>"], images=[pil_img])
24
+ # inputs → {"input_ids", "attention_mask", "pixel_values", "image_token_id"}
25
+ """
26
+
27
+ # Class-level attrs used by some HF utilities (save_pretrained etc.)
28
+ image_processor_class = "TinyDocImageProcessor"
29
+ tokenizer_class = "AutoTokenizer"
30
+
31
+ def __init__(
32
+ self,
33
+ image_processor: Optional[TinyDocImageProcessor] = None,
34
+ tokenizer=None,
35
+ config=None,
36
+ **kwargs,
37
+ ):
38
+ self.image_processor = image_processor or TinyDocImageProcessor()
39
+ self.tokenizer = tokenizer or AutoTokenizer.from_pretrained(
40
+ "HuggingFaceTB/SmolLM2-135M-Instruct"
41
+ )
42
+ self.config = config
43
+
44
+ # Ensure <image> special token exists
45
+ self.image_token = "<image>"
46
+ if self.image_token not in self.tokenizer.get_vocab():
47
+ self.tokenizer.add_special_tokens(
48
+ {"additional_special_tokens": [self.image_token]}
49
+ )
50
+ self.image_token_id = self.tokenizer.convert_tokens_to_ids(self.image_token)
51
+
52
+ # ------------------------------------------------------------------
53
+ # Core __call__
54
+ # ------------------------------------------------------------------
55
+
56
+ def __call__(
57
+ self,
58
+ text: Union[str, List[str]],
59
+ images: Optional[Union[Image.Image, List[Image.Image]]] = None,
60
+ padding: bool = True,
61
+ truncation: bool = True,
62
+ max_length: Optional[int] = None,
63
+ return_tensors: str = "pt",
64
+ ) -> Dict[str, Any]:
65
+ """
66
+ Preprocesses text and images into tensors for the model.
67
+
68
+ Returns:
69
+ dict with keys: input_ids, attention_mask, pixel_values (optional),
70
+ image_token_id.
71
+ """
72
+ # ---- 1. Image processing ------------------------------------------------
73
+ pixel_values = None
74
+ num_tiles_list: List[int] = []
75
+
76
+ if images is not None:
77
+ if not isinstance(images, list):
78
+ images = [images]
79
+
80
+ processed: List[torch.Tensor] = []
81
+ for img in images:
82
+ tile_tensor = self.image_processor.preprocess(img) # (T, 3, H, W)
83
+ processed.append(tile_tensor)
84
+ num_tiles_list.append(tile_tensor.shape[0])
85
+
86
+ # Pad to max tiles so we can stack into a single tensor
87
+ max_tiles = max(num_tiles_list)
88
+ padded: List[torch.Tensor] = []
89
+ sz = self.image_processor.image_size
90
+ for tile_tensor in processed:
91
+ T = tile_tensor.shape[0]
92
+ if T < max_tiles:
93
+ pad = torch.zeros(
94
+ (max_tiles - T, 3, sz, sz),
95
+ dtype=tile_tensor.dtype,
96
+ device=tile_tensor.device,
97
+ )
98
+ tile_tensor = torch.cat([tile_tensor, pad], dim=0)
99
+ padded.append(tile_tensor)
100
+
101
+ pixel_values = torch.stack(padded, dim=0) # (B, max_tiles, 3, H, W)
102
+
103
+ # ---- 2. Expand <image> tokens in text ----------------------------------
104
+ scale = (
105
+ getattr(self.config, "pixel_shuffle_scale", 3) if self.config else 3
106
+ )
107
+ patch_size = (
108
+ getattr(self.config, "patch_size", 16) if self.config else 16
109
+ )
110
+ sz = self.image_processor.image_size
111
+ tokens_per_tile = (sz // patch_size // scale) ** 2
112
+
113
+ if isinstance(text, list):
114
+ expanded: List[str] = []
115
+ for idx, t in enumerate(text):
116
+ if idx < len(num_tiles_list):
117
+ total_vis = num_tiles_list[idx] * tokens_per_tile
118
+ else:
119
+ total_vis = 0
120
+ expanded.append(self._expand_image_tokens(t, total_vis))
121
+ processed_text = expanded
122
+ else:
123
+ total_vis = num_tiles_list[0] * tokens_per_tile if num_tiles_list else 0
124
+ processed_text = self._expand_image_tokens(text, total_vis)
125
+
126
+ # ---- 3. Tokenise -------------------------------------------------------
127
+ enc = self.tokenizer(
128
+ processed_text,
129
+ padding=padding,
130
+ truncation=truncation,
131
+ max_length=max_length,
132
+ return_tensors=return_tensors,
133
+ )
134
+
135
+ inputs: Dict[str, Any] = {
136
+ "input_ids": enc["input_ids"],
137
+ "attention_mask": enc["attention_mask"],
138
+ }
139
+ if pixel_values is not None:
140
+ inputs["pixel_values"] = pixel_values
141
+ inputs["image_token_id"] = self.image_token_id
142
+
143
+ return inputs
144
+
145
+ # ------------------------------------------------------------------
146
+ # Helpers
147
+ # ------------------------------------------------------------------
148
+
149
+ def _expand_image_tokens(self, text: str, total_tokens: int) -> str:
150
+ """Replaces the single '<image>' placeholder with `total_tokens` copies."""
151
+ expansion = self.image_token * total_tokens
152
+ return text.replace(self.image_token, expansion)
153
+
154
+ # ------------------------------------------------------------------
155
+ # save / from_pretrained stubs (for HF Hub compatibility)
156
+ # ------------------------------------------------------------------
157
+
158
+ def save_pretrained(self, save_directory: str, **kwargs):
159
+ os.makedirs(save_directory, exist_ok=True)
160
+ self.image_processor.save_pretrained(save_directory)
161
+ self.tokenizer.save_pretrained(save_directory)
162
+ # Write a minimal processor_config.json
163
+ cfg = {
164
+ "processor_class": "TinyDocVLMProcessor",
165
+ "image_token": self.image_token,
166
+ "image_token_id": self.image_token_id,
167
+ }
168
+ with open(os.path.join(save_directory, "processor_config.json"), "w") as f:
169
+ json.dump(cfg, f, indent=2)
170
+
171
+ @classmethod
172
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
173
+ image_processor = TinyDocImageProcessor.from_pretrained(
174
+ pretrained_model_name_or_path
175
+ )
176
+ tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path)
177
+ return cls(image_processor=image_processor, tokenizer=tokenizer, **kwargs)
processor_config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "processor_class": "TinyDocVLMProcessor",
3
+ "image_token": "<image>",
4
+ "image_token_id": 49153
5
+ }
token_compressor.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ import torch.nn as nn
4
+ from .configuration import TinyDocVLMConfig
5
+
6
+ class RMSNorm(nn.Module):
7
+ def __init__(self, dim: int, eps: float = 1e-6):
8
+ super().__init__()
9
+ self.eps = eps
10
+ self.weight = nn.Parameter(torch.ones(dim))
11
+
12
+ def forward(self, x):
13
+ variance = x.pow(2).mean(-1, keepdim=True)
14
+ return x * torch.rsqrt(variance + self.eps) * self.weight
15
+
16
+ class PixelShuffleTokenCompressor(nn.Module):
17
+ """
18
+ Performs space-to-depth token compression on Vision Transformer patch sequences.
19
+ Groups scale_factor x scale_factor patches and projects to decoder hidden dimension.
20
+ """
21
+ def __init__(self, config: TinyDocVLMConfig, encoder_dim: int, decoder_dim: int):
22
+ super().__init__()
23
+ self.config = config
24
+ self.scale_factor = config.pixel_shuffle_scale
25
+ self.encoder_dim = encoder_dim
26
+ self.decoder_dim = decoder_dim
27
+
28
+ # After space-to-depth, channel dimension becomes encoder_dim * scale_factor^2
29
+ compressed_dim = encoder_dim * (self.scale_factor ** 2)
30
+
31
+ # MLP projection to map visual tokens to language model dimension
32
+ self.projection = nn.Sequential(
33
+ nn.Linear(compressed_dim, decoder_dim),
34
+ nn.GELU(),
35
+ nn.Linear(decoder_dim, decoder_dim)
36
+ )
37
+ self.norm = RMSNorm(decoder_dim)
38
+
39
+ def forward(self, visual_features: torch.Tensor) -> torch.Tensor:
40
+ """
41
+ Args:
42
+ visual_features: shape (batch_size, num_tiles, num_patches, encoder_dim)
43
+
44
+ Returns:
45
+ compressed_features: shape (batch_size, num_tiles, num_compressed_patches, decoder_dim)
46
+ """
47
+ batch_size, num_tiles, num_patches, encoder_dim = visual_features.shape
48
+
49
+ # Determine spatial dimensions assuming a square grid of patches
50
+ grid_size = int(math.sqrt(num_patches))
51
+ if grid_size * grid_size != num_patches:
52
+ raise ValueError(
53
+ f"Number of patches ({num_patches}) must be a perfect square to apply 2D pixel shuffle."
54
+ )
55
+
56
+ if grid_size % self.scale_factor != 0:
57
+ raise ValueError(
58
+ f"Grid size ({grid_size}) must be divisible by pixel_shuffle_scale ({self.scale_factor})."
59
+ )
60
+
61
+ # Reshape to 2D spatial grid: (batch_size * num_tiles, grid_size, grid_size, encoder_dim)
62
+ x = visual_features.view(batch_size * num_tiles, grid_size, grid_size, encoder_dim)
63
+
64
+ # Apply space-to-depth: (batch_size * num_tiles, H//s, s, W//s, s, C)
65
+ s = self.scale_factor
66
+ x = x.view(batch_size * num_tiles, grid_size // s, s, grid_size // s, s, encoder_dim)
67
+
68
+ # Permute: (batch_size * num_tiles, H//s, W//s, s, s, C)
69
+ x = x.permute(0, 1, 3, 2, 4, 5).contiguous()
70
+
71
+ # Reshape to flatten the spatial groups into the channel dimension:
72
+ # (batch_size * num_tiles, (H//s) * (W//s), s * s * C)
73
+ new_patches = (grid_size // s) ** 2
74
+ x = x.view(batch_size * num_tiles, new_patches, s * s * encoder_dim)
75
+
76
+ # Project and normalize
77
+ x = self.projection(x)
78
+ x = self.norm(x)
79
+
80
+ # Reshape back to batch: (batch_size, num_tiles, new_patches, decoder_dim)
81
+ x = x.view(batch_size, num_tiles, new_patches, self.decoder_dim)
82
+ return x
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<|im_start|>",
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "extra_special_tokens": [
9
+ "<image>"
10
+ ],
11
+ "is_local": false,
12
+ "local_files_only": false,
13
+ "model_max_length": 8192,
14
+ "pad_token": "<|im_end|>",
15
+ "tokenizer_class": "GPT2Tokenizer",
16
+ "unk_token": "<|endoftext|>",
17
+ "vocab_size": 49152
18
+ }
vision_encoder.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from transformers import SiglipVisionModel
4
+ from .configuration import TinyDocVLMConfig
5
+
6
+ class SigLIPVisionEncoder(nn.Module):
7
+ """
8
+ Wrapper around HuggingFace's SiglipVisionModel.
9
+ Handles encoding of multiple image tiles and thumbnails.
10
+ """
11
+ def __init__(self, config: TinyDocVLMConfig):
12
+ super().__init__()
13
+ self.config = config
14
+
15
+ # Load from config or create model
16
+ vision_config = config.vision_config
17
+ # We can initialize from config. If we are running pretraining, we load weights.
18
+ # During runtime we might load a pretrained siglip model.
19
+ self.encoder = SiglipVisionModel(vision_config)
20
+ self.hidden_size = vision_config.hidden_size
21
+
22
+ # Add special region classification or auxiliary layers if needed in future
23
+ # For now, just a wrapper around the SigLIP vision encoder
24
+
25
+ def forward(
26
+ self,
27
+ pixel_values: torch.Tensor,
28
+ interpolate_pos_encoding: bool = False
29
+ ) -> torch.Tensor:
30
+ """
31
+ Args:
32
+ pixel_values: shape (batch_size, num_tiles, channels, height, width)
33
+ or (batch_size * num_tiles, channels, height, width)
34
+ interpolate_pos_encoding: whether to interpolate positional embeddings if resolution changes
35
+
36
+ Returns:
37
+ visual_features: shape (batch_size, num_tiles, num_patches, hidden_size)
38
+ """
39
+ # If input has shape (batch_size, num_tiles, channels, height, width)
40
+ if len(pixel_values.shape) == 5:
41
+ batch_size, num_tiles, channels, height, width = pixel_values.shape
42
+ # Flatten batch and tiles for vision encoder
43
+ pixel_values = pixel_values.view(batch_size * num_tiles, channels, height, width)
44
+ else:
45
+ # Assumed to be already flattened (batch_size * num_tiles, channels, height, width)
46
+ batch_size = 1
47
+ num_tiles = pixel_values.shape[0]
48
+ channels, height, width = pixel_values.shape[1:]
49
+
50
+ # Run through SigLIP Vision Model
51
+ outputs = self.encoder(
52
+ pixel_values=pixel_values,
53
+ interpolate_pos_encoding=interpolate_pos_encoding
54
+ )
55
+
56
+ # Last hidden state: (batch_size * num_tiles, num_patches, hidden_size)
57
+ # For SigLIP-B/16 with 384x384 input: num_patches = (384/16)^2 = 24^2 = 576
58
+ last_hidden_state = outputs.last_hidden_state
59
+
60
+ # Reshape back to batch format
61
+ num_patches = last_hidden_state.shape[1]
62
+ last_hidden_state = last_hidden_state.view(batch_size, num_tiles, num_patches, self.hidden_size)
63
+
64
+ return last_hidden_state