File size: 12,250 Bytes
80c945e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
#
# Re-authoring of the Gemma 4 vision encoder (Gemma4VisionModel + the EmbeddingGemma 2 vision embedder) as
# described by modeling_gemma4.py / modeling_embedding_gemma2.py in Hugging Face transformers 5.19
# (Copyright 2026 The HuggingFace Team, Apache-2.0). Model weights: Google DeepMind (Gemma Team), Apache-2.0,
# https://huggingface.co/google/embeddinggemma-2 — see ../NOTICE for the attribution and modification list.
"""EmbeddingGemma 2 vision encoder re-authored for Core AI export: patches -> soft tokens in the text space.

One fixed shape (the default budget of 280 soft tokens = 2520 patches of 16x16 px):
  patches    [1, 2520, 768] fp16, integer pixel values 0..255 (exact in fp16), 16x16x3 flattened (py, px, c),
             CELL-MAJOR order (the 9 patches of each 3x3 pooling cell contiguous, cells row-major), zero padded
  pos_x/pos_y [1, 2520] int32 patch coordinates (padding 0)
  cos/sin    [1, 1, 2520, 64] fp16 axial RoPE tables computed on the host (channels 0..31 by x, 32..63 by y)
  patch_mask [1, 2520] fp16, 1 = real patch, 0 = padding
  -> (P - 127.5) -> Linear 768->768 (weights x 2/255) + posx[x] + posy[y] -> 16 encoder layers (12 heads x 64, sandwich RMSNorm, q/k/v
     norms, scale 1.0, bidirectional with a key mask) -> 3x3 average pooling = reshape [280, 9, 768].mean
     -> RMSNorm without scale -> Linear 768->512
  soft_tokens [1, 280, 512] fp16; rows at index >= n_soft (= cells of the image) are padding and must be ignored.

Differences from the upstream graph (all mathematically identical in exact arithmetic):
  - upstream feeds p = P/255 and computes 2p - 1. P/255 is not representable in fp16 (rounding it alone costs a
    heavily upscaled tiny image cosine 0.996), so the graph takes the integer pixel P and computes P - 127.5
    (exact in fp16) and the 2/255 scale is folded into input_proj's weights: W(2P/255 - 1) == (2W/255)(P - 127.5).
  - cell-major patch order makes the 3x3 pooling a reshape + mean instead of a one-hot matmul; attention is
    permutation-equivariant so the layers do not care.
  - the upstream pooler multiplies by sqrt(768) in fp32 before the projector's RMSNorm (and warns that this
    exceeds fp16). RMSNorm(x*s, eps) == RMSNorm(x, eps/s^2), so the scale is dropped and eps/768 used instead.
  - the position-embedding tables are cut to the 840 rows an image of this budget can address (x and y < 840).
  - fp16 "ane" precision mode as in eg2_text.py: every RMSNorm divides the row by its max-abs value before
    squaring (the vision residual reaches ~1400), no divides, finite -1e4 key mask.
"""
from __future__ import annotations

import json
import math
from pathlib import Path

import torch
import torch.nn as nn
import torch.nn.functional as F

PRECISION_MODE = "ane"  # "ane" (all fp16, Neural Engine) | "fp32" (correctness gate of the re-authoring)
MASK_VALUE = -10000.0
NORM_FLOOR = 1e-3
MAX_SOFT = 280
POOL = 3
MAX_PATCHES = MAX_SOFT * POOL * POOL  # 2520
POS_ROWS = MAX_SOFT * POOL  # 840 (budget 280); budget 1120 would need 3360
# Query chunking of the 2520x2520 attention (QUERY_CHUNKS > 1 splits the score matrix into row blocks so that
# no single intermediate exceeds 2520*2520/QUERY_CHUNKS*12 fp16 values); 1 = one matmul per layer.
QUERY_CHUNKS = int(__import__("os").environ.get("EG2_VISION_QCHUNKS", "1"))


class Cfg:
    def __init__(self, model_dir: str):
        c = json.loads((Path(model_dir) / "config.json").read_text())["vision_config"]
        t = json.loads((Path(model_dir) / "config.json").read_text())["text_config"]
        self.hidden = c["hidden_size"]  # 768
        self.inter = c["intermediate_size"]  # 3072
        self.n_layers = c["num_hidden_layers"]  # 16
        self.n_heads = c["num_attention_heads"]  # 12
        self.head_dim = c["head_dim"]  # 64
        self.patch = c["patch_size"]  # 16
        self.pool = c["pooling_kernel_size"]  # 3
        self.eps = c["rms_norm_eps"]  # 1e-6
        self.rope_theta = c["rope_parameters"]["rope_theta"]  # 100
        self.pos_size = c["position_embedding_size"]  # 10240
        self.text_hidden = t["hidden_size"]  # 512
        assert c["hidden_activation"] == "gelu_pytorch_tanh"
        assert c["num_key_value_heads"] == self.n_heads and not c["use_clipped_linears"] and not c["standardize"]
        assert c["rope_parameters"]["rope_type"] == "axial"


def _f16(x: float) -> torch.Tensor:
    return torch.tensor(x, dtype=torch.float16)


def _recip(m: torch.Tensor) -> torch.Tensor:
    r = torch.rsqrt(m)
    return r * r


class RMSNorm(nn.Module):
    """Gemma RMSNorm (weight used as-is), with the max-abs rescaling in "ane" mode."""

    def __init__(self, dim: int, eps: float, with_scale: bool = True):
        super().__init__()
        self.with_scale = with_scale
        if with_scale:
            self.weight = nn.Parameter(torch.ones(dim), requires_grad=False)
        self.eps = eps
        self.register_buffer("sqrt_eps16", _f16(math.sqrt(eps)), persistent=False)
        self.register_buffer("floor16", _f16(NORM_FLOOR), persistent=False)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if PRECISION_MODE == "ane":
            inv_m = _recip(torch.maximum(x.abs().amax(-1, keepdim=True), self.floor16))
            u = x * inv_m
            r = self.sqrt_eps16 * inv_m
            y = u * torch.rsqrt((u * u).mean(-1, keepdim=True) + r * r)
            return y * self.weight if self.with_scale else y
        xf = x.float()
        y = xf * torch.pow(xf.pow(2).mean(-1, keepdim=True) + self.eps, -0.5)
        if self.with_scale:
            y = y * self.weight.float()
        return y.to(x.dtype)


def rope_axial(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
    """x [1, H, N, 64]: rotate_half inside each 32-channel half (x-half, y-half). cos/sin [1, 1, N, 64]."""
    B, H, N, D = x.shape
    xr = x.reshape(B, H, N, 2, D // 2)
    h = D // 4
    rot = torch.cat((-xr[..., h:], xr[..., :h]), dim=-1).reshape(B, H, N, D)
    return x * cos + rot * sin


class Attention(nn.Module):
    def __init__(self, cfg: Cfg):
        super().__init__()
        self.n_heads, self.hd = cfg.n_heads, cfg.head_dim
        self.q_proj = nn.Linear(cfg.hidden, cfg.n_heads * cfg.head_dim, bias=False)
        self.k_proj = nn.Linear(cfg.hidden, cfg.n_heads * cfg.head_dim, bias=False)
        self.v_proj = nn.Linear(cfg.hidden, cfg.n_heads * cfg.head_dim, bias=False)
        self.o_proj = nn.Linear(cfg.n_heads * cfg.head_dim, cfg.hidden, bias=False)
        self.q_norm = RMSNorm(cfg.head_dim, cfg.eps)
        self.k_norm = RMSNorm(cfg.head_dim, cfg.eps)
        self.v_norm = RMSNorm(cfg.head_dim, cfg.eps, with_scale=False)

    def forward(self, x, cos, sin, bias):
        B, N, _ = x.shape
        q = self.q_norm(self.q_proj(x).view(B, N, self.n_heads, self.hd)).transpose(1, 2)
        k = self.k_norm(self.k_proj(x).view(B, N, self.n_heads, self.hd)).transpose(1, 2)
        v = self.v_norm(self.v_proj(x).view(B, N, self.n_heads, self.hd)).transpose(1, 2)
        q, k = rope_axial(q, cos, sin), rope_axial(k, cos, sin)
        kt = k.transpose(-1, -2)
        if QUERY_CHUNKS > 1:
            outs = []
            for qc in q.chunk(QUERY_CHUNKS, dim=2):
                s = torch.matmul(qc, kt) + bias  # scale 1.0
                p = torch.softmax(s, dim=-1) if PRECISION_MODE == "ane" else torch.softmax(s.float(), dim=-1).to(q.dtype)
                outs.append(torch.matmul(p, v))
            o = torch.cat(outs, dim=2)
        else:
            s = torch.matmul(q, kt) + bias  # [1, 12, N, N], scale 1.0
            p = torch.softmax(s, dim=-1) if PRECISION_MODE == "ane" else torch.softmax(s.float(), dim=-1).to(q.dtype)
            o = torch.matmul(p, v)
        return self.o_proj(o.transpose(1, 2).reshape(B, N, self.n_heads * self.hd))


class MLP(nn.Module):
    def __init__(self, cfg: Cfg):
        super().__init__()
        self.gate_proj = nn.Linear(cfg.hidden, cfg.inter, bias=False)
        self.up_proj = nn.Linear(cfg.hidden, cfg.inter, bias=False)
        self.down_proj = nn.Linear(cfg.inter, cfg.hidden, bias=False)

    def forward(self, x):
        return self.down_proj(F.gelu(self.gate_proj(x), approximate="tanh") * self.up_proj(x))


class Layer(nn.Module):
    def __init__(self, cfg: Cfg):
        super().__init__()
        self.self_attn = Attention(cfg)
        self.mlp = MLP(cfg)
        self.input_layernorm = RMSNorm(cfg.hidden, cfg.eps)
        self.post_attention_layernorm = RMSNorm(cfg.hidden, cfg.eps)
        self.pre_feedforward_layernorm = RMSNorm(cfg.hidden, cfg.eps)
        self.post_feedforward_layernorm = RMSNorm(cfg.hidden, cfg.eps)

    def forward(self, x, cos, sin, bias):
        x = x + self.post_attention_layernorm(self.self_attn(self.input_layernorm(x), cos, sin, bias))
        return x + self.post_feedforward_layernorm(self.mlp(self.pre_feedforward_layernorm(x)))


class EmbeddingGemma2Vision(nn.Module):
    def __init__(self, cfg: Cfg):
        super().__init__()
        self.cfg = cfg
        d = cfg.patch * cfg.patch * 3
        self.input_proj = nn.Linear(d, cfg.hidden, bias=False)
        self.pos_x = nn.Embedding(POS_ROWS, cfg.hidden)
        self.pos_y = nn.Embedding(POS_ROWS, cfg.hidden)
        self.layers = nn.ModuleList([Layer(cfg) for _ in range(cfg.n_layers)])
        # projector: RMSNorm without scale with eps/768 (replaces x*sqrt(768) followed by RMSNorm(eps)), Linear
        self.pre_projection_norm = RMSNorm(cfg.hidden, cfg.eps / cfg.hidden, with_scale=False)
        self.embedding_projection = nn.Linear(cfg.hidden, cfg.text_hidden, bias=False)
        self.register_buffer("mask_value", _f16(MASK_VALUE), persistent=False)
        self.register_buffer("one", _f16(1.0), persistent=False)
        self.register_buffer("c_mid", _f16(127.5), persistent=False)
        self.register_buffer("inv_pool", _f16(1.0 / (POOL * POOL)), persistent=False)

    def forward(self, patches, pos_x, pos_y, cos, sin, patch_mask):
        B, N, _ = patches.shape
        # upstream: input_proj(2 * P/255 - 1); here P - 127.5 (exact in fp16) with 2/255 folded into input_proj
        x = self.input_proj(patches - self.c_mid) + self.pos_x(pos_x) + self.pos_y(pos_y)
        bias = ((self.one - patch_mask) * self.mask_value)[:, None, None, :]  # [1, 1, 1, N] key mask
        for layer in self.layers:
            x = layer(x, cos, sin, bias)
        pooled = x.view(B, N // (POOL * POOL), POOL * POOL, self.cfg.hidden)
        if PRECISION_MODE == "ane":
            pooled = pooled.sum(2) * self.inv_pool  # 9 values of <= ~1400 each: sum < 13k, fp16-safe
        else:
            pooled = pooled.float().mean(2).to(x.dtype)
        return self.embedding_projection(self.pre_projection_norm(pooled))  # [1, 280, 512]


def load_model(model_dir: str, dtype: torch.dtype) -> EmbeddingGemma2Vision:
    from safetensors import safe_open

    cfg = Cfg(model_dir)
    m = EmbeddingGemma2Vision(cfg)
    sd = {}
    with safe_open(str(Path(model_dir) / "model.safetensors"), "pt") as f:
        for k in f.keys():
            if k.startswith("vision_tower.encoder.layers."):
                nk = k[len("vision_tower.encoder."):].replace(".linear.weight", ".weight")
                sd[nk] = f.get_tensor(k).float()
            elif k == "vision_tower.patch_embedder.input_proj.weight":
                sd["input_proj.weight"] = f.get_tensor(k).float() * (2.0 / 255.0)  # takes P - 127.5, P in 0..255
            elif k == "vision_tower.patch_embedder.position_embedding_table":
                t = f.get_tensor(k).float()
                sd["pos_x.weight"] = t[0, :POS_ROWS].clone()
                sd["pos_y.weight"] = t[1, :POS_ROWS].clone()
            elif k == "embed_vision.embedding_projection.weight":
                sd["embedding_projection.weight"] = f.get_tensor(k).float()
    sd = {k: v.to(dtype) for k, v in sd.items()}
    missing, unexpected = m.load_state_dict(sd, strict=False)
    missing = [k for k in missing if not any(s in k for s in ("sqrt_eps16", "floor16", "mask_value", "one", "c_mid", "inv_pool"))]
    assert not missing and not unexpected, (missing, unexpected)
    return m.to(dtype).eval()