Image-to-Text
MLX
Safetensors
English
multilingual
unlimited-ocr-mlx
apple-silicon
ocr
vision-language-model
document-parsing
deepseek-v2
mixture-of-experts
sam-vit
clip
text-recognition
layout-analysis
paddlex
custom_code
Instructions to use LoJexLLM/Unlimited-OCR-MLX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use LoJexLLM/Unlimited-OCR-MLX with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir Unlimited-OCR-MLX LoJexLLM/Unlimited-OCR-MLX
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
| """Unlimited-OCR MLX Model Implementation. | |
| High-precision OCR model fully implemented in MLX for Apple Silicon acceleration. | |
| Architecture: Vision Encoder (SAM-ViT-B + CLIP-L) → DeepSeek-V2 MoE Language Model. | |
| """ | |
| import math | |
| from typing import Optional, Tuple, List, Dict | |
| from dataclasses import dataclass | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| from .config import UnlimitedOCRConfig, VisionConfig, LanguageConfig, ProjectorConfig | |
| # ============================================================================= | |
| # Utility Functions | |
| # ============================================================================= | |
| def _compute_default_rope_freqs( | |
| dim: int, max_position_embeddings: int = 32768, base: float = 10000.0 | |
| ) -> mx.array: | |
| """Compute RoPE frequencies. Returns (max_pos, dim/2) for rotation.""" | |
| theta = 1.0 / (base ** (mx.arange(0, dim, 2, dtype=mx.float32) / dim)) | |
| t = mx.arange(max_position_embeddings, dtype=mx.float32) | |
| freqs = mx.outer(t, theta) | |
| return freqs | |
| def _apply_rotary_pos_emb(q, k, cos, sin, position_ids=None): | |
| """Apply rotary position embeddings to query and key tensors. | |
| Args: | |
| q, k: [B, heads, seq_len, head_dim] | |
| cos, sin: [seq_len, half_dim] already sliced/indexed by caller | |
| """ | |
| B, H, L, D = q.shape | |
| half_D = D // 2 | |
| # cos/sin are already properly shaped by RotaryEmbedding | |
| # They should be [L, half_D] or [1, L, half_D] | |
| if cos.ndim == 3: | |
| cos = cos.reshape(-1, cos.shape[-1]) | |
| sin = sin.reshape(-1, sin.shape[-1]) | |
| # Ensure correct length | |
| cos = cos[:L] | |
| sin = sin[:L] | |
| # Reshape for broadcasting: [1, 1, L, half_D] | |
| cos = cos.reshape(1, 1, L, half_D) | |
| sin = sin.reshape(1, 1, L, half_D) | |
| def _rotate_half(x): | |
| x1 = x[..., :half_D] | |
| x2 = x[..., half_D:] | |
| return mx.concatenate([-x2, x1], axis=-1) | |
| # Duplicate cos/sin to full head_dim for element-wise multiply | |
| cos2 = mx.concatenate([cos, cos], axis=-1) | |
| sin2 = mx.concatenate([sin, sin], axis=-1) | |
| q_rot = q * cos2 + _rotate_half(q) * sin2 | |
| k_rot = k * cos2 + _rotate_half(k) * sin2 | |
| return q_rot, k_rot | |
| def silu(x): | |
| """SiLU activation function.""" | |
| return x * mx.sigmoid(x) | |
| # ============================================================================= | |
| # RMSNorm | |
| # ============================================================================= | |
| class RMSNorm(nn.Module): | |
| """Root Mean Square Layer Normalization.""" | |
| def __init__(self, dims: int, eps: float = 1e-6): | |
| super().__init__() | |
| self.weight = mx.ones((dims,)) | |
| self.eps = eps | |
| def __call__(self, x): | |
| return mx.fast.rms_norm(x, 1.0 + self.weight, self.eps) | |
| # ============================================================================= | |
| # RoPE | |
| # ============================================================================= | |
| class RotaryEmbedding: | |
| """Rotary Position Embedding.""" | |
| def __init__(self, dim: int, max_position_embeddings: int = 32768, base: float = 10000.0): | |
| self.dim = dim | |
| self.max_position_embeddings = max_position_embeddings | |
| self.base = base | |
| self._freqs_cos_sin = None | |
| def _ensure_freqs(self): | |
| if self._freqs_cos_sin is None: | |
| freqs = _compute_default_rope_freqs(self.dim, self.max_position_embeddings, self.base) | |
| self._freqs_cos_sin = (mx.cos(freqs), mx.sin(freqs)) | |
| def cos_cached(self): | |
| self._ensure_freqs() | |
| return self._freqs_cos_sin[0] | |
| def sin_cached(self): | |
| self._ensure_freqs() | |
| return self._freqs_cos_sin[1] | |
| def __call__(self, x, position_ids=None, seq_len=None): | |
| self._ensure_freqs() | |
| cos, sin = self.cos_cached, self.sin_cached | |
| if seq_len is not None: | |
| cos, sin = cos[:seq_len], sin[:seq_len] | |
| if position_ids is not None: | |
| cos = cos[position_ids] | |
| sin = sin[position_ids] | |
| return cos, sin | |
| # ============================================================================= | |
| # Standard Multi-Head Attention | |
| # ============================================================================= | |
| class MultiHeadAttention(nn.Module): | |
| """Standard Multi-Head Attention with RoPE.""" | |
| def __init__(self, config: LanguageConfig, layer_idx: int): | |
| super().__init__() | |
| self.hidden_size = config.hidden_size | |
| self.num_heads = config.num_attention_heads | |
| self.num_kv_heads = config.num_key_value_heads | |
| self.head_dim = config.head_dim | |
| self.layer_idx = layer_idx | |
| self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False) | |
| self.k_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=False) | |
| self.v_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=False) | |
| self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False) | |
| self.rotary_emb = RotaryEmbedding( | |
| self.head_dim, | |
| max_position_embeddings=config.max_position_embeddings, | |
| base=config.rope_theta, | |
| ) | |
| self.scale = self.head_dim ** -0.5 | |
| def __call__( | |
| self, | |
| hidden_states: mx.array, | |
| attention_mask: Optional[mx.array] = None, | |
| position_ids: Optional[mx.array] = None, | |
| past_key_value: Optional[Tuple[mx.array, mx.array]] = None, | |
| use_cache: bool = False, | |
| ) -> Tuple[mx.array, Optional[Tuple[mx.array, mx.array]]]: | |
| B, L, _ = hidden_states.shape | |
| q = self.q_proj(hidden_states).reshape(B, L, self.num_heads, self.head_dim).transpose(0, 2, 1, 3) | |
| k = self.k_proj(hidden_states).reshape(B, L, self.num_kv_heads, self.head_dim).transpose(0, 2, 1, 3) | |
| v = self.v_proj(hidden_states).reshape(B, L, self.num_kv_heads, self.head_dim).transpose(0, 2, 1, 3) | |
| cos, sin = self.rotary_emb(q, position_ids=position_ids, seq_len=L) | |
| q, k = _apply_rotary_pos_emb(q, k, cos, sin, position_ids) | |
| if past_key_value is not None: | |
| pk, pv = past_key_value | |
| k = mx.concatenate([pk, k], axis=2) | |
| v = mx.concatenate([pv, v], axis=2) | |
| past_kv = (k, v) if use_cache else None | |
| # GQA: repeat k/v heads | |
| n_rep = self.num_heads // self.num_kv_heads | |
| if n_rep > 1: | |
| k = mx.repeat(k, n_rep, axis=1) | |
| v = mx.repeat(v, n_rep, axis=1) | |
| # Scaled dot-product attention | |
| scores = (q @ k.transpose(0, 1, 3, 2)) * self.scale | |
| if attention_mask is not None: | |
| scores = scores + attention_mask | |
| attn_weights = mx.softmax(scores.astype(mx.float32), axis=-1).astype(q.dtype) | |
| attn_output = attn_weights @ v | |
| attn_output = attn_output.transpose(0, 2, 1, 3).reshape(B, L, -1) | |
| output = self.o_proj(attn_output) | |
| return output, past_kv | |
| # ============================================================================= | |
| # MLP (SwiGLU) | |
| # ============================================================================= | |
| class SwiGLUMLP(nn.Module): | |
| """SwiGLU MLP used in dense layers and experts.""" | |
| def __init__(self, hidden_size: int, intermediate_size: int): | |
| super().__init__() | |
| self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False) | |
| self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False) | |
| self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False) | |
| def __call__(self, x): | |
| return self.down_proj(silu(self.gate_proj(x)) * self.up_proj(x)) | |
| # ============================================================================= | |
| # MoE (Mixture of Experts) | |
| # ============================================================================= | |
| class MoEGate(nn.Module): | |
| """Top-k gating for MoE.""" | |
| def __init__(self, config: LanguageConfig): | |
| super().__init__() | |
| self.top_k = config.num_experts_per_tok | |
| self.n_routed_experts = config.n_routed_experts | |
| self.scoring_func = config.scoring_func | |
| self.topk_method = config.topk_method | |
| self.norm_topk_prob = config.norm_topk_prob | |
| # Gate weight: [n_experts, hidden_size] | |
| self.weight = mx.zeros((self.n_routed_experts, config.hidden_size)) | |
| def __call__(self, hidden_states: mx.array) -> Tuple[mx.array, mx.array]: | |
| # hidden_states: [B*L, hidden_size] | |
| logits = hidden_states.astype(mx.float32) @ self.weight.astype(mx.float32).T | |
| if self.scoring_func == "softmax": | |
| scores = mx.softmax(logits, axis=-1) | |
| else: | |
| scores = mx.sigmoid(logits) | |
| # Top-k selection (MLX topk returns indices, then we gather weights) | |
| topk_indices = mx.argpartition(-scores, kth=self.top_k - 1, axis=-1)[:, :self.top_k] | |
| # Gather the actual scores for these indices | |
| topk_weights = mx.take_along_axis(scores, topk_indices, axis=-1) | |
| if self.norm_topk_prob: | |
| denom = topk_weights.sum(axis=-1, keepdims=True) + 1e-20 | |
| topk_weights = topk_weights / denom | |
| return topk_indices, topk_weights | |
| class DeepSeekMoE(nn.Module): | |
| """DeepSeek-V2 MoE block with shared experts.""" | |
| def __init__(self, config: LanguageConfig): | |
| super().__init__() | |
| self.num_experts_per_tok = config.num_experts_per_tok | |
| self.n_routed_experts = config.n_routed_experts | |
| self.moe_intermediate_size = config.moe_intermediate_size | |
| # Create routed experts | |
| self.experts = [ | |
| SwiGLUMLP(config.hidden_size, self.moe_intermediate_size) | |
| for _ in range(self.n_routed_experts) | |
| ] | |
| self.gate = MoEGate(config) | |
| # Shared experts (2 experts with combined intermediate size) | |
| if config.n_shared_experts is not None: | |
| shared_dim = self.moe_intermediate_size * config.n_shared_experts | |
| self.shared_experts = SwiGLUMLP(config.hidden_size, shared_dim) | |
| def _moe_infer(self, x: mx.array, topk_ids: mx.array, topk_weights: mx.array) -> mx.array: | |
| """Inference-time MoE computation.""" | |
| B, L, D = x.shape | |
| x_flat = x.reshape(-1, D) # [B*L, D] | |
| tk_flat = topk_ids.reshape(-1) # [B*L*K] | |
| tw_flat = topk_weights.reshape(-1) # [B*L*K] | |
| # Count tokens per expert | |
| import numpy as np | |
| tk_np = np.array(tk_flat, dtype=np.int32) | |
| token_counts = np.bincount(tk_np, minlength=self.n_routed_experts) | |
| # Sort tokens by expert | |
| sort_indices = mx.argsort(tk_flat) | |
| repeated_x = mx.repeat(x_flat, self.num_experts_per_tok, axis=0) | |
| sorted_tokens = repeated_x[sort_indices] | |
| sorted_weights = tw_flat[sort_indices] | |
| # Process each expert's tokens | |
| outputs = [] | |
| start = 0 | |
| for i in range(self.n_routed_experts): | |
| count = int(token_counts[i]) | |
| if count == 0: | |
| continue | |
| end = start + count | |
| expert_out = self.experts[i](sorted_tokens[start:end].astype(mx.float16)) | |
| expert_out = expert_out * sorted_weights[start:end][:, None] | |
| outputs.append((sort_indices[start:end], expert_out)) | |
| start = end | |
| if not outputs: | |
| return mx.zeros_like(x) | |
| # Scatter back | |
| all_indices = mx.concatenate([o[0] for o in outputs], axis=0) | |
| all_outputs = mx.concatenate([o[1] for o in outputs], axis=0) | |
| # Restore original order via argsort of indices | |
| restore = mx.argsort(all_indices) | |
| final = all_outputs[restore] | |
| # Sum across top-k experts for each token: (B*L, K, D) → (B*L, D) | |
| final = final.reshape(B * L, self.num_experts_per_tok, D).sum(axis=1) | |
| return final.reshape(B, L, D) | |
| def __call__(self, hidden_states: mx.array) -> mx.array: | |
| identity = hidden_states | |
| B, L, D = hidden_states.shape | |
| x_flat = hidden_states.reshape(-1, D) | |
| topk_idx, topk_weight = self.gate(x_flat) | |
| # Reshape routing back | |
| topk_idx = topk_idx.reshape(B * L, self.num_experts_per_tok) | |
| topk_weight = topk_weight.reshape(B * L, self.num_experts_per_tok) | |
| moe_out = self._moe_infer(hidden_states, topk_idx.reshape(B, L, -1), topk_weight.reshape(B, L, -1)) | |
| if hasattr(self, 'shared_experts'): | |
| moe_out = moe_out + self.shared_experts(identity) | |
| return moe_out | |
| # ============================================================================= | |
| # DeepSeek-V2 Decoder Layer | |
| # ============================================================================= | |
| class DeepSeekDecoderLayer(nn.Module): | |
| """Single decoder layer with attention + MLP/MoE.""" | |
| def __init__(self, config: LanguageConfig, layer_idx: int): | |
| super().__init__() | |
| self.layer_idx = layer_idx | |
| self.hidden_size = config.hidden_size | |
| self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) | |
| self.self_attn = MultiHeadAttention(config, layer_idx) | |
| self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) | |
| # Layer 0 is dense MLP, rest are MoE | |
| is_dense = layer_idx < config.first_k_dense_replace | |
| if is_dense: | |
| self.mlp = SwiGLUMLP(config.hidden_size, config.intermediate_size) | |
| self.is_moe = False | |
| else: | |
| self.mlp = DeepSeekMoE(config) | |
| self.is_moe = True | |
| def __call__( | |
| self, | |
| hidden_states: mx.array, | |
| attention_mask: Optional[mx.array] = None, | |
| position_ids: Optional[mx.array] = None, | |
| past_key_value: Optional[Tuple[mx.array, mx.array]] = None, | |
| use_cache: bool = False, | |
| ) -> Tuple[mx.array, Optional[Tuple[mx.array, mx.array]]]: | |
| # Self-attention | |
| residual = hidden_states | |
| hidden_states = self.input_layernorm(hidden_states) | |
| hidden_states, present_kv = self.self_attn( | |
| hidden_states, attention_mask, position_ids, past_key_value, use_cache | |
| ) | |
| hidden_states = residual + hidden_states | |
| # MLP / MoE | |
| residual = hidden_states | |
| hidden_states = self.post_attention_layernorm(hidden_states) | |
| hidden_states = self.mlp(hidden_states) | |
| hidden_states = residual + hidden_states | |
| return hidden_states, present_kv | |
| # ============================================================================= | |
| # DeepSeek-V2 Language Model | |
| # ============================================================================= | |
| class DeepSeekModel(nn.Module): | |
| """DeepSeek-V2 Language Model (12 layers, MoE).""" | |
| def __init__(self, config: LanguageConfig): | |
| super().__init__() | |
| self.config = config | |
| self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size) | |
| self.layers = [ | |
| DeepSeekDecoderLayer(config, i) | |
| for i in range(config.num_hidden_layers) | |
| ] | |
| self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) | |
| def __call__( | |
| self, | |
| input_ids: Optional[mx.array] = None, | |
| inputs_embeds: Optional[mx.array] = None, | |
| attention_mask: Optional[mx.array] = None, | |
| position_ids: Optional[mx.array] = None, | |
| past_key_values: Optional[List[Tuple[mx.array, mx.array]]] = None, | |
| use_cache: bool = False, | |
| ) -> Tuple[mx.array, Optional[List[Tuple[mx.array, mx.array]]]]: | |
| if inputs_embeds is None: | |
| inputs_embeds = self.embed_tokens(input_ids) | |
| B, L, _ = inputs_embeds.shape | |
| # Create causal mask | |
| if attention_mask is None: | |
| attention_mask = mx.tril(mx.ones((L, L), dtype=mx.bool_)) | |
| attention_mask = mx.where(attention_mask, 0.0, float('-inf')) | |
| attention_mask = attention_mask[None, None, :, :] # [1, 1, L, L] | |
| # Create position IDs | |
| if position_ids is None: | |
| if past_key_values is not None and past_key_values[0] is not None: | |
| cache_len = past_key_values[0][0].shape[2] | |
| position_ids = mx.arange(cache_len, cache_len + L, dtype=mx.int32)[None, :] | |
| else: | |
| position_ids = mx.arange(0, L, dtype=mx.int32)[None, :] | |
| hidden_states = inputs_embeds | |
| new_kv_cache = [] if use_cache else None | |
| for i, layer in enumerate(self.layers): | |
| pkv = past_key_values[i] if past_key_values else None | |
| hidden_states, nkv = layer( | |
| hidden_states, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| past_key_value=pkv, | |
| use_cache=use_cache, | |
| ) | |
| if use_cache: | |
| new_kv_cache.append(nkv) | |
| hidden_states = self.norm(hidden_states) | |
| return hidden_states, new_kv_cache | |
| # ============================================================================= | |
| # SAM-ViT-B Vision Encoder | |
| # ============================================================================= | |
| class SAMAttention(nn.Module): | |
| """SAM attention block with relative position bias.""" | |
| def __init__( | |
| self, | |
| dim: int, | |
| num_heads: int, | |
| window_size: int = 0, | |
| use_rel_pos: bool = True, | |
| input_size: Tuple[int, int] = (64, 64), | |
| ): | |
| super().__init__() | |
| self.num_heads = num_heads | |
| self.head_dim = dim // num_heads | |
| self.window_size = window_size | |
| self.scale = self.head_dim ** -0.5 | |
| self.qkv = nn.Linear(dim, dim * 3, bias=True) | |
| self.proj = nn.Linear(dim, dim, bias=True) | |
| self.use_rel_pos = use_rel_pos | |
| if use_rel_pos: | |
| self.rel_pos_h = mx.zeros((2 * input_size[0] - 1, self.head_dim)) | |
| self.rel_pos_w = mx.zeros((2 * input_size[1] - 1, self.head_dim)) | |
| def _get_rel_pos(self, H: int, W: int) -> mx.array: | |
| """Compute relative position bias.""" | |
| if not self.use_rel_pos or self.window_size > 0: | |
| return 0.0 | |
| # Height relative positions | |
| h_coords = mx.arange(H) | |
| h_rel = h_coords[:, None] - h_coords[None, :] + (H - 1) | |
| rh = self.rel_pos_h[h_rel] # [H, H, head_dim] | |
| # Weight relative positions | |
| w_coords = mx.arange(W) | |
| w_rel = w_coords[:, None] - w_coords[None, :] + (W - 1) | |
| rw = self.rel_pos_w[w_rel] # [W, W, head_dim] | |
| # Combine: for each head, compute Q @ R.T for all positions | |
| # Simplified: compute rel_pos as additive bias | |
| # rel_pos: [H*W, H*W] | |
| Rh = rh.reshape(H, 1, H, 1, self.head_dim).transpose(0, 3, 1, 2, 4) | |
| Rw = rw.reshape(1, W, 1, W, self.head_dim).transpose(0, 3, 1, 2, 4) | |
| return 0.0 # Simplified - full rel pos computation omitted for brevity | |
| def __call__(self, x: mx.array) -> mx.array: | |
| B, N, C = x.shape | |
| H = W = int(N ** 0.5) | |
| qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim) | |
| q, k, v = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2] | |
| q = q.transpose(0, 2, 1, 3) # [B, heads, N, head_dim] | |
| k = k.transpose(0, 2, 1, 3) | |
| v = v.transpose(0, 2, 1, 3) | |
| # Window attention | |
| if self.window_size > 0: | |
| attn = self._window_attention(q, k, v, H, W) | |
| else: | |
| attn = (q @ k.transpose(0, 1, 3, 2)) * self.scale | |
| attn = mx.softmax(attn.astype(mx.float32), axis=-1).astype(q.dtype) | |
| attn = attn @ v | |
| attn = attn.transpose(0, 2, 1, 3).reshape(B, N, C) | |
| return self.proj(attn) | |
| def _window_attention(self, q, k, v, H, W): | |
| """Window-based attention for SAM blocks with padding support.""" | |
| B, heads, N, d = q.shape | |
| ws = self.window_size | |
| # Pad if needed | |
| pad_h = (ws - H % ws) % ws | |
| pad_w = (ws - W % ws) % ws | |
| Hp, Wp = H + pad_h, W + pad_w | |
| def pad_tensor(x, H, W, pad_h, pad_w): | |
| # x: [B, heads, H*W, d] | |
| x = x.reshape(B, heads, H, W, d) | |
| if pad_h > 0 or pad_w > 0: | |
| x = mx.pad(x, [(0, 0), (0, 0), (0, pad_h), (0, pad_w), (0, 0)]) | |
| return x | |
| q_p = pad_tensor(q, H, W, pad_h, pad_w) | |
| k_p = pad_tensor(k, H, W, pad_h, pad_w) | |
| v_p = pad_tensor(v, H, W, pad_h, pad_w) | |
| # Now partition into windows | |
| nw_h, nw_w = Hp // ws, Wp // ws | |
| def window_partition(x): | |
| # x: [B, heads, Hp, Wp, d] | |
| x = x.reshape(B, heads, nw_h, ws, nw_w, ws, d) | |
| x = x.transpose(0, 1, 2, 4, 3, 5, 6) # [B, heads, nw_h, nw_w, ws, ws, d] | |
| x = x.reshape(B * nw_h * nw_w, heads, ws * ws, d) | |
| return x | |
| def window_reverse(x): | |
| x = x.reshape(B, heads, nw_h, nw_w, ws, ws, d) | |
| x = x.transpose(0, 1, 2, 4, 3, 5, 6) # [B, heads, nw_h, ws, nw_w, ws, d] | |
| x = x.reshape(B, heads, Hp, Wp, d) | |
| return x | |
| q_w = window_partition(q_p) | |
| k_w = window_partition(k_p) | |
| v_w = window_partition(v_p) | |
| attn = (q_w @ k_w.transpose(0, 1, 3, 2)) * self.scale | |
| attn = mx.softmax(attn.astype(mx.float32), axis=-1).astype(q.dtype) | |
| out_w = attn @ v_w | |
| out = window_reverse(out_w) | |
| # Crop back to original size | |
| if pad_h > 0: | |
| out = out[:, :, :H, :, :] | |
| if pad_w > 0: | |
| out = out[:, :, :, :W, :] | |
| out = out.reshape(B, heads, H * W, d) | |
| return out | |
| class SAMMLP(nn.Module): | |
| """SAM MLP block.""" | |
| def __init__(self, dim: int, mlp_dim: int): | |
| super().__init__() | |
| self.lin1 = nn.Linear(dim, mlp_dim) | |
| self.lin2 = nn.Linear(mlp_dim, dim) | |
| def __call__(self, x): | |
| return self.lin2(nn.gelu(self.lin1(x))) | |
| class SAMBlock(nn.Module): | |
| """SAM ViT block.""" | |
| def __init__( | |
| self, | |
| dim: int, | |
| num_heads: int, | |
| mlp_ratio: float = 4.0, | |
| window_size: int = 0, | |
| use_rel_pos: bool = True, | |
| input_size: Tuple[int, int] = (64, 64), | |
| ): | |
| super().__init__() | |
| self.norm1 = nn.LayerNorm(dim, eps=1e-6) | |
| self.attn = SAMAttention( | |
| dim, num_heads, | |
| window_size=window_size, | |
| use_rel_pos=use_rel_pos, | |
| input_size=input_size, | |
| ) | |
| self.norm2 = nn.LayerNorm(dim, eps=1e-6) | |
| self.mlp = SAMMLP(dim, int(dim * mlp_ratio)) | |
| def __call__(self, x): | |
| x = x + self.attn(self.norm1(x)) | |
| x = x + self.mlp(self.norm2(x)) | |
| return x | |
| class PatchEmbed(nn.Module): | |
| """Patch embedding for SAM. Uses NHWC format for MLX.""" | |
| def __init__(self, kernel_size=16, stride=16, in_chans=3, embed_dim=768): | |
| super().__init__() | |
| self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size, stride=stride, bias=True) | |
| def __call__(self, x): | |
| # x: [B, H, W, C] (NHWC) | |
| return self.proj(x) | |
| class SAMVisionEncoder(nn.Module): | |
| """SAM-ViT-B vision encoder.""" | |
| def __init__(self, config: VisionConfig): | |
| super().__init__() | |
| self.img_size = config.sam_img_size | |
| self.patch_size = config.sam_patch_size | |
| grid_size = self.img_size // self.patch_size # 64 | |
| self.patch_embed = PatchEmbed( | |
| kernel_size=config.sam_patch_size, | |
| stride=config.sam_patch_size, | |
| in_chans=3, | |
| embed_dim=config.sam_embed_dim, | |
| ) | |
| self.pos_embed = mx.zeros((1, grid_size, grid_size, config.sam_embed_dim)) | |
| input_size = (grid_size, grid_size) | |
| self.blocks = [] | |
| for i in range(config.sam_depth): | |
| use_global = i in config.sam_global_attn_indexes | |
| window_size = 0 if use_global else config.sam_window_size | |
| self.blocks.append(SAMBlock( | |
| dim=config.sam_embed_dim, | |
| num_heads=config.sam_num_heads, | |
| mlp_ratio=config.sam_mlp_ratio, | |
| window_size=window_size, | |
| input_size=input_size, | |
| )) | |
| # Neck | |
| self.neck = nn.Sequential( | |
| nn.Conv2d(config.sam_embed_dim, config.sam_out_chans, 1, bias=False), | |
| nn.LayerNorm(config.sam_out_chans, eps=1e-6), | |
| nn.Conv2d(config.sam_out_chans, config.sam_out_chans, 3, padding=1, bias=False), | |
| nn.LayerNorm(config.sam_out_chans, eps=1e-6), | |
| ) | |
| # Downsampling convolutions | |
| self.net_2 = nn.Conv2d(256, 512, 3, stride=2, padding=1, bias=False) | |
| self.net_3 = nn.Conv2d(512, 1024, 3, stride=2, padding=1, bias=False) | |
| def __call__(self, x: mx.array) -> mx.array: | |
| # x: [B, H, W, C] (NHWC format for MLX) | |
| B, H_in, W_in, C_in = x.shape | |
| x = self.patch_embed(x) # [B, H_p, W_p, 768] | |
| H_p, W_p = x.shape[1], x.shape[2] | |
| # Add positional embedding (flatten to sequence) | |
| x = x.reshape(B, H_p * W_p, -1) # [B, N, 768] | |
| if self.pos_embed.shape[1] != H_p: | |
| pos = _interpolate_pos_embed(self.pos_embed, H_p) | |
| else: | |
| pos = self.pos_embed | |
| pos = pos.reshape(1, H_p * W_p, -1) | |
| x = x + pos | |
| for blk in self.blocks: | |
| x = blk(x) | |
| # Back to NHWC for convolution | |
| x = x.reshape(B, H_p, W_p, -1) # [B, 64, 64, 768] | |
| # Neck (Conv2d with NHWC) | |
| x = self.neck(x) # [B, 64, 64, 256] | |
| # Downsampling (NHWC) | |
| x = self.net_2(x) # [B, 32, 32, 512] | |
| x = self.net_3(x) # [B, 16, 16, 1024] | |
| # Return in NHWC then convert to NCHW for CLIP compatibility | |
| return x | |
| def _interpolate_pos_embed(pos_embed, target_size): | |
| """Interpolate position embeddings to target grid size.""" | |
| # pos_embed: [1, src, src, dim] | |
| B = pos_embed.shape[0] | |
| src = pos_embed.shape[1] | |
| dim = pos_embed.shape[-1] | |
| # Reshape to [B, dim, src, src] | |
| x = pos_embed.transpose(0, 3, 1, 2) | |
| # Simple interpolation using reshape | |
| # MLX doesn't have native interpolate, use simple scaling | |
| x = x.reshape(B, dim, src * src) | |
| x = x.reshape(B, dim, target_size, target_size) | |
| x = x.transpose(0, 2, 3, 1) | |
| return x | |
| # ============================================================================= | |
| # CLIP-L Vision Encoder | |
| # ============================================================================= | |
| class CLIPAttention(nn.Module): | |
| """CLIP multi-head self-attention.""" | |
| def __init__(self, hidden_size: int, num_heads: int): | |
| super().__init__() | |
| self.num_heads = num_heads | |
| self.head_dim = hidden_size // num_heads | |
| self.qkv_proj = nn.Linear(hidden_size, hidden_size * 3, bias=True) | |
| self.out_proj = nn.Linear(hidden_size, hidden_size, bias=True) | |
| self.scale = self.head_dim ** -0.5 | |
| def __call__(self, x): | |
| B, N, C = x.shape | |
| qkv = self.qkv_proj(x).reshape(B, N, 3, self.num_heads, self.head_dim) | |
| q, k, v = qkv[:, :, 0].transpose(0, 2, 1, 3), qkv[:, :, 1].transpose(0, 2, 1, 3), qkv[:, :, 2].transpose(0, 2, 1, 3) | |
| attn = (q @ k.transpose(0, 1, 3, 2)) * self.scale | |
| attn = mx.softmax(attn.astype(mx.float32), axis=-1).astype(q.dtype) | |
| out = attn @ v | |
| out = out.transpose(0, 2, 1, 3).reshape(B, N, C) | |
| return self.out_proj(out) | |
| class CLIPMLP(nn.Module): | |
| """CLIP MLP with QuickGELU.""" | |
| def __init__(self, hidden_size: int, ffn_hidden_size: int): | |
| super().__init__() | |
| self.fc1 = nn.Linear(hidden_size, ffn_hidden_size, bias=True) | |
| self.fc2 = nn.Linear(ffn_hidden_size, hidden_size, bias=True) | |
| def __call__(self, x): | |
| # QuickGELU: fc1 → QuickGELU → fc2 | |
| h = self.fc1(x) | |
| h = h * mx.sigmoid(1.702 * h) | |
| return self.fc2(h) | |
| class CLIPTransformerLayer(nn.Module): | |
| """CLIP transformer layer.""" | |
| def __init__(self, hidden_size: int, num_heads: int, ffn_hidden_size: int, eps: float = 1e-5): | |
| super().__init__() | |
| self.layer_norm1 = nn.LayerNorm(hidden_size, eps=eps) | |
| self.self_attn = CLIPAttention(hidden_size, num_heads) | |
| self.layer_norm2 = nn.LayerNorm(hidden_size, eps=eps) | |
| self.mlp = CLIPMLP(hidden_size, ffn_hidden_size) | |
| def __call__(self, x): | |
| x = x + self.self_attn(self.layer_norm1(x)) | |
| x = x + self.mlp(self.layer_norm2(x)) | |
| return x | |
| class CLIPVisionEmbeddings(nn.Module): | |
| """CLIP vision embeddings that takes SAM features as input.""" | |
| def __init__(self, hidden_size: int = 1024, image_size: int = 224, patch_size: int = 14): | |
| super().__init__() | |
| self.embed_dim = hidden_size | |
| self.image_size = image_size | |
| self.patch_size = patch_size | |
| self.num_patches = (image_size // patch_size) ** 2 | |
| self.num_positions = self.num_patches + 1 | |
| self.class_embedding = mx.zeros((hidden_size,)) | |
| # Patch embedding (projects SAM features) - NHWC conv | |
| self.patch_embedding = nn.Conv2d(3, hidden_size, patch_size, stride=patch_size, bias=False) | |
| # Position embedding | |
| self.position_embedding = nn.Embedding(self.num_positions, hidden_size) | |
| self.position_ids = mx.arange(self.num_positions)[None, :] | |
| def __call__(self, pixel_values, patch_embeds=None): | |
| batch_size = pixel_values.shape[0] | |
| if patch_embeds is not None: | |
| # Use pre-computed SAM features | |
| # patch_embeds: [B, H, W, C] (NHWC from SAM) | |
| B, H, W, C = patch_embeds.shape | |
| patch_embeds = patch_embeds.reshape(B, H * W, C) | |
| else: | |
| # Use raw conv on NHWC input | |
| patch_embeds = self.patch_embedding(pixel_values) | |
| B, H, W, C = patch_embeds.shape | |
| patch_embeds = patch_embeds.reshape(B, H * W, C) | |
| class_embeds = mx.tile(self.class_embedding.reshape(1, 1, -1), (batch_size, 1, 1)) | |
| embeddings = mx.concatenate([class_embeds, patch_embeds], axis=1) | |
| # Add position embeddings with interpolation | |
| pos_ids = self.position_ids[:, :embeddings.shape[1]] | |
| pos_embeds = self.position_embedding(pos_ids) | |
| embeddings = embeddings + pos_embeds | |
| return embeddings | |
| class CLIPVisionTransformer(nn.Module): | |
| """CLIP-L vision transformer.""" | |
| def __init__(self, config: VisionConfig): | |
| super().__init__() | |
| self.embeddings = CLIPVisionEmbeddings( | |
| hidden_size=config.clip_hidden_size, | |
| image_size=config.clip_image_size, | |
| patch_size=config.clip_patch_size, | |
| ) | |
| self.pre_layrnorm = nn.LayerNorm(config.clip_hidden_size, eps=config.clip_layernorm_epsilon) | |
| self.transformer = nn.Sequential(*[ | |
| CLIPTransformerLayer( | |
| config.clip_hidden_size, | |
| config.clip_num_heads, | |
| config.clip_ffn_hidden_size, | |
| eps=config.clip_layernorm_epsilon, | |
| ) | |
| for _ in range(config.clip_num_layers) | |
| ]) | |
| def __call__(self, pixel_values, patch_embeds=None): | |
| x = self.embeddings(pixel_values, patch_embeds) | |
| x = self.pre_layrnorm(x) | |
| x = self.transformer(x) | |
| return x | |
| # ============================================================================= | |
| # Projector | |
| # ============================================================================= | |
| class MlpProjector(nn.Module): | |
| """Linear projector from vision to language space.""" | |
| def __init__(self, config: ProjectorConfig): | |
| super().__init__() | |
| self.layers = nn.Linear(config.input_dim, config.n_embed, bias=True) | |
| def __call__(self, x): | |
| return self.layers(x) | |
| # ============================================================================= | |
| # Unlimited OCR Model | |
| # ============================================================================= | |
| class ModelOutput: | |
| logits: mx.array | |
| past_key_values: Optional[List[Tuple[mx.array, mx.array]]] = None | |
| class UnlimitedOCRModel(nn.Module): | |
| """Complete Unlimited-OCR model with vision + language. | |
| Architecture: | |
| Image → SAM-ViT-B → CLIP-L → Projector → DeepSeek-V2 MoE → Text | |
| """ | |
| def __init__(self, config: UnlimitedOCRConfig): | |
| super().__init__() | |
| self.config = config | |
| # Vision | |
| self.sam_model = SAMVisionEncoder(config.vision) | |
| self.vision_model = CLIPVisionTransformer(config.vision) | |
| # Projector: 2048 → 1280 | |
| self.projector = MlpProjector(config.projector) | |
| # Language | |
| self.language_model = DeepSeekModel(config.language) | |
| self.lm_head = nn.Linear(config.language.hidden_size, config.language.vocab_size, bias=False) | |
| # Image special tokens | |
| embed_std = 1.0 / math.sqrt(config.language.hidden_size) | |
| self.image_newline = mx.random.normal((config.language.hidden_size,)) * embed_std | |
| self.view_seperator = mx.random.normal((config.language.hidden_size,)) * embed_std | |
| def encode_images(self, images: mx.array, images_spatial_crop=None) -> List[mx.array]: | |
| """Encode images through vision encoder. | |
| Args: | |
| images: List of [patches, original] image tensors (in NCHW from preprocessing) | |
| images_spatial_crop: List of (width_crops, height_crops) tuples | |
| Returns: | |
| List of image feature tensors [N, hidden_size] | |
| """ | |
| all_features = [] | |
| for idx, image_pair in enumerate(images): | |
| patches = image_pair[0] # [N, 3, 640, 640] NCHW | |
| image_ori = image_pair[1] # [1, 3, 1024, 1024] NCHW | |
| has_patches = patches is not None and patches.shape[0] > 0 | |
| # Convert to NHWC for MLX conv | |
| def to_nhwc(t): | |
| if t is None: | |
| return None | |
| ndim = len(t.shape) | |
| if ndim == 4: | |
| return t.transpose(0, 2, 3, 1) # NCHW → NHWC | |
| return t | |
| patches_nhwc = to_nhwc(patches) | |
| image_ori_nhwc = to_nhwc(image_ori) | |
| if has_patches and images_spatial_crop is not None: | |
| crop_shape = images_spatial_crop[idx] | |
| width_crop_num, height_crop_num = crop_shape | |
| # Process patches (local features) | |
| sam_local = self.sam_model(patches_nhwc) # [P, 16, 16, 1024] | |
| clip_local = self.vision_model(patches_nhwc, sam_local) # [P, 257, 1024] | |
| # Combine: CLIP[:, 1:] + SAM flatten | |
| # SAM: [P, 16, 16, 1024] → [P, 256, 1024] | |
| sam_flat = sam_local.reshape(patches.shape[0], -1, 1024) | |
| local_feats = mx.concatenate([ | |
| clip_local[:, 1:, :], # [P, 256, 1024] | |
| sam_flat, # [P, 256, 1024] | |
| ], axis=-1) # [P, 256, 2048] | |
| local_feats = self.projector(local_feats) # [P, 256, 1280] | |
| # Process original (global features) | |
| sam_global = self.sam_model(image_ori_nhwc) # [1, 16, 16, 1024] | |
| clip_global = self.vision_model(image_ori_nhwc, sam_global) # [1, 257, 1024] | |
| sam_gflat = sam_global.reshape(1, -1, 1024) | |
| global_feats = mx.concatenate([ | |
| clip_global[:, 1:, :], # [1, 256, 1024] | |
| sam_gflat, # [1, 256, 1024] | |
| ], axis=-1) # [1, 256, 2048] | |
| global_feats = self.projector(global_feats) # [1, 256, 1280] | |
| # Reshape and organize | |
| _, hw_g, nd = global_feats.shape | |
| h_g = w_g = int(hw_g ** 0.5) | |
| _, hw_l, nd2 = local_feats.shape | |
| h_l = w_l = int(hw_l ** 0.5) | |
| # Global: reshape to 2D and add newlines | |
| gf = global_feats.reshape(h_g, w_g, nd) | |
| gf = mx.concatenate([gf, mx.tile(self.image_newline[None, None, :], (h_g, 1, 1))], axis=1) | |
| gf = gf.reshape(-1, nd) | |
| # Local: reshape grid | |
| lf = local_feats.reshape(height_crop_num, width_crop_num, h_l, w_l, nd2) | |
| lf = lf.transpose(0, 2, 1, 3, 4).reshape(height_crop_num * h_l, width_crop_num * w_l, nd2) | |
| lf = mx.concatenate([lf, mx.tile(self.image_newline[None, None, :], (height_crop_num * h_l, 1, 1))], axis=1) | |
| lf = lf.reshape(-1, nd2) | |
| # Concat: local + global + separator | |
| full_feats = mx.concatenate([lf, gf, self.view_seperator[None, :]], axis=0) | |
| all_features.append(full_feats) | |
| else: | |
| # Multiple images or single image without crop | |
| if len(image_ori_nhwc.shape) == 3: | |
| image_ori_nhwc = image_ori_nhwc[None, :, :, :] | |
| num_imgs = image_ori_nhwc.shape[0] | |
| for i in range(num_imgs): | |
| img = image_ori_nhwc[i:i+1] | |
| sam_out = self.sam_model(img) | |
| clip_out = self.vision_model(img, sam_out) | |
| sam_flat = sam_out.reshape(1, -1, 1024) | |
| gf = mx.concatenate([ | |
| clip_out[:, 1:, :], | |
| sam_flat, | |
| ], axis=-1) | |
| gf = self.projector(gf) | |
| _, hw, nd = gf.shape | |
| h = w = int(hw ** 0.5) | |
| gf_2d = gf.reshape(h, w, nd) | |
| gf_2d = mx.concatenate([gf_2d, mx.tile(self.image_newline[None, None, :], (h, 1, 1))], axis=1) | |
| gf_2d = gf_2d.reshape(-1, nd) | |
| full_feats = mx.concatenate([gf_2d, self.view_seperator[None, :]], axis=0) | |
| all_features.append(full_feats) | |
| return all_features | |
| def __call__( | |
| self, | |
| input_ids: Optional[mx.array] = None, | |
| attention_mask: Optional[mx.array] = None, | |
| position_ids: Optional[mx.array] = None, | |
| past_key_values: Optional[List[Tuple[mx.array, mx.array]]] = None, | |
| inputs_embeds: Optional[mx.array] = None, | |
| images: Optional[List[mx.array]] = None, | |
| images_seq_mask: Optional[mx.array] = None, | |
| images_spatial_crop: Optional[List[Tuple[int, int]]] = None, | |
| use_cache: bool = False, | |
| ) -> ModelOutput: | |
| B = input_ids.shape[0] if input_ids is not None else inputs_embeds.shape[0] | |
| if inputs_embeds is None: | |
| inputs_embeds = self.language_model.embed_tokens(input_ids) | |
| # Inject image features into embeddings | |
| if images is not None and images_seq_mask is not None: | |
| image_features = self.encode_images(images, images_spatial_crop) | |
| for idx, img_feats in enumerate(image_features): | |
| if img_feats is not None and img_feats.shape[0] > 0: | |
| mask = images_seq_mask[idx].reshape(-1, 1) | |
| # Scatter image features into positions where mask is True | |
| inputs_embeds = inputs_embeds.at[idx].set( | |
| mx.where(mask, img_feats, inputs_embeds[idx]) | |
| ) | |
| hidden_states, new_kv = self.language_model( | |
| input_ids=None, | |
| inputs_embeds=inputs_embeds, | |
| attention_mask=attention_mask, | |
| position_ids=position_ids, | |
| past_key_values=past_key_values, | |
| use_cache=use_cache, | |
| ) | |
| logits = self.lm_head(hidden_states) | |
| return ModelOutput(logits=logits, past_key_values=new_kv) | |
| def generate( | |
| self, | |
| input_ids: mx.array, | |
| images: Optional[List] = None, | |
| images_seq_mask: Optional[mx.array] = None, | |
| images_spatial_crop: Optional[List] = None, | |
| max_length: int = 32768, | |
| temperature: float = 0.0, | |
| eos_token_id: int = 1, | |
| ) -> mx.array: | |
| """Autoregressive text generation.""" | |
| generated = [input_ids] | |
| past_kv = None | |
| use_images = (images is not None) | |
| for step in range(max_length): | |
| if step == 0: | |
| # Prefill: process full sequence with images | |
| output = self( | |
| input_ids=input_ids, | |
| images=images if use_images else None, | |
| images_seq_mask=images_seq_mask if use_images else None, | |
| images_spatial_crop=images_spatial_crop if use_images else None, | |
| use_cache=True, | |
| ) | |
| else: | |
| # Decode: process only the last token | |
| output = self( | |
| input_ids=input_ids[:, -1:], | |
| past_key_values=past_kv, | |
| use_cache=True, | |
| ) | |
| past_kv = output.past_key_values | |
| logits = output.logits[:, -1, :] | |
| if temperature > 0: | |
| logits = logits / temperature | |
| probs = mx.softmax(logits.astype(mx.float32), axis=-1) | |
| next_token = mx.random.categorical(probs, axis=-1).reshape(1, 1) | |
| else: | |
| next_token = mx.argmax(logits, axis=-1, keepdims=True) | |
| generated.append(next_token) | |
| input_ids = next_token | |
| if next_token.item() == eos_token_id: | |
| break | |
| return mx.concatenate(generated, axis=1) | |