Buckets:
| """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, 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 | |
| # position_ids carry ABSOLUTE positions. Index the full table directly; | |
| # slicing by seq_len first would break decode (abs pos >= seq_len) and | |
| # is required for correctness under R-SWA KV eviction. | |
| if position_ids is not None: | |
| return cos[position_ids], sin[position_ids] | |
| if seq_len is not None: | |
| cos, sin = cos[:seq_len], sin[:seq_len] | |
| 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 _stack_experts(self): | |
| """Lazily stack expert weights for gather-based inference (cached).""" | |
| if getattr(self, "_GW", None) is None: | |
| self._GW = mx.stack([e.gate_proj.weight for e in self.experts]) # [E, M, D] | |
| self._UW = mx.stack([e.up_proj.weight for e in self.experts]) # [E, M, D] | |
| self._DW = mx.stack([e.down_proj.weight for e in self.experts]) # [E, D, M] | |
| return self._GW, self._UW, self._DW | |
| 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 | |
| T = B * L | |
| K = self.num_experts_per_tok | |
| x_flat = x.reshape(T, D) | |
| ids = topk_ids.reshape(T, K) | |
| tw = topk_weights.reshape(T, K) | |
| # 解码步 (T 小): 纯 gather+einsum, 无 CPU 同步、无 Python 循环 | |
| if T <= 32: | |
| GW, UW, DW = self._stack_experts() | |
| idf = ids.reshape(-1) # [T*K] | |
| xe = mx.repeat(x_flat, K, axis=0) # [T*K, D] | |
| gw = mx.take(GW, idf, axis=0) # [T*K, M, D] | |
| uw = mx.take(UW, idf, axis=0) # [T*K, M, D] | |
| dw = mx.take(DW, idf, axis=0) # [T*K, D, M] | |
| h = silu(mx.einsum("nmd,nd->nm", gw, xe)) * mx.einsum("nmd,nd->nm", uw, xe) | |
| o = mx.einsum("ndm,nm->nd", dw, h) # [T*K, D] | |
| o = o.reshape(T, K, D) * tw[:, :, None] | |
| return o.sum(axis=1).reshape(B, L, D) | |
| # 预填充 (T 大): 按专家分组, gather 全权重会爆显存, 故保留循环 | |
| tk_flat = ids.reshape(-1) | |
| tw_flat = tw.reshape(-1) | |
| import numpy as np | |
| token_counts = np.bincount(np.array(tk_flat, dtype=np.int32), | |
| minlength=self.n_routed_experts) | |
| sort_indices = mx.argsort(tk_flat) | |
| repeated_x = mx.repeat(x_flat, K, axis=0) | |
| sorted_tokens = repeated_x[sort_indices] | |
| sorted_weights = tw_flat[sort_indices] | |
| 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]) | |
| 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) | |
| 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 = mx.argsort(all_indices) | |
| final = all_outputs[restore] | |
| final = final.reshape(T, K, 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 | |
| # ============================================================================= | |
| def _interp_linear(rel_pos: mx.array, out_size: int) -> mx.array: | |
| """1D linear interpolation of a rel-pos table (align_corners=False).""" | |
| L, C = rel_pos.shape | |
| if L == out_size: | |
| return rel_pos | |
| scale = L / out_size | |
| dst = mx.arange(out_size).astype(mx.float32) | |
| src = mx.clip((dst + 0.5) * scale - 0.5, 0.0, L - 1.0) | |
| lo = mx.floor(src).astype(mx.int32) | |
| hi = mx.minimum(lo + 1, L - 1) | |
| w = (src - lo.astype(mx.float32))[:, None] | |
| return rel_pos[lo] * (1.0 - w) + rel_pos[hi] * w | |
| def _get_rel_pos(q_size: int, k_size: int, rel_pos: mx.array) -> mx.array: | |
| """Standard SAM get_rel_pos: interpolate + gather by relative coords.""" | |
| max_rel_dist = 2 * max(q_size, k_size) - 1 | |
| rel_pos_resized = _interp_linear(rel_pos, max_rel_dist) | |
| q_coords = mx.arange(q_size)[:, None] * max(k_size / q_size, 1.0) | |
| k_coords = mx.arange(k_size)[None, :] * max(q_size / k_size, 1.0) | |
| relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0) | |
| return rel_pos_resized[relative_coords.astype(mx.int32)] | |
| def _add_decomposed_rel_pos(attn, q, rel_pos_h, rel_pos_w, q_size, k_size): | |
| """Add decomposed relative position bias to attention (standard SAM).""" | |
| q_h, q_w = q_size | |
| k_h, k_w = k_size | |
| Rh = _get_rel_pos(q_h, k_h, rel_pos_h) # [q_h, k_h, C] | |
| Rw = _get_rel_pos(q_w, k_w, rel_pos_w) # [q_w, k_w, C] | |
| B, _, dim = q.shape | |
| r_q = q.reshape(B, q_h, q_w, dim) | |
| rel_h = mx.einsum("bhwc,hkc->bhwk", r_q, Rh) | |
| rel_w = mx.einsum("bhwc,wkc->bhwk", r_q, Rw) | |
| attn = attn.reshape(B, q_h, q_w, k_h, k_w) | |
| attn = attn + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :] | |
| return attn.reshape(B, q_h * q_w, k_h * k_w) | |
| def _window_partition(x: mx.array, window_size: int): | |
| """Partition [B,H,W,C] into windows [B*nw, ws, ws, C] with padding.""" | |
| B, H, W, C = x.shape | |
| pad_h = (window_size - H % window_size) % window_size | |
| pad_w = (window_size - W % window_size) % window_size | |
| if pad_h > 0 or pad_w > 0: | |
| x = mx.pad(x, [(0, 0), (0, pad_h), (0, pad_w), (0, 0)]) | |
| Hp, Wp = H + pad_h, W + pad_w | |
| x = x.reshape(B, Hp // window_size, window_size, Wp // window_size, window_size, C) | |
| windows = x.transpose(0, 1, 3, 2, 4, 5).reshape(-1, window_size, window_size, C) | |
| return windows, (Hp, Wp) | |
| def _window_unpartition(windows, window_size, pad_hw, hw): | |
| """Inverse of _window_partition; crop back to original [B,H,W,C].""" | |
| Hp, Wp = pad_hw | |
| H, W = hw | |
| B = windows.shape[0] // (Hp * Wp // window_size // window_size) | |
| x = windows.reshape(B, Hp // window_size, Wp // window_size, window_size, window_size, -1) | |
| x = x.transpose(0, 1, 3, 2, 4, 5).reshape(B, Hp, Wp, -1) | |
| if Hp > H or Wp > W: | |
| x = x[:, :H, :W, :] | |
| return x | |
| 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 __call__(self, x: mx.array) -> mx.array: | |
| # x: [B, H, W, C] spatial format (standard SAM) | |
| B, H, W, C = x.shape | |
| qkv = self.qkv(x).reshape(B, H * W, 3, self.num_heads, self.head_dim) | |
| qkv = qkv.transpose(2, 0, 3, 1, 4).reshape(3, B * self.num_heads, H * W, self.head_dim) | |
| q, k, v = qkv[0], qkv[1], qkv[2] | |
| attn = (q * self.scale) @ k.transpose(0, 2, 1) | |
| if self.use_rel_pos: | |
| attn = _add_decomposed_rel_pos( | |
| attn, q, self.rel_pos_h, self.rel_pos_w, (H, W), (H, W) | |
| ) | |
| attn = mx.softmax(attn.astype(mx.float32), axis=-1).astype(q.dtype) | |
| out = attn @ v # [B*heads, H*W, head_dim] | |
| out = out.reshape(B, self.num_heads, H, W, self.head_dim) | |
| out = out.transpose(0, 2, 3, 1, 4).reshape(B, H, W, C) | |
| return self.proj(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.window_size = window_size | |
| 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: [B, H, W, C] spatial format | |
| shortcut = x | |
| x = self.norm1(x) | |
| # Window partition | |
| if self.window_size > 0: | |
| H, W = x.shape[1], x.shape[2] | |
| x, pad_hw = _window_partition(x, self.window_size) | |
| x = self.attn(x) | |
| # Reverse windows | |
| if self.window_size > 0: | |
| x = _window_unpartition(x, self.window_size, pad_hw, (H, W)) | |
| x = shortcut + 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.window_size = config.sam_window_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 | |
| # rel_pos table size follows the attention span: | |
| # global blocks see the full grid; window blocks see one window. | |
| blk_input_size = input_size if use_global else (window_size, 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=blk_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 (spatial 2D, bilinear interpolated to grid) | |
| if self.pos_embed.shape[1] != H_p: | |
| pos = _interpolate_pos_embed(self.pos_embed, H_p) | |
| else: | |
| pos = self.pos_embed | |
| x = x + pos # [B, H_p, W_p, 768] | |
| # Transformer blocks operate in spatial format | |
| for blk in self.blocks: | |
| x = blk(x) | |
| # 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 x | |
| def _interpolate_pos_embed(pos_embed, target_size): | |
| """Bilinear interpolation of [1, src, src, dim] pos embed to target grid.""" | |
| src = pos_embed.shape[1] | |
| if src == target_size: | |
| return pos_embed | |
| # interpolate rows then cols using the shared 1D linear helper | |
| dim = pos_embed.shape[-1] | |
| x = pos_embed[0] # [src, src, dim] | |
| # rows | |
| x = _interp_linear(x.reshape(src, src * dim), target_size).reshape(target_size, src, dim) | |
| # cols | |
| x = x.transpose(1, 0, 2).reshape(src, target_size * dim) | |
| x = _interp_linear(x, target_size).reshape(target_size, target_size, dim).transpose(1, 0, 2) | |
| return x[None] | |
| # ============================================================================= | |
| # 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) | |
| rows = [] | |
| for idx, img_feats in enumerate(image_features): | |
| emb = inputs_embeds[idx] # [L, D] | |
| if img_feats is not None and img_feats.shape[0] > 0: | |
| mask = images_seq_mask[idx] # [L] bool | |
| # first True position (image feature block is contiguous) | |
| start = int(mx.argmax(mask.astype(mx.int32)).item()) | |
| n = img_feats.shape[0] | |
| # replace the [start:start+n] rows with image features | |
| emb = mx.concatenate([emb[:start], img_feats, emb[start + n:]], axis=0) | |
| rows.append(emb[None]) | |
| inputs_embeds = mx.concatenate(rows, axis=0) | |
| 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) | |
| def step_fn(ids, pkv, first): | |
| if first: | |
| out = self( | |
| input_ids=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: | |
| out = self(input_ids=ids, past_key_values=pkv, use_cache=True) | |
| logits = out.logits[:, -1, :] | |
| if temperature > 0: | |
| probs = mx.softmax((logits / temperature).astype(mx.float32), axis=-1) | |
| nt = mx.random.categorical(probs, axis=-1).reshape(1, 1) | |
| else: | |
| nt = mx.argmax(logits, axis=-1, keepdims=True) | |
| return nt, out.past_key_values | |
| # 流水线解码:GPU 算下一步时不阻塞等 CPU 检查 EOS(async_eval) | |
| next_token, past_kv = step_fn(input_ids, None, True) | |
| mx.async_eval(next_token, *[kv for pair in past_kv for kv in pair]) | |
| for step in range(max_length): | |
| cur = next_token | |
| if step + 1 < max_length: | |
| next_token, past_kv = step_fn(cur, past_kv, False) | |
| mx.async_eval(next_token, *[kv for pair in past_kv for kv in pair]) | |
| tok_id = cur.item() # 此时 cur 已在上一轮 async_eval 中求值完毕 | |
| if tok_id == eos_token_id: | |
| break | |
| generated.append(cur) | |
| return mx.concatenate(generated, axis=1) | |
| def generate_batch( | |
| 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 = 8192, | |
| eos_token_id: int = 1, | |
| ) -> mx.array: | |
| """批量贪心解码。要求 batch 内各行序列等长(同 crop_shape 分桶即可)。 | |
| 返回 [B, T] 的生成 token(不含 prompt);各行遇 EOS 后填充 EOS,调用方按行截断。 | |
| """ | |
| B = input_ids.shape[0] | |
| eos = mx.array(eos_token_id, dtype=input_ids.dtype) | |
| # Prefill | |
| out = self( | |
| input_ids=input_ids, | |
| images=images, | |
| images_seq_mask=images_seq_mask, | |
| images_spatial_crop=images_spatial_crop, | |
| use_cache=True, | |
| ) | |
| past_kv = out.past_key_values | |
| next_token = mx.argmax(out.logits[:, -1, :], axis=-1).astype(input_ids.dtype) # [B] | |
| mx.async_eval(next_token, *[kv for pair in past_kv for kv in pair]) | |
| tokens = [next_token] | |
| finished = (next_token == eos) | |
| for step in range(max_length - 1): | |
| if bool(mx.all(finished).item()): | |
| break | |
| out = self(input_ids=next_token[:, None], past_key_values=past_kv, use_cache=True) | |
| past_kv = out.past_key_values | |
| nt = mx.argmax(out.logits[:, -1, :], axis=-1).astype(input_ids.dtype) | |
| next_token = mx.where(finished, eos, nt) # 已结束行锁定为 EOS | |
| tokens.append(next_token) | |
| finished = finished | (next_token == eos) | |
| mx.async_eval(next_token, *[kv for pair in past_kv for kv in pair]) | |
| return mx.stack(tokens, axis=1) # [B, T] | |
| # ===================================================================== | |
| # Long-horizon (R-SWA) decoding | |
| # ===================================================================== | |
| def embed_multipage(self, prefix_ids, page_features, suffix_ids): | |
| """拼接多页 prefill embedding:[prefix_ids] + 各页视觉特征 + [suffix_ids]。 | |
| prefix_ids / suffix_ids: list[int](如 [bos] 与 encode("\n"+prompt)) | |
| page_features: list of [N_i, D] —— 每页 encode_images 的输出 | |
| 返回 inputs_embeds [1, L_m, D],图像特征已注入,可直接送入 generate_long。 | |
| """ | |
| emb = self.language_model.embed_tokens | |
| parts = [] | |
| if prefix_ids: | |
| parts.append(emb(mx.array([prefix_ids], dtype=mx.int32))[0]) | |
| parts.extend(page_features) # 每个 [N_i, D] | |
| if suffix_ids: | |
| parts.append(emb(mx.array([suffix_ids], dtype=mx.int32))[0]) | |
| return mx.concatenate(parts, axis=0)[None] # [1, L_m, D] | |
| def _evict_kv(past_kv, prefix_len, window): | |
| """R-SWA 缓存淘汰:保留全部 prefix + 最近 window 个 decode token 的 KV。 | |
| keys/values 中已包含绝对位置的 RoPE,因此裁剪任意 decode 槽都不影响正确性。 | |
| """ | |
| new = [] | |
| for k, v in past_kv: | |
| seq = k.shape[2] | |
| if seq - prefix_len > window: | |
| keep = seq - window # 起始裁剪点,保证保留最近 window 个 | |
| k = mx.concatenate([k[:, :, :prefix_len, :], k[:, :, keep:, :]], axis=2) | |
| v = mx.concatenate([v[:, :, :prefix_len, :], v[:, :, keep:, :]], axis=2) | |
| new.append((k, v)) | |
| return new | |
| def generate_long( | |
| self, | |
| inputs_embeds: mx.array, | |
| window: int = 128, | |
| max_length: int = 32768, | |
| temperature: float = 0.0, | |
| eos_token_id: int = 1, | |
| ) -> mx.array: | |
| """长程 R-SWA 贪心解码(batch=1)。 | |
| - Prefill:对全部 inputs_embeds 做完整因果注意力(视觉+prompt 即受保护前缀 P)。 | |
| - Decode:每步用绝对位置 position_ids,并在追加 KV 后淘汰窗口外的 decode KV, | |
| 使 KV 缓存恒定为 L_m + window,与论文 R-SWA 一致。 | |
| 返回 [1, T] 生成的 token(不含 prefill)。 | |
| """ | |
| _, prefix_len, _ = inputs_embeds.shape | |
| pos = mx.arange(0, prefix_len, dtype=mx.int32)[None] | |
| hs, past_kv = self.language_model( | |
| inputs_embeds=inputs_embeds, position_ids=pos, use_cache=True, | |
| ) | |
| logits = self.lm_head(hs[:, -1, :]) | |
| def pick(lg): | |
| if temperature > 0: | |
| probs = mx.softmax((lg / temperature).astype(mx.float32), axis=-1) | |
| return mx.random.categorical(probs, axis=-1).reshape(1, 1) | |
| return mx.argmax(lg, axis=-1, keepdims=True) | |
| generated = [] | |
| abs_pos = prefix_len | |
| for _ in range(max_length): | |
| nt = pick(logits) | |
| mx.eval(nt) | |
| if nt.item() == eos_token_id: | |
| break | |
| generated.append(nt) | |
| emb = self.language_model.embed_tokens(nt) # [1, 1, D] | |
| step_pos = mx.array([[abs_pos]], dtype=mx.int32) | |
| hs, past_kv = self.language_model( | |
| inputs_embeds=emb, position_ids=step_pos, | |
| past_key_values=past_kv, use_cache=True, | |
| ) | |
| past_kv = self._evict_kv(past_kv, prefix_len, window) | |
| logits = self.lm_head(hs[:, -1, :]) | |
| abs_pos += 1 | |
| if not generated: | |
| return mx.zeros((1, 0), dtype=mx.int32) | |
| return mx.concatenate(generated, axis=1) | |
Xet Storage Details
- Size:
- 49.1 kB
- Xet hash:
- 4d949ae174347fa39eb415fa66a43df047ad36a91e298833e0d44f28ed9e55dd
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.