ndrugov's picture
ndrugov HF Staff
Upload folder using huggingface_hub
1775d16 verified
Raw
History Blame Contribute Delete
5.41 kB
"""Self-contained model definition for the encoder-free VLM demo.
This mirrors the inference-relevant subset of the training repo's `vlm.py`
(no wandb / datasets / matplotlib imports), so the module is light enough for a
Space while producing exactly the same state-dict keys as the checkpoint.
Architecture: a learned patch embedder (no pretrained vision encoder) projects
512x512 images into Qwen3-1.7B's hidden space; the projected patch embeddings
are spliced into the <|image|> token positions and the pretrained decoder
generates from inputs_embeds.
"""
from dataclasses import dataclass, field
import torch
import torch.nn as nn
from torchvision import transforms
from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig
if torch.cuda.is_available():
device = "cuda"
elif torch.backends.mps.is_available():
device = "mps"
else:
device = "cpu"
# Standardize arbitrary input images to the fixed 512x512 the embedder expects.
transform = transforms.Compose([
transforms.Resize(512),
transforms.CenterCrop(512),
transforms.ToTensor(),
])
@dataclass
class VisionEmbedderConfig:
img_size: int = 512
patch_size: int = 32
n_in_channels: int = 3
out_dim: int = 2048
max_patch_positions: int = 16
layernorm_eps: float = 1e-6
flat_patch_dim: int = field(init=False)
num_patches: int = field(init=False)
def __post_init__(self):
self.flat_patch_dim = self.patch_size ** 2 * self.n_in_channels
self.num_patches = (self.img_size // self.patch_size) ** 2
@dataclass
class DecoderConfig:
checkpoint: str = "Qwen/Qwen3-1.7B"
image_token: str = "<|image|>"
hidden_size: int = field(init=False)
def __post_init__(self):
cfg = AutoConfig.from_pretrained(self.checkpoint)
self.hidden_size = cfg.hidden_size
@dataclass
class VLMConfig:
vision: VisionEmbedderConfig = field(default_factory=VisionEmbedderConfig)
decoder: DecoderConfig = field(default_factory=DecoderConfig)
def extract_flattened_patches(x: torch.Tensor, patch_size: int) -> torch.Tensor:
"""Split (B, C, H, W) into non-overlapping P x P patches: (B, N, C*P*P)."""
B, C, H, W = x.shape
P = patch_size
nh, nw = H // P, W // P
x = x.reshape(B, C, nh, P, nw, P)
x = x.permute(0, 2, 4, 1, 3, 5)
x = x.reshape(B, nh * nw, C * P * P)
return x
class VisionEmbedder(nn.Module):
def __init__(self, cfg: VisionEmbedderConfig):
super().__init__()
self.cfg = cfg
self.ln1 = nn.LayerNorm(cfg.flat_patch_dim, eps=cfg.layernorm_eps)
self.fc = nn.Linear(cfg.flat_patch_dim, cfg.out_dim)
self.ln2 = nn.LayerNorm(cfg.out_dim, eps=cfg.layernorm_eps)
self.x_pos_emb = nn.Parameter(torch.randn(1, cfg.max_patch_positions, cfg.out_dim))
self.y_pos_emb = nn.Parameter(torch.randn(1, cfg.max_patch_positions, cfg.out_dim))
self.ln3 = nn.LayerNorm(cfg.out_dim, eps=cfg.layernorm_eps)
def forward(self, x: torch.Tensor) -> torch.Tensor:
B, C, H, W = x.shape
P = self.cfg.patch_size
nh, nw = H // P, W // P
assert nh <= self.cfg.max_patch_positions and nw <= self.cfg.max_patch_positions, \
f"Patch grid {nh}x{nw} exceeds max_patch_positions={self.cfg.max_patch_positions}"
x = extract_flattened_patches(x, P)
x = self.ln1(x)
x = self.fc(x)
x = self.ln2(x)
row_emb = self.y_pos_emb[:, :nh, :]
col_emb = self.x_pos_emb[:, :nw, :]
pos = (row_emb.unsqueeze(2) + col_emb.unsqueeze(1)).reshape(1, nh * nw, -1)
x = x + pos
x = self.ln3(x)
return x
def replace_img_tokens_with_embd(input_ids, combined_embd, image_embd, image_token_id):
"""Overwrite <|image|> token positions with projected patch embeddings."""
mask = (input_ids == image_token_id)
result = combined_embd.clone()
result[mask] = image_embd.reshape(-1, image_embd.shape[-1]).to(result.dtype)
return result
class VLM(nn.Module):
def __init__(self, cfg: VLMConfig):
super().__init__()
self.cfg = cfg
self.vision_embedder = VisionEmbedder(cfg.vision)
self.connector = nn.Linear(cfg.vision.out_dim, cfg.decoder.hidden_size, bias=False)
self.tokenizer = AutoTokenizer.from_pretrained(
cfg.decoder.checkpoint,
extra_special_tokens={"image_token": cfg.decoder.image_token},
)
self.decoder = AutoModelForCausalLM.from_pretrained(cfg.decoder.checkpoint)
self.decoder.resize_token_embeddings(len(self.tokenizer))
@property
def image_token_id(self):
return self.tokenizer.image_token_id
def _replace_img_tokens_with_embd(self, input_ids, combined_embd, image_embd):
return replace_img_tokens_with_embd(input_ids, combined_embd, image_embd, self.image_token_id)
def forward(self, input_ids: torch.Tensor, image: torch.Tensor) -> torch.Tensor:
image_embd = self.vision_embedder(image)
projected_image_embd = self.connector(image_embd)
token_embd = self.decoder.get_input_embeddings()(input_ids)
combined_embd = self._replace_img_tokens_with_embd(
input_ids, token_embd, projected_image_embd
)
return self.decoder(inputs_embeds=combined_embd).logits