ncm-modular / src /ncm_modular /gen_mlx.py
Red-fuchs's picture
ncm-modular: initial release (code + results, no weights)
b5731a6 verified
Raw History Blame Contribute Delete
6.29 kB
"""Prefix-conditioned NCM generator (MLX) over the frozen wmt14 codec. Matches upstream wmt14 layout:
prefix attends only to itself (pad-masked); level k>=1 positions see prefix + own level (block_diagonal);
RoPE restarts per level; learned level emb; codebook-tied heads; level 0 predicted from last non-pad prefix position."""
import os, sys, math
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from paths import WORK
import numpy as np, mlx.core as mx
from mlx import nn
import codec_mlx
from ncm_mlx.official.model import Block, rope_tables
class FrozenCodec:
"""thin wrapper: stop-gradient teacher inputs / sampling inputs from codes."""
def __init__(self, d=WORK+"/mlx/wmt14"):
self.c = codec_mlx.load(d)
self.levels, self.K, self.L = self.c.levels, self.c.K, self.c.L
self.cb = [self.c.W[f"cb.{k}"] for k in range(self.K)]
self.tok = self.c.W["wte"] # (vocab, 384) frozen token embedding
self.dim = self.tok.shape[1]
def encode(self, ids): return self.c.encode(ids)
def teacher_inputs(self, codes):
"""codes: list K of (B,l_k) -> inputs for levels 1..K-1: area_k @ cum recon through level k-1."""
z, outs = None, []
for k in range(self.K - 1):
q = self.c.contrib(codes[k], k); z = q if z is None else z + q
outs.append(self.c.area[k + 1] @ z)
return outs
def next_input(self, z, k): return self.c.area[k] @ z
def contrib(self, c, k): return self.c.contrib(c, k)
def decode(self, z): return mx.argmax(self.c.decode_logits(z), -1)
class Gen(nn.Module):
def __init__(self, fc: FrozenCodec, P=48, D=512, H=8, nl=8, pad_id=151643, rope_base=10000.0, drop=0.1):
super().__init__()
self.P, self.D, self.pad_id, self.levels, self.K = P, D, pad_id, fc.levels, fc.K
cd = fc.dim
self.in_proj = nn.Linear(cd, D)
self.level_emb = nn.Embedding(self.K - 1, D)
self.blocks = [Block(D, H) for _ in range(nl)]
self.ln_f = nn.LayerNorm(D, eps=1e-6)
self.drop = nn.Dropout(drop)
self.proj = [nn.Linear(D, cd, bias=False) for _ in range(self.K)] # tied-codebook heads
self.log_inv_t = [mx.array(0.4) for _ in range(self.K)] # effective inverse temperature = exp(8*u), init ~24
self.cdt = mx.float32
hd = D // H
self._cb = fc.cb; self._tok = fc.tok
self._esq = [(E * E).sum(-1) for E in fc.cb]; self._scale = [mx.maximum(e.mean(), 1e-12) for e in self._esq]
pos = np.concatenate([np.arange(P)] + [np.arange(l) for l in self.levels[1:]])
self._rope = rope_tables(pos, hd, rope_base)
self._lvl = mx.array(np.concatenate([np.full(l, k) for k, l in enumerate(self.levels[1:])]))
grp = np.concatenate([np.full(P, -1)] + [np.full(l, k) for k, l in enumerate(self.levels[1:])])
S = len(grp); kv_p = grp[None, :] == -1
base = np.where(grp[:, None] == -1, kv_p, kv_p | (grp[:, None] == grp[None, :]))
self._base = mx.array(base)[None, None]
# single-level (inference) tables
self._rope_lv = [rope_tables(np.concatenate([np.arange(P), np.arange(l)]), hd, rope_base) for l in self.levels[1:]]
self._rope_p = rope_tables(np.arange(P), hd, rope_base)
def set_compute(self, dt):
self.set_dtype(dt); self.cdt = dt; return self
def head(self, k, h):
E = self._cb[k]; z = self.proj[k](h).astype(mx.float32)
return (2.0 * (z @ E.T) - self._esq[k]) / self._scale[k] * mx.exp(8.0 * self.log_inv_t[k].astype(mx.float32))
def prefix_emb(self, ptoks): return self.in_proj(self._tok[ptoks].astype(self.cdt))
def rp(self, r): return (r[0].astype(self.cdt), r[1].astype(self.cdt))
def keep(self, ptoks): return ptoks != self.pad_id
def _stack(self, h, mask, rope):
for b in self.blocks: h = b(h, mask, rope)
return self.ln_f(h)
def l0_logits_from(self, hp, ptoks):
last = mx.maximum(self.keep(ptoks).sum(1) - 1, 0)
h0 = mx.take_along_axis(hp, last[:, None, None], axis=1) # (B,1,D)
return self.head(0, h0)
def __call__(self, inputs, ptoks):
"""teacher-forced: inputs = list of K-1 arrays (B,l_k,cd) for levels 1..K-1 -> list of K logits."""
x = mx.concatenate(inputs, axis=1).astype(self.cdt)
h = self.drop(self.in_proj(x) + self.level_emb(self._lvl))
h = mx.concatenate([self.drop(self.prefix_emb(ptoks)), h], axis=1)
keep = mx.concatenate([self.keep(ptoks), mx.ones((x.shape[0], x.shape[1]), dtype=mx.bool_)], 1)
h = self._stack(h, self._base & keep[:, None, None, :], self.rp(self._rope))
out = [self.l0_logits_from(h[:, :self.P], ptoks)]
o = self.P
for k in range(1, self.K):
l = self.levels[k]; out.append(self.head(k, h[:, o:o + l])); o += l
return out
# ---- inference
def encode_prefix(self, ptoks):
pe = self.prefix_emb(ptoks)
keep = self.keep(ptoks)
hp = self._stack(pe, keep[:, None, None, :] & mx.ones((1, 1, self.P, 1), dtype=mx.bool_), self.rp(self._rope_p))
return pe, keep, self.l0_logits_from(hp, ptoks)
def level_logits(self, k, inp, pe, keep):
h = self.in_proj(inp.astype(self.cdt)) + self.level_emb.weight[k - 1]
h = mx.concatenate([pe, h], axis=1)
n = inp.shape[1]
kk = mx.concatenate([keep, mx.ones((keep.shape[0], n), dtype=mx.bool_)], 1)
T = self.P + n
rows = mx.arange(T)[:, None]; cols = mx.arange(T)[None, :]
allowed = ~((rows < self.P) & (cols >= self.P))
h = self._stack(h, allowed[None, None] & kk[:, None, None, :], self.rp(self._rope_lv[k - 1]))
return self.head(k, h[:, self.P:])
def generate(gen: Gen, fc: FrozenCodec, ptoks):
"""greedy; NFE = 1 (prefix/level0) + K-1 level forwards."""
pe, keep, l0 = gen.encode_prefix(ptoks)
c = mx.argmax(l0, -1); z = fc.contrib(c, 0); codes = [c]
for k in range(1, gen.K):
c = mx.argmax(gen.level_logits(k, fc.next_input(z, k), pe, keep), -1)
codes.append(c); z = z + fc.contrib(c, k)
return fc.decode(z), codes
def count_params(m):
from mlx.utils import tree_flatten
return sum(v.size for _, v in tree_flatten(m.parameters()))