File size: 23,940 Bytes
dfe210b 305fc2a dfe210b 80c945e 305fc2a dfe210b 305fc2a 80c945e dfe210b 305fc2a dfe210b 80c945e dfe210b 80c945e dfe210b 305fc2a dfe210b 80c945e dfe210b 305fc2a dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e 305fc2a 80c945e dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e dfe210b 80c945e 305fc2a 80c945e 305fc2a 80c945e dfe210b 80c945e 305fc2a 80c945e 305fc2a dfe210b 80c945e dfe210b 80c945e 305fc2a dfe210b 80c945e 305fc2a dfe210b 80c945e dfe210b 80c945e 305fc2a dfe210b 80c945e dfe210b 80c945e 305fc2a 80c945e dfe210b 305fc2a 80c945e 305fc2a 80c945e dfe210b 80c945e dfe210b | 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 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | # Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
"""Export EmbeddingGemma 2 (text + images + audio) to the three Core AI `.aimodel` assets of this package.
# 1. download the original checkpoint: hf download google/embeddinggemma-2 --local-dir ./embeddinggemma-2
# 2. python3 -W ignore export.py --model-dir ./embeddinggemma-2 --out <bundle folder> \
# --buckets 32,64,128,288,512,1024,2048 --qchunks 12 --audio-buckets 192,384,1024,1536,3000 --date 2026-10-12
# (the released bundle, ~3 min)
Written to <out> (see README "Why three assets"):
embeddinggemma-2-audio.aimodel functions audio_<Tb>: features [1,Tb,128] fp16 (log-mel frames), frame_mask [1,Tb],
half_mask [1,Tb/2], token_mask [1,Tb/4] fp16 -> soft_tokens [1,Tb/4,512] fp16
(eg2_audio.py; Tb in 192, 384, 1024, 1536, 3000 frames of 10 ms)
embeddinggemma-2-text-mm.aimodel functions mm_<S>: input_ids [1,S] int32, attention_mask [1,S] fp16,
mm_embeds [1,S,512] fp16, mm_mask [1,S] fp16 -> embedding [1,768] fp16.
The text encoder (eg2_text.py, unchanged from v1.0) with a soft-token mix at the
input (eg2_text_mm.py); with zero soft tokens it is exactly the text encoder.
embeddinggemma-2-vision.aimodel function vision_2520: patches [1,2520,768] fp16 (pixel values 0..255), pos_x/pos_y [1,2520] int32,
cos/sin [1,1,2520,64] fp16, patch_mask [1,2520] fp16 -> soft_tokens [1,280,512] fp16
(eg2_vision.py)
tokenizer/ unchanged Gemma tokenizer (text queries and the image sequence ids)
config.json, MODEL-ATTRIBUTION.json
`--part text` / `--part vision` / `--part audio` re-exports one asset and keeps the others' entries of an existing
config.json.
The result is functionally identical to the released bundle, not byte-identical: the converter names internal graph
resources at random (main.mlirb / main.hash differ) and the runtime writes metadata.json with the export time.
config.json, MODEL-ATTRIBUTION.json (with --date 2026-10-12) and the tokenizer files come out byte-identical.
Requirements: Python 3.12, torch 2.9.0, coreai-torch 0.4.3, coreai-opt 0.3.0, torchao 0.17.0, coreai-core 1.0.0b3,
safetensors (see ../python/requirements.lock).
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import subprocess
import sys
import time
from pathlib import Path
import torch
LAB = Path(__file__).resolve().parent
sys.path.insert(0, str(LAB))
import eg2_audio # noqa: E402
import eg2_text # noqa: E402
import eg2_text_mm # noqa: E402
import eg2_vision # noqa: E402
T0 = time.time()
PROMPTS = {
"document": "title: none | text: ",
"search_query": "task: search result | query: ",
"question_answering": "task: question answering | query: ",
"fact_checking": "task: fact checking | query: ",
"code_retrieval": "task: code retrieval | query: ",
"classification": "task: classification | query: ",
"clustering": "task: clustering | query: ",
"sentence_similarity": "task: sentence similarity | query: ",
}
SPECIAL = {"bos": 2, "eos": 1, "pad": 0, "boi": 255999, "image": 258880, "eoi": 258882, "video": 258884,
"boa": 256000, "audio": 258881, "eoa": 258883}
def log(msg: str) -> None:
print(f"[{time.time() - T0:7.1f}s] {msg}", flush=True)
def sha256_file(p: Path) -> str:
h = hashlib.sha256()
with open(p, "rb") as f:
for chunk in iter(lambda: f.read(1 << 24), b""):
h.update(chunk)
return h.hexdigest()
def dir_bytes(p: Path) -> int:
return sum(f.stat().st_size for f in p.rglob("*") if f.is_file()) if p.is_dir() else p.stat().st_size
def save_asset(prog, path: Path, description: str) -> None:
from coreai.runtime import AIModelAssetMetadata
shutil.rmtree(path, ignore_errors=True)
meta = AIModelAssetMetadata()
meta.author = "Gemma Team (Google); Core AI port"
meta.license = "Apache-2.0"
meta.model_description = description
meta.creation_date = int(time.time())
prog.save_asset(path, meta)
log(f"saved {path} ({dir_bytes(path) / 2**20:.1f} MiB)")
try:
print(subprocess.run(["xcrun", "coreai-build", "inspect", str(path), "--no-metadata"], capture_output=True, text=True, timeout=120).stdout[:2500])
except Exception as e: # noqa: BLE001
log(f"inspect failed: {e}")
def export_vision(args, out: Path) -> dict:
import coreai_torch
eg2_vision.PRECISION_MODE = "ane"
eg2_vision.QUERY_CHUNKS = args.qchunks
model = eg2_vision.load_model(args.model_dir, torch.float16)
if args.num_layers:
model.layers = model.layers[: args.num_layers]
log(f"vision model loaded: {sum(p.numel() for p in model.parameters()) / 1e6:.1f} M params, qchunks={args.qchunks}")
N = eg2_vision.MAX_PATCHES
ex = {"patches": torch.rand(1, N, 768, dtype=torch.float16), "pos_x": torch.zeros(1, N, dtype=torch.int32),
"pos_y": torch.zeros(1, N, dtype=torch.int32), "cos": torch.ones(1, 1, N, 64, dtype=torch.float16),
"sin": torch.zeros(1, 1, N, 64, dtype=torch.float16), "patch_mask": torch.ones(1, N, dtype=torch.float16)}
with torch.no_grad():
ep = torch.export.export(model, args=(), kwargs=ex)
log("torch.export (vision) done")
ep = ep.run_decompositions(coreai_torch.get_decomp_table())
conv = coreai_torch.TorchConverter(mode=coreai_torch.TorchConverter.Mode.DEBUG if args.debug_info else coreai_torch.TorchConverter.Mode.RELEASE)
fn = f"vision_{N}"
conv.add_exported_program(ep, input_names=["patches", "pos_x", "pos_y", "cos", "sin", "patch_mask"], output_names=["soft_tokens"], entrypoint_name=fn)
prog = conv.to_coreai()
log("converted vision to AIProgram")
asset = out / "embeddinggemma-2-vision.aimodel"
save_asset(prog, asset, f"EmbeddingGemma 2 vision encoder ({args.hf_id}): 2520 patches -> 280 soft tokens in the text space; "
f"fp16, integer pixel inputs (0..255), cell-major pooling, max-abs RMSNorm.")
return {"asset": asset.name, "function": fn, "max_patches": N, "max_soft_tokens": eg2_vision.MAX_SOFT, "qchunks": args.qchunks}
def export_audio(args, out: Path, buckets: list[int]) -> dict:
"""One dynamic-T torch.export (T = 4t) of eg2_audio, specialized to the frame buckets as functions audio_<Tb>."""
import coreai_torch
from torch.export import Dim
eg2_audio.PRECISION_MODE = "ane"
model = eg2_audio.load_model(args.model_dir, torch.float16)
if args.num_layers:
model.layers = model.layers[: args.num_layers]
log(f"audio model loaded: {sum(p.numel() for p in model.parameters()) / 1e6:.1f} M params; frame buckets {buckets}")
assert all(b % 4 == 0 for b in buckets) and len(buckets) >= 2
names = ["features", "frame_mask", "half_mask", "token_mask"]
b0 = buckets[1]
ex = {"features": torch.randn(1, b0, 128, dtype=torch.float16), "frame_mask": torch.ones(1, b0, dtype=torch.float16),
"half_mask": torch.ones(1, b0 // 2, dtype=torch.float16), "token_mask": torch.ones(1, b0 // 4, dtype=torch.float16)}
t = Dim("t", min=buckets[0] // 4, max=buckets[-1] // 4)
dyn = {"features": {1: 4 * t}, "frame_mask": {1: 4 * t}, "half_mask": {1: 2 * t}, "token_mask": {1: t}}
with torch.no_grad():
ep = torch.export.export(model, args=(), kwargs=ex, dynamic_shapes=dyn)
log("torch.export (audio) done")
ep = ep.run_decompositions(coreai_torch.get_decomp_table())
conv = coreai_torch.TorchConverter(mode=coreai_torch.TorchConverter.Mode.DEBUG if args.debug_info else coreai_torch.TorchConverter.Mode.RELEASE)
conv.add_exported_program(ep, input_names=names, output_names=["soft_tokens"], entrypoint_name="audio")
prog = conv.to_coreai()
log("converted audio to AIProgram")
cfg = {f'"{b}"': {"features": (1, b, 128), "frame_mask": (1, b), "half_mask": (1, b // 2), "token_mask": (1, b // 4)} for b in buckets}
prog.set_static_shape_config("audio", cfg)
functions = [f"audio_{b}" for b in buckets]
log(f"static shape specializations: {functions}")
asset = out / "embeddinggemma-2-audio.aimodel"
save_asset(prog, asset, f"EmbeddingGemma 2 audio encoder ({args.hf_id}): log-mel frames -> soft tokens in the text space; "
f"fp16, banded local attention, max-abs RMSNorm; frame buckets {buckets}.")
return {"asset": asset.name, "functions": functions, "frame_buckets": buckets, "max_frames": buckets[-1],
"max_soft_tokens": buckets[-1] // 4}
def export_text_mm(args, out: Path, buckets: list[int]) -> dict:
import coreai_torch
from torch.export import Dim
eg2_text.PRECISION_MODE = "ane"
max_seq = max(buckets)
model = eg2_text_mm.load_model(args.model_dir, torch.float16, max_seq=max_seq)
if args.num_layers:
model.layers = model.layers[: args.num_layers]
model.cfg.layer_types = model.cfg.layer_types[: args.num_layers]
log(f"text-mm model loaded: {sum(p.numel() for p in model.parameters()) / 1e6:.1f} M params")
S0 = buckets[min(1, len(buckets) - 1)]
ref = {"input_ids": torch.zeros(1, S0, dtype=torch.int32), "attention_mask": torch.ones(1, S0, dtype=torch.float16),
"mm_embeds": torch.zeros(1, S0, 512, dtype=torch.float16), "mm_mask": torch.zeros(1, S0, dtype=torch.float16)}
ref["input_ids"][0, :8] = torch.tensor([2, 8071, 236787, 3927, 1354, 1109, 7609, 1], dtype=torch.int32)
ref["attention_mask"][0, 8:] = 0
S = Dim("S", min=min(8, buckets[0]), max=max_seq)
dyn = {"input_ids": {1: S}, "attention_mask": {1: S}, "mm_embeds": {1: S}, "mm_mask": {1: S}}
with torch.no_grad():
ep = torch.export.export(model, args=(), kwargs=ref, dynamic_shapes=dyn)
log("torch.export (text-mm) done")
ep = ep.run_decompositions(coreai_torch.get_decomp_table())
conv = coreai_torch.TorchConverter(mode=coreai_torch.TorchConverter.Mode.DEBUG if args.debug_info else coreai_torch.TorchConverter.Mode.RELEASE)
conv.add_exported_program(ep, input_names=["input_ids", "attention_mask", "mm_embeds", "mm_mask"], output_names=["embedding"], entrypoint_name="mm")
prog = conv.to_coreai()
log("converted text-mm to AIProgram")
cfg = {f'"{b}"': {"input_ids": (1, b), "attention_mask": (1, b), "mm_embeds": (1, b, 512), "mm_mask": (1, b)} for b in buckets}
prog.set_static_shape_config("mm", cfg)
functions = [f"mm_{b}" for b in buckets]
log(f"static shape specializations: {functions}")
asset = out / "embeddinggemma-2-text-mm.aimodel"
save_asset(prog, asset, f"EmbeddingGemma 2 text encoder ({args.hf_id}) with soft-token (image) inputs, mean pooling, 768-d "
f"projection and L2 normalization in-graph; fp16 (Neural Engine); sequence buckets {buckets}.")
return {"asset": asset.name, "functions": functions, "buckets": buckets, "max_tokens": max_seq}
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--model-dir", required=True, help="folder with the original google/embeddinggemma-2 checkpoint")
ap.add_argument("--hf-id", default="google/embeddinggemma-2")
ap.add_argument("--out", required=True)
ap.add_argument("--part", default="all", choices=["all", "text", "vision", "audio"])
ap.add_argument("--buckets", default="32,64,128,288,512,1024,2048")
ap.add_argument("--qchunks", type=int, default=12,
help="split the vision attention's queries into N blocks (released: 12; 1 = one 2520x2520 matmul per layer)")
ap.add_argument("--audio-buckets", default="192,384,1024,1536,3000",
help="frame buckets of the audio encoder (multiples of 4; only the default set is validated on the Neural "
"Engine, see README)")
ap.add_argument("--num-layers", type=int, default=None, help="smoke export with the first N layers")
ap.add_argument("--debug-info", action="store_true")
ap.add_argument("--date", default=None, help="port date written to MODEL-ATTRIBUTION.json (default: today)")
args = ap.parse_args()
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
buckets = sorted(int(b) for b in args.buckets.split(","))
log(f"args {vars(args)}")
existing = json.loads((out / "config.json").read_text()) if (out / "config.json").exists() else {}
vision = ({k: existing["vision"][k] for k in ("asset", "function", "max_patches", "max_soft_tokens", "qchunks")}
if "vision" in existing else None)
text = {k: existing[k] for k in ("asset", "functions", "buckets", "max_tokens")} if "asset" in existing else None
audio = ({k: existing["audio"][k] for k in ("asset", "functions", "frame_buckets", "max_frames", "max_soft_tokens")}
if "audio" in existing else None)
audio_buckets = sorted(int(b) for b in args.audio_buckets.split(","))
if args.part in ("all", "text"):
text = export_text_mm(args, out, buckets)
if args.part in ("all", "vision"):
vision = export_vision(args, out)
if args.part in ("all", "audio"):
audio = export_audio(args, out, audio_buckets)
if text is None or vision is None or audio is None:
raise SystemExit("--part text|vision|audio needs an existing config.json in --out with the other assets' entries")
tok_dir = out / "tokenizer"
tok_dir.mkdir(exist_ok=True)
for f in ("tokenizer.json", "tokenizer_config.json"):
shutil.copy(Path(args.model_dir) / f, tok_dir / f)
cfg_json = {
"base_model": args.hf_id, "format": "coreai", **text, "precision_mode": "ane",
"inputs": {
"input_ids": "[1, S] int32 (bos=2 ... eos=1, right-padded with pad=0; image slots hold 258880)",
"attention_mask": "[1, S] float16 (1 = token, 0 = padding)",
"mm_embeds": "[1, S, 512] float16 soft tokens at their slots (zeros elsewhere), unscaled",
"mm_mask": "[1, S] float16 (1 at a soft-token slot)"},
"output": {"embedding": "[1, 768] float16, L2-normalized; Matryoshka: keep the first 512/256/128 and re-normalize"},
"text_only_use": "mm_embeds = 0 and mm_mask = 0 give the text embedding (bit-identical to the v1.0 main_<S> functions)",
"vision": {**vision, "inputs": {
"patches": "[1, 2520, 768] float16: 16x16x3 patches (py, px, c), integer pixel values 0..255 (not /255), CELL-MAJOR order, zero padded",
"pos_x": "[1, 2520] int32 patch column (0 for padding)", "pos_y": "[1, 2520] int32 patch row (0 for padding)",
"cos": "[1, 1, 2520, 64] float16 axial RoPE cos (channels 0-31 by x, 32-63 by y, 16 freqs theta 100 duplicated)",
"sin": "[1, 1, 2520, 64] float16", "patch_mask": "[1, 2520] float16 (1 = patch, 0 = padding)"},
"output": {"soft_tokens": "[1, 280, 512] float16; rows >= n_soft are padding"},
"patch_size": 16, "pooling_kernel": 3, "cell_px": 48, "rope_theta": 100.0, "head_dim": 64,
"resize": "aspect-preserving to multiples of 48 px within 2520 patches; antialiased bicubic on uint8 (torchvision-exact)",
"image_prompt": None, "exif_orientation": "not applied (upstream parity); optional in the runners"},
"image_sequence": "<bos> <|image> <|image|> x n_soft <image|> <eos> (n_soft + 4 tokens, at most 284 -> bucket 288)",
"audio": {**audio, "inputs": {
"features": "[1, Tb, 128] float16 log-mel frames (10 ms hop, 128 HTK mel bands, ln(mel + 1e-3)), the first n_frames rows real, zero after",
"frame_mask": "[1, Tb] float16 (1 for the first n_frames rows)",
"half_mask": "[1, Tb/2] float16 (1 for the first ceil(n_frames/2) rows)",
"token_mask": "[1, Tb/4] float16 (1 for the first ceil(ceil(n_frames/2)/2) rows = the soft-token count)"},
"output": {"soft_tokens": "[1, Tb/4, 512] float16; rows >= n_tokens are padding"},
"sample_rate": 16000, "max_seconds": 30.0, "max_samples": 480000, "frame_ms": 20, "hop_ms": 10, "fft": 512, "n_mels": 128,
"frames_per_token": 4, "front_end": "python/eg2_audio_frontend.py, swift AudioFrontend (upstream Gemma4AudioFeatureExtractor defaults)",
"resampler": "Kaiser-windowed sinc, pass band 0.913 / stop band 1.0 of the lower Nyquist, 125 dB (upstream: libsoxr HQ)",
"audio_prompt": None, "long_audio": "the first 30 s (upstream); the runners also offer the mean of 30 s windows"},
"audio_sequence": "<bos> <|audio> <|audio|> x n_tokens <audio|> <eos> (n_tokens + 4 tokens, at most 754 -> bucket 1024)",
"hidden_size": 512, "embedding_dim": 768, "matryoshka_dims": [768, 512, 256, 128],
"bos_token_id": SPECIAL["bos"], "eos_token_id": SPECIAL["eos"], "pad_token_id": SPECIAL["pad"],
"boi_token_id": SPECIAL["boi"], "image_token_id": SPECIAL["image"], "eoi_token_id": SPECIAL["eoi"],
"video_token_id": SPECIAL["video"], "boa_token_id": SPECIAL["boa"], "audio_token_id": SPECIAL["audio"],
"eoa_token_id": SPECIAL["eoa"], "prompts": PROMPTS,
"tokenizer": "tokenizer/tokenizer.json (unchanged Gemma tokenizer; the runner adds <bos> and <eos>)",
}
(out / "config.json").write_text(json.dumps(cfg_json, indent=2) + "\n")
import coreai_torch as ct
from importlib.metadata import version as pkg_version
tb = text["buckets"]
attribution = {
"name": "EmbeddingGemma-2-CoreAI",
"version": "1.2",
"base_model": args.hf_id,
"base_model_author": "Gemma Team, Google DeepMind",
"base_model_source": f"https://huggingface.co/{args.hf_id}",
"base_model_license": "Apache-2.0",
"base_model_terms": "Deployments must adhere to the Gemma Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy",
"base_model_file_sha256": {"model.safetensors": sha256_file(Path(args.model_dir) / "model.safetensors")},
"port": {
"copyright": "2026 Everpaper",
"license": "Apache-2.0 (see LICENSE); attribution and modification notice in NOTICE",
"date": args.date or time.strftime("%Y-%m-%d"),
"format": "Apple Core AI .aimodel (macOS 27), fp16, static shapes",
"exporter": {
"coreai_torch": ct.__version__, "coreai_core": pkg_version("coreai-core"), "torch": torch.__version__.split("+")[0],
"recipe": f"export/export.py --buckets {','.join(map(str, tb))} --qchunks {vision['qchunks']} "
f"--audio-buckets {','.join(map(str, audio['frame_buckets']))}",
"global_attention": eg2_text.GLOBAL_ATTN_MODE, "gqa": eg2_text.GQA_MODE,
},
},
"modifications": [
"All three encoders of the multimodal checkpoint (text, vision, audio) are included, as separate assets.",
"Architecture re-authored in plain PyTorch for Core AI export; weights are the original ones (bf16 rounded "
"to fp16), with the scale folds and table cuts listed below.",
"Audio: encoder (Gemma4AudioModel: subsampling convolutions, 12 conformer layers) and audio embedder re-authored "
"in plain PyTorch; the chunked local attention is written in its banded form (each token attends to itself and "
"the 11 previous tokens; the relative-position term is a per-layer constant), the q/k scales are folded into "
"q_proj/k_proj and their clip bounds, the gradient-clipping clamps are dropped, the padded-key mask is an "
"additive -10000, every RMSNorm uses the max-abs rescaling (fp16). Mathematically identical to the reference "
"(fp32 soft-token cosine >= 0.99999998 on 213 clips).",
f"Audio: fixed frame buckets {', '.join(map(str, audio['frame_buckets']))} (10 ms frames) as functions audio_<Tb>; "
"inputs longer than 30 s are truncated by the host (the upstream default). The three masks are host inputs.",
"Audio preprocessing (host): the upstream log-mel feature extractor re-implemented in numpy and Swift (bit-exact "
"in numpy); resampling to 16 kHz with a Kaiser-windowed sinc instead of upstream's libsoxr (see README).",
"Text: the sqrt(512) token-embedding scale and the 512^-0.5 per-layer-embedding scale are folded into the tables.",
"Text: the four global (full-attention, single-kv-head) layers compute attention as a plain 4-D matmul with k/v "
"broadcast over the heads; the sliding-window layers use a 5-D grouped matmul. Mathematically identical to "
"the reference (fp32 max abs diff 3e-7).",
"fp16 everywhere (Neural Engine): every RMSNorm and the final L2 normalization divide the row by its max-abs "
"value before squaring (text activations reach ~2000, the vision residual ~1400); in the model code every "
"division is expressed as rsqrt(x)*rsqrt(x) (the Neural Engine compiler rejects divide).",
"Text: bidirectional attention masks (padding + sliding window 512) are built inside the graph from "
"attention_mask; the additive mask value is -10000 (finite).",
"Text: masked mean pooling, the 512->768 embedding projection and the L2 normalization run inside the graph; "
"the output is the final sentence embedding.",
"Text: an input mix x = embed(ids)*(1-mm_mask) + mm_embeds*mm_mask at the start of the graph, so that image "
"soft tokens replace the <|image|> placeholder embeddings (soft tokens are not scaled); with zero soft tokens "
"the graph is the v1.0 text encoder.",
f"Text: fixed sequence buckets {', '.join(map(str, tb))} as functions mm_<S>; input longer than "
f"{text['max_tokens']} tokens must be truncated or chunked by the caller (the original model accepts 8192).",
"Vision: encoder (Gemma4VisionModel, 16 layers) and vision embedder, one fixed shape: the default budget of 280 "
"soft tokens (2520 patches of 16x16). Patches are fed in cell-major order so that the 3x3 average pooling is a "
"reshape + mean instead of a one-hot matmul (attention is permutation-equivariant; identical result).",
"Vision: the patches are integer pixel values 0..255 (exact in fp16) instead of pixel/255: the graph computes "
"P - 127.5 and the 2/255 scale is folded into the patch-embedding projection's weights, which equals upstream's "
"projection of 2*(P/255) - 1 without rounding the inputs to fp16 (that rounding cost heavily upscaled tiny "
"images up to 0.004 of cosine).",
"Vision: the pooler's multiplication by sqrt(768) (fp32 upstream, overflows fp16) is dropped and the vision "
"embedder's RMSNorm uses eps/768 instead: RMSNorm(x*s, eps) == RMSNorm(x, eps/s^2).",
"Vision: the 2-D position-embedding tables are cut to the 840 rows reachable at this budget; the axial RoPE "
"cos/sin tables and the key mask are inputs computed by the host; the additive key-mask value is -10000.",
"Image preprocessing (host): the upstream aspect-preserving size rule and the torchvision-exact antialiased "
"bicubic resize on uint8 re-implemented in numpy and Swift.",
],
}
if args.num_layers:
attribution["port"]["exporter"]["num_layers"] = args.num_layers
(out / "MODEL-ATTRIBUTION.json").write_text(json.dumps(attribution, indent=2) + "\n")
log(f"bundle written: {out} ({dir_bytes(out) / 2**20:.1f} MiB)")
log("DONE")
if __name__ == "__main__":
main()
|