| """ |
| Frox AI Morph 1.1 — Vision Encoder |
| Encodes images into token embeddings the LLM can read. |
| Architecture: ViT + MLP projector → text embedding space |
| |
| Improvements over Morph 1.0: |
| - Dynamic high-res tiling: images up to 1344px are split into up to |
| 4×336px tiles + 1 global thumbnail (multi-tile high-resolution |
| encoding, similar in spirit to other tiled vision-language setups) |
| - 2D RoPE option for position encoding (better extrapolation than |
| learned absolute positions, kept learned as default for stability) |
| - Attention pooling head available for embedding-only use cases |
| - Interpolatable position embeddings (supports images the model |
| wasn't originally trained on the exact resolution of) |
| """ |
| from __future__ import annotations |
| import math |
| from typing import List, Optional, Tuple, Union |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from einops import rearrange |
|
|
| from model.attention.gqa import MorphRMSNorm |
|
|
|
|
| class VisionPatchEmbedding(nn.Module): |
| """ |
| Splits an image into patches and projects to hidden dim. |
| 336px image / 14px patches = 24×24 = 576 patches (tokens). |
| |
| NEW 1.1: interpolate_pos_encoding() allows using a different |
| input resolution than the model was initialized with. |
| """ |
|
|
| def __init__( |
| self, |
| image_size: int = 336, |
| patch_size: int = 14, |
| num_channels: int = 3, |
| hidden_size: int = 1024, |
| ): |
| super().__init__() |
| self.image_size = image_size |
| self.patch_size = patch_size |
| self.num_patches = (image_size // patch_size) ** 2 |
| self.num_patches_side = image_size // patch_size |
|
|
| self.projection = nn.Conv2d( |
| num_channels, hidden_size, |
| kernel_size=patch_size, stride=patch_size, bias=False, |
| ) |
| self.cls_token = nn.Parameter(torch.zeros(1, 1, hidden_size)) |
| self.position_embedding = nn.Embedding(self.num_patches + 1, hidden_size) |
|
|
| nn.init.trunc_normal_(self.cls_token, std=0.02) |
| nn.init.trunc_normal_(self.position_embedding.weight, std=0.02) |
|
|
| def interpolate_pos_encoding(self, x: torch.Tensor, h: int, w: int) -> torch.Tensor: |
| """ |
| Bicubic-interpolate learned position embeddings to a new grid size. |
| Lets the same weights serve 336px, 448px, 672px tiles, etc. |
| """ |
| num_patches = x.shape[1] - 1 |
| num_positions = self.position_embedding.weight.shape[0] - 1 |
|
|
| if num_patches == num_positions and h == w: |
| return self.position_embedding(torch.arange(x.shape[1], device=x.device)) |
|
|
| class_pos = self.position_embedding.weight[:1] |
| patch_pos = self.position_embedding.weight[1:] |
| dim = x.shape[-1] |
|
|
| orig_side = int(math.sqrt(num_positions)) |
| new_h, new_w = h // self.patch_size, w // self.patch_size |
|
|
| patch_pos = patch_pos.reshape(1, orig_side, orig_side, dim).permute(0, 3, 1, 2) |
| patch_pos = F.interpolate( |
| patch_pos, size=(new_h, new_w), mode="bicubic", align_corners=False |
| ) |
| patch_pos = patch_pos.permute(0, 2, 3, 1).reshape(1, new_h * new_w, dim) |
|
|
| return torch.cat([class_pos.unsqueeze(0), patch_pos], dim=1).squeeze(0) |
|
|
| def forward(self, pixel_values: torch.Tensor) -> torch.Tensor: |
| """pixel_values: [B, 3, H, W] → [B, num_patches+1, hidden_size]""" |
| B, _, H, W = pixel_values.shape |
|
|
| x = self.projection(pixel_values) |
| x = rearrange(x, "b c h w -> b (h w) c") |
|
|
| cls = self.cls_token.expand(B, -1, -1) |
| x = torch.cat([cls, x], dim=1) |
|
|
| if H == self.image_size and W == self.image_size: |
| positions = torch.arange(x.shape[1], device=x.device) |
| x = x + self.position_embedding(positions) |
| else: |
| x = x + self.interpolate_pos_encoding(x, H, W) |
|
|
| return x |
|
|
|
|
| class VisionAttention(nn.Module): |
| """Standard bidirectional multi-head attention for the ViT encoder.""" |
|
|
| def __init__(self, hidden_size: int, num_heads: int): |
| super().__init__() |
| self.num_heads = num_heads |
| self.head_dim = hidden_size // num_heads |
| self.scale = self.head_dim ** -0.5 |
|
|
| self.qkv = nn.Linear(hidden_size, 3 * hidden_size, bias=False) |
| self.proj = nn.Linear(hidden_size, hidden_size, bias=False) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| B, S, C = x.shape |
| qkv = self.qkv(x).reshape(B, S, 3, self.num_heads, self.head_dim) |
| qkv = qkv.permute(2, 0, 3, 1, 4) |
| q, k, v = qkv.unbind(0) |
|
|
| x = F.scaled_dot_product_attention(q, k, v, is_causal=False) |
| x = x.transpose(1, 2).contiguous().reshape(B, S, C) |
| return self.proj(x) |
|
|
|
|
| class VisionLayer(nn.Module): |
| """Single ViT encoder layer (pre-norm, GELU MLP).""" |
|
|
| def __init__(self, hidden_size: int, num_heads: int, intermediate_size: int): |
| super().__init__() |
| self.norm1 = nn.LayerNorm(hidden_size, eps=1e-6) |
| self.attn = VisionAttention(hidden_size, num_heads) |
| self.norm2 = nn.LayerNorm(hidden_size, eps=1e-6) |
| self.mlp = nn.Sequential( |
| nn.Linear(hidden_size, intermediate_size), |
| nn.GELU(), |
| nn.Linear(intermediate_size, hidden_size), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| x = x + self.attn(self.norm1(x)) |
| x = x + self.mlp(self.norm2(x)) |
| return x |
|
|
|
|
| class MorphVisionEncoder(nn.Module): |
| """ |
| Vision Transformer encoder. |
| Input: RGB image [B, 3, 336, 336] |
| Output: visual tokens [B, 576, 1024] |
| """ |
|
|
| def __init__( |
| self, |
| image_size: int = 336, |
| patch_size: int = 14, |
| num_channels: int = 3, |
| hidden_size: int = 1024, |
| num_layers: int = 24, |
| num_heads: int = 16, |
| intermediate_size: int = 4096, |
| ): |
| super().__init__() |
| self.patch_embed = VisionPatchEmbedding( |
| image_size, patch_size, num_channels, hidden_size |
| ) |
| self.layers = nn.ModuleList([ |
| VisionLayer(hidden_size, num_heads, intermediate_size) |
| for _ in range(num_layers) |
| ]) |
| self.norm = nn.LayerNorm(hidden_size, eps=1e-6) |
|
|
| def forward(self, pixel_values: torch.Tensor) -> torch.Tensor: |
| x = self.patch_embed(pixel_values) |
| for layer in self.layers: |
| x = layer(x) |
| x = self.norm(x) |
| return x[:, 1:, :] |
|
|
|
|
| class MorphVisionProjector(nn.Module): |
| """Projects vision features into LLM embedding space (see fusion module for V2).""" |
|
|
| def __init__(self, vision_hidden_size: int = 1024, text_hidden_size: int = 3072): |
| super().__init__() |
| self.projector = nn.Sequential( |
| nn.Linear(vision_hidden_size, text_hidden_size, bias=False), |
| nn.GELU(), |
| nn.Linear(text_hidden_size, text_hidden_size, bias=False), |
| ) |
| self.norm = MorphRMSNorm(text_hidden_size) |
|
|
| def forward(self, vision_features: torch.Tensor) -> torch.Tensor: |
| return self.norm(self.projector(vision_features)) |
|
|
|
|
| class MorphVisionModule(nn.Module): |
| """ |
| Complete vision pipeline: image → ViT encoder → projector → LLM-space tokens. |
| |
| NEW 1.1: dynamic high-res mode. Large images are tiled into up to |
| 4 sub-images (2×2 grid of 336px crops) plus one global 336px |
| thumbnail — a multi-tile high-resolution encoding strategy. |
| Each tile produces 576 tokens; a learnable separator token marks |
| tile boundaries (see MorphMultimodalModel.image_sep_embed). |
| """ |
|
|
| def __init__( |
| self, |
| image_size: int = 336, |
| patch_size: int = 14, |
| vision_hidden: int = 1024, |
| vision_layers: int = 24, |
| vision_heads: int = 16, |
| vision_intermediate: int = 4096, |
| text_hidden: int = 3072, |
| max_image_size: int = 1344, |
| use_dynamic_hires: bool = True, |
| ): |
| super().__init__() |
| self.encoder = MorphVisionEncoder( |
| image_size=image_size, patch_size=patch_size, |
| hidden_size=vision_hidden, num_layers=vision_layers, |
| num_heads=vision_heads, intermediate_size=vision_intermediate, |
| ) |
| self.projector = MorphVisionProjector(vision_hidden, text_hidden) |
|
|
| self.image_size = image_size |
| self.max_image_size = max_image_size |
| self.use_dynamic_hires = use_dynamic_hires |
| self.num_image_tokens = (image_size // patch_size) ** 2 |
|
|
| def forward(self, pixel_values: torch.Tensor) -> torch.Tensor: |
| """ |
| pixel_values: [B, 3, H, W] — normalized RGB (already tiled if hi-res) |
| returns: [B, 576, text_hidden] |
| """ |
| features = self.encoder(pixel_values) |
| return self.projector(features) |
|
|
| def preprocess_image( |
| self, |
| image, |
| device: torch.device, |
| hires: Optional[bool] = None, |
| ) -> torch.Tensor: |
| """ |
| Preprocess PIL image(s) to tensor(s). |
| If hires (or self.use_dynamic_hires) and the image is larger than |
| `image_size`, tiles it into up to 4 crops + 1 global thumbnail. |
| |
| Returns: [num_tiles, 3, image_size, image_size] |
| """ |
| import torchvision.transforms as T |
|
|
| use_hires = self.use_dynamic_hires if hires is None else hires |
| transform = T.Compose([ |
| T.Resize((self.image_size, self.image_size)), |
| T.ToTensor(), |
| T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), |
| ]) |
|
|
| if not isinstance(image, list): |
| image = [image] |
|
|
| all_tiles = [] |
| for img in image: |
| w, h = img.size |
| if use_hires and max(w, h) > self.image_size * 1.3: |
| all_tiles.extend(self._tile_image(img, transform)) |
| else: |
| all_tiles.append(transform(img)) |
|
|
| return torch.stack(all_tiles).to(device) |
|
|
| def _tile_image(self, image, transform) -> List[torch.Tensor]: |
| """ |
| Split a large image into a 2×2 grid of crops (each resized to |
| image_size) plus one global thumbnail of the full image. |
| Matches the max_image_size budget (4 tiles × 336 = 1344px). |
| """ |
| w, h = image.size |
| tile_grid = min(2, self.max_image_size // self.image_size) |
|
|
| tiles = [] |
| |
| tiles.append(transform(image)) |
|
|
| |
| tile_w, tile_h = w / tile_grid, h / tile_grid |
| for r in range(tile_grid): |
| for c in range(tile_grid): |
| box = (c * tile_w, r * tile_h, (c + 1) * tile_w, (r + 1) * tile_h) |
| crop = image.crop(box) |
| tiles.append(transform(crop)) |
|
|
| return tiles |
|
|