Download reference/d1a_coreml.py from JohnP1/d1a-e4b-coreml: direct link, hf CLI and curl.
- Browser
- Download file 9.33 kB
-
https://huggingface.co/JohnP1/d1a-e4b-coreml/resolve/main/reference/d1a_coreml.py
- Command line
-
hf download hf://JohnP1/d1a-e4b-coreml/reference/d1a_coreml.py
-
curl -L -o d1a_coreml.py https://huggingface.co/JohnP1/d1a-e4b-coreml/resolve/main/reference/d1a_coreml.py
9.33 kB
| """D1A (Gemma 4 text) -> Core ML pilot (jonpol01/d1a#224): rebuild a D1A MLX export as a torch module, split into | |
| - host side: the two embedding tables (token embedding, per-layer embedding: 2.75B of E2B's 4.6B parameters) read | |
| row by row from the export's safetensors (dequantized per request, never held whole), and | |
| - Core: everything that computes (per-layer projection, 35 decoder layers, final norm, pointer head), which is what | |
| gets converted to Core ML. | |
| Inputs follow D1A's packed encoding (d1a.core.encoding.encode): one record, block-causal mask, positions restarting per | |
| question after the state, readout at each question's <decide> and </opt> tokens.""" | |
| import json | |
| import math | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from safetensors import safe_open | |
| from torch import nn | |
| from transformers import AutoConfig | |
| from transformers.models.gemma4 import modeling_gemma4 as G | |
| PREFIX = "language_model.model." | |
| def dequant(packed, scales, biases, bits, group=64): | |
| """MLX affine dequantization: packed uint32 [out, in*bits/32] (lowest bits first) -> float32 [out, in].""" | |
| p = packed.view(torch.int32).to(torch.int64) & 0xFFFFFFFF | |
| per = 32 // bits | |
| shifts = torch.arange(per, dtype=torch.int64) * bits | |
| q = ((p[..., None] >> shifts) & ((1 << bits) - 1)).reshape(p.shape[0], -1).to(torch.float32) | |
| s, b = scales.to(torch.float32), biases.to(torch.float32) | |
| return (q.view(q.shape[0], -1, group) * s[..., None] + b[..., None]).reshape(q.shape[0], -1) | |
| class _Shards: | |
| """model.safetensors or model-0000N-of-0000M.safetensors (via model.safetensors.index.json), read like one safe_open.""" | |
| def __init__(self, path): | |
| idx = Path(path) / "model.safetensors.index.json" | |
| if idx.exists(): | |
| wm = json.loads(idx.read_text())["weight_map"] | |
| files = {f: safe_open(str(Path(path) / f), "pt") for f in sorted(set(wm.values()))} | |
| self.where = {k: files[f] for k, f in wm.items()} | |
| else: # a released Core ML bundle ships only the embedding tables, as embeddings.safetensors | |
| name = "model.safetensors" if (Path(path) / "model.safetensors").exists() else "embeddings.safetensors" | |
| f = safe_open(str(Path(path) / name), "pt") | |
| self.where = {k: f for k in f.keys()} | |
| def keys(self): | |
| return self.where.keys() | |
| def get_tensor(self, k): | |
| return self.where[k].get_tensor(k) | |
| def get_slice(self, k): | |
| return self.where[k].get_slice(k) | |
| class Export: | |
| """A D1A MLX export directory (config.json, d1a_config.json, model.safetensors, head.safetensors).""" | |
| def __init__(self, path): | |
| self.path = Path(path) | |
| self.cfg = AutoConfig.from_pretrained(self.path) | |
| self.tc = self.cfg.text_config | |
| self.tc._attn_implementation = "eager" | |
| self.d1a = json.loads((self.path / "d1a_config.json").read_text()) | |
| q = self.d1a["quantization"] | |
| self.bits = {"default": (q["bits"], q["group_size"]), "embed_tokens_per_layer": (q[PREFIX.replace("model.", "") + "model.embed_tokens_per_layer"]["bits"] if False else q["language_model.model.embed_tokens_per_layer"]["bits"], q["language_model.model.embed_tokens_per_layer"]["group_size"])} | |
| self.f = _Shards(self.path) | |
| def bits_of(self, name): | |
| return self.bits["embed_tokens_per_layer"] if name.endswith("embed_tokens_per_layer") else self.bits["default"] | |
| def tensor(self, name): | |
| """A float32 tensor for `name` (without PREFIX), dequantized if it is stored quantized.""" | |
| full = PREFIX + name | |
| keys = set(self.f.keys()) | |
| if full + ".weight" in keys and full + ".scales" in keys: | |
| bits, group = self.bits_of(name) | |
| return dequant(self.f.get_tensor(full + ".weight"), self.f.get_tensor(full + ".scales"), self.f.get_tensor(full + ".biases"), bits, group) | |
| return self.f.get_tensor(full).to(torch.float32) | |
| def text_model(self, dtype=torch.float32): | |
| """The Gemma 4 text trunk with every non-embedding weight loaded (embedding tables stay empty: host side).""" | |
| with torch.device("meta"): | |
| m = G.Gemma4TextModel(self.tc) | |
| m = m.to_empty(device="cpu") | |
| state = {} | |
| keys = set(self.f.keys()) | |
| for name, p in m.named_parameters(): | |
| if name.startswith("embed_tokens"): | |
| continue | |
| base = name[: -len(".weight")] if name.endswith(".weight") else name | |
| if PREFIX + base + ".scales" in keys: | |
| state[name] = self.tensor(base) | |
| else: | |
| state[name] = self.f.get_tensor(PREFIX + name).to(torch.float32) | |
| assert state[name].shape == p.shape, (name, state[name].shape, p.shape) | |
| for name, b in m.named_buffers(): | |
| if PREFIX + name in keys: | |
| state[name] = self.f.get_tensor(PREFIX + name).to(torch.float32) | |
| missing, unexpected = m.load_state_dict(state, strict=False) | |
| missing = [k for k in missing if not k.startswith("embed_tokens")] | |
| assert not missing and not unexpected, (missing, unexpected) | |
| # buffers transformers computes itself (rotary inv_freq, embed_scale): re-run their init on CPU | |
| m.rotary_emb = G.Gemma4TextRotaryEmbedding(self.tc) | |
| return m.to(dtype).eval() | |
| def head(self): | |
| h = safe_open(str(self.path / "head.safetensors"), "pt") | |
| dp, d = h.get_tensor("q.weight").shape # pointer width, hidden size (E2B 1536, E4B 2560) | |
| q, k = nn.Linear(d, dp), nn.Linear(d, dp) | |
| q.weight.data, q.bias.data = h.get_tensor("q.weight").float(), h.get_tensor("q.bias").float() | |
| k.weight.data, k.bias.data = h.get_tensor("k.weight").float(), h.get_tensor("k.bias").float() | |
| return q, k | |
| class HostEmbed: | |
| """Row lookups of the two embedding tables, dequantized per request: emb = E[ids] * sqrt(hidden) and | |
| ple = E_pl[ids] * sqrt(ple_dim), reshaped [L, layers, ple_dim] (transformers' get_per_layer_inputs).""" | |
| def __init__(self, ex): | |
| self.ex, self.tc = ex, ex.tc | |
| self.f = ex.f | |
| def _rows(self, name, ids): | |
| full = PREFIX + name | |
| bits, group = self.ex.bits_of(name) | |
| w, s, b = (self.f.get_slice(full + suf) for suf in (".weight", ".scales", ".biases")) | |
| uniq, inv = np.unique(np.asarray(ids), return_inverse=True) | |
| rows = [dequant(w[int(i): int(i) + 1], s[int(i): int(i) + 1], b[int(i): int(i) + 1], bits, group) for i in uniq] | |
| return torch.cat(rows)[torch.as_tensor(inv)] | |
| def __call__(self, ids): | |
| emb = self._rows("embed_tokens", ids) * (self.tc.hidden_size ** 0.5) | |
| ple = self._rows("embed_tokens_per_layer", ids) * (self.tc.hidden_size_per_layer_input ** 0.5) | |
| return emb[None], ple.view(1, len(ids), self.tc.num_hidden_layers, self.tc.hidden_size_per_layer_input) | |
| def masks_for(enc, window, L=None, dtype=torch.float32): | |
| """D1A's packed masks (d1a.backends.torch.branch_masks) for one record, padded to L: additive [1, 1, L, L].""" | |
| seg, pos = enc["seg"], enc["pos"] | |
| n = len(seg) | |
| L = L or n | |
| s = torch.full((L,), -1) | |
| s[:n] = torch.tensor(seg) | |
| p = torch.zeros(L, dtype=torch.long) | |
| p[:n] = torch.tensor(pos) | |
| causal = torch.tril(torch.ones(L, L, dtype=torch.bool)) | |
| same = (s[None, :] == s[:, None]) | (s[None, :] == 0) | |
| allow = (causal & same & (s != -1)[None, :]) | torch.eye(L, dtype=torch.bool) | |
| neg = torch.finfo(dtype).min | |
| full = torch.zeros(L, L, dtype=dtype).masked_fill(~allow, neg) | |
| far = (p[:, None] - p[None, :] >= window) & ~torch.eye(L, dtype=torch.bool) | |
| slide = full.masked_fill(far, neg) | |
| return full[None, None], slide[None, None], p[None] | |
| class Core(nn.Module): | |
| """Everything that computes: per-layer projection, decoder layers, final norm, pointer head. | |
| forward(emb [1,L,d], ple_tok [1,L,layers,256], pos [1,L] int, mask_full, mask_slide [1,1,L,L], | |
| decide [Q] int, opts [Q,K] int, opt_valid [Q,K] float (1 used, 0 padding)) -> logits [Q,K] | |
| (raw, temperature not applied; padded options get a large negative).""" | |
| def __init__(self, text, head_q, head_k, silu_tanh=True): | |
| super().__init__() | |
| self.text, self.hq, self.hk = text, head_q, head_k | |
| self.types = text.config.layer_types | |
| self.scale = 1 / math.sqrt(head_q.out_features) | |
| def forward(self, emb, ple_tok, pos, mask_full, mask_slide, decide, opts, opt_valid): | |
| t = self.text | |
| ple = t.project_per_layer_inputs(emb, ple_tok) | |
| rope = {lt: t.rotary_emb(emb, pos, lt) for lt in t.unique_layer_types} | |
| masks = {"full_attention": mask_full, "sliding_attention": mask_slide} | |
| from collections import UserDict | |
| kv = UserDict() | |
| h = emb | |
| for i, layer in enumerate(t.layers): | |
| h = layer(h, ple[:, :, i, :], shared_kv_states=kv, position_embeddings=rope[self.types[i]], | |
| attention_mask=masks[self.types[i]], position_ids=pos) | |
| h = t.norm(h)[0] # [L, d] | |
| q = self.hq(h[decide]) # [Q, dp] | |
| k = self.hk(h[opts.reshape(-1)]).view(*opts.shape, -1) # [Q, K, dp] | |
| z = (k * q[:, None, :]).sum(-1) * self.scale | |
| return z + (opt_valid - 1.0) * 1e4 | |