Download python/bhc/xlit_model.py from sourojitdotcom/AksharaLP-1M-v2: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2/resolve/main/python/bhc/xlit_model.py
- Command line
-
hf download hf://sourojitdotcom/AksharaLP-1M-v2/python/bhc/xlit_model.py
-
curl -L -o xlit_model.py https://huggingface.co/sourojitdotcom/AksharaLP-1M-v2/resolve/main/python/bhc/xlit_model.py
15.7 kB
| """BhashaCore Track B candidate generator: tiny character-level Transformer (Roman -> Devanagari). | |
| Pre-LN encoder/decoder, learned positions, tied decoder input/output embeddings. Inference = batched beam | |
| search over all words of a sentence at once, with an incremental (cached) decoder, optionally constrained to a | |
| Devanagari lexicon trie. Weights are stored as INT8 (per-output-row symmetric scales) + fp16 for small vectors | |
| and de-quantised to fp32 at load time (compute is fp32; storage is INT8). | |
| """ | |
| import io, gzip, json, math, os | |
| import numpy as np | |
| import torch, torch.nn as nn, torch.nn.functional as F | |
| ROMAN = "abcdefghijklmnopqrstuvwxyz" | |
| PAD, BOS, EOS, UNK = 0, 1, 2, 3 | |
| DEVA_CHARS = [chr(c) for c in range(0x0900, 0x0980)] # full block (128), most unused but cheap | |
| SRC_VOCAB = ["<pad>", "<s>", "</s>", "<unk>"] + list(ROMAN) | |
| TGT_VOCAB = ["<pad>", "<s>", "</s>", "<unk>"] + DEVA_CHARS | |
| S2I = {c: i for i, c in enumerate(SRC_VOCAB)}; T2I = {c: i for i, c in enumerate(TGT_VOCAB)} | |
| MAXLEN = 32 | |
| def enc_src(w): return [S2I.get(c, UNK) for c in w.lower()[:MAXLEN - 1]] + [EOS] | |
| def enc_tgt(w): return [T2I.get(c, UNK) for c in w[:MAXLEN - 2]] + [EOS] | |
| def dec_tgt(ids): return "".join(TGT_VOCAB[i] for i in ids if i > UNK) | |
| class MHA(nn.Module): | |
| def __init__(s, d, h): | |
| super().__init__(); s.h = h; s.dk = d // h | |
| s.q = nn.Linear(d, d); s.kv = nn.Linear(d, 2 * d); s.o = nn.Linear(d, d) | |
| def forward(s, x, mem, mask=None, cache=None): | |
| B, T, D = x.shape | |
| q = s.q(x).view(B, T, s.h, s.dk).transpose(1, 2) | |
| if cache is not None and "k" in cache and cache.get("static"): | |
| k, v = cache["k"], cache["v"] | |
| else: | |
| k, v = s.kv(mem).view(B, -1, 2, s.h, s.dk).permute(2, 0, 3, 1, 4) | |
| if cache is not None: | |
| if "k" in cache: # self-attn incremental | |
| k = torch.cat([cache["k"], k], 2); v = torch.cat([cache["v"], v], 2) | |
| cache["k"], cache["v"] = k, v | |
| if mask is not None and mask.dtype != q.dtype: mask = mask.to(q.dtype) | |
| a = F.scaled_dot_product_attention(q, k, v, attn_mask=mask) | |
| return s.o(a.transpose(1, 2).reshape(B, T, D)) | |
| class EncLayer(nn.Module): | |
| def __init__(s, d, h, ff, p): | |
| super().__init__(); s.n1 = nn.LayerNorm(d); s.att = MHA(d, h); s.n2 = nn.LayerNorm(d) | |
| s.ff = nn.Sequential(nn.Linear(d, ff), nn.GELU(), nn.Linear(ff, d)); s.dr = nn.Dropout(p) | |
| def forward(s, x, mask): | |
| y = s.n1(x); x = x + s.dr(s.att(y, y, mask)); return x + s.dr(s.ff(s.n2(x))) | |
| class DecLayer(nn.Module): | |
| def __init__(s, d, h, ff, p): | |
| super().__init__(); s.n1 = nn.LayerNorm(d); s.sa = MHA(d, h); s.n2 = nn.LayerNorm(d); s.ca = MHA(d, h) | |
| s.n3 = nn.LayerNorm(d); s.ff = nn.Sequential(nn.Linear(d, ff), nn.GELU(), nn.Linear(ff, d)); s.dr = nn.Dropout(p) | |
| def forward(s, x, mem, smask, mmask, cache=None): | |
| y = s.n1(x); x = x + s.dr(s.sa(y, y, smask, None if cache is None else cache["sa"])) | |
| x = x + s.dr(s.ca(s.n2(x), mem, mmask, None if cache is None else cache["ca"])) | |
| return x + s.dr(s.ff(s.n3(x))) | |
| class XlitTransformer(nn.Module): | |
| def __init__(s, d=128, h=4, enc=3, dec=2, ff=384, p=0.1): | |
| super().__init__(); s.cfg = dict(d=d, h=h, enc=enc, dec=dec, ff=ff, p=p) | |
| s.semb = nn.Embedding(len(SRC_VOCAB), d, padding_idx=PAD); s.temb = nn.Embedding(len(TGT_VOCAB), d, padding_idx=PAD) | |
| s.spos = nn.Embedding(MAXLEN, d); s.tpos = nn.Embedding(MAXLEN, d) | |
| s.enc = nn.ModuleList(EncLayer(d, h, ff, p) for _ in range(enc)); s.dec = nn.ModuleList(DecLayer(d, h, ff, p) for _ in range(dec)) | |
| s.en = nn.LayerNorm(d); s.dn = nn.LayerNorm(d); s.scale = math.sqrt(d) | |
| for e in (s.semb, s.temb): nn.init.normal_(e.weight, 0, d ** -0.5) | |
| nn.init.normal_(s.spos.weight, 0, 0.02); nn.init.normal_(s.tpos.weight, 0, 0.02) | |
| def encode(s, src): | |
| B, S = src.shape; pad = src == PAD | |
| x = s.semb(src) * s.scale + s.spos(torch.arange(S, device=src.device))[None] | |
| m = torch.zeros(B, 1, 1, S, device=src.device).masked_fill(pad[:, None, None, :], float("-inf")) | |
| for l in s.enc: x = l(x, m) | |
| return s.en(x), m | |
| def logits(s, x): return x @ s.temb.weight.t() | |
| def forward(s, src, tgt_in): | |
| mem, mm = s.encode(src); B, T = tgt_in.shape | |
| x = s.temb(tgt_in) * s.scale + s.tpos(torch.arange(T, device=tgt_in.device))[None] | |
| causal = torch.full((T, T), float("-inf"), device=tgt_in.device).triu(1) | |
| for l in s.dec: x = l(x, mem, causal, mm) | |
| return s.logits(s.dn(x)) | |
| def step(s, tok, pos, mem, mm, caches): | |
| x = s.temb(tok) * s.scale + s.tpos.weight[pos][None, None] | |
| for l, c in zip(s.dec, caches): x = l(x, mem, None, mm, c) | |
| return F.log_softmax(s.logits(s.dn(x))[:, -1], -1) | |
| def n_params(s): return sum(p.numel() for p in s.parameters()) | |
| # ------------------------------------------------------------------ lexicon trie (for constrained decoding) | |
| class Trie: | |
| """Character trie over a word list, stored as flat arrays; node 0 = root. child(node, tok) lookup via dict.""" | |
| def __init__(s, words): | |
| s.kids = [{}]; s.final = [False] | |
| for w in words: | |
| n = 0 | |
| for c in w: | |
| t = T2I.get(c) | |
| if t is None: break | |
| nx = s.kids[n].get(t) | |
| if nx is None: | |
| nx = len(s.kids); s.kids[n][t] = nx; s.kids.append({}); s.final.append(False) | |
| n = nx | |
| else: | |
| s.final[n] = True | |
| # ------------------------------------------------------------------ beam search | |
| def beam_search(model, words, beam=8, nbest=8, trie=None, maxlen=None, constrain=None): | |
| """Return per word a list of (devanagari, logprob) best first. All words decoded in one batch. | |
| trie: constrain outputs to lexicon words (EOS allowed only at final nodes); constrain[i] selects which rows | |
| are constrained (default: all rows when a trie is given).""" | |
| if not words: return [] | |
| src = [enc_src(w) for w in words]; S = max(map(len, src)) | |
| src = torch.tensor([x + [PAD] * (S - len(x)) for x in src]) | |
| mem, mm = model.encode(src); W = len(words); K = beam | |
| mem = mem.repeat_interleave(K, 0); mm = mm.repeat_interleave(K, 0) | |
| caches = [{"sa": {}, "ca": {"static": False}} for _ in model.dec] | |
| # first step: only beam 0 alive | |
| score = torch.full((W, K), float("-inf")); score[:, 0] = 0 | |
| tok = torch.full((W * K, 1), BOS); seqs = [[[] for _ in range(K)] for _ in range(W)] | |
| if trie and constrain is None: constrain = [True] * len(words) | |
| nodes = [[0 if constrain[i] else None] * K for i in range(W)] if trie else None | |
| done = [[] for _ in range(W)]; V = len(TGT_VOCAB) | |
| L = maxlen or min(MAXLEN - 1, S + 8) | |
| for t in range(L): | |
| lp = model.step(tok, t, mem, mm, caches) | |
| if t == 0: | |
| for c in caches: c["ca"]["static"] = True | |
| lp = lp.view(W, K, V) | |
| if trie: | |
| mask = torch.zeros((W, K, V)) | |
| for i in range(W): | |
| if not constrain[i]: continue | |
| mask[i] = float("-inf") | |
| for k in range(K): | |
| n = nodes[i][k] | |
| if n is None: continue | |
| ks = list(trie.kids[n].keys()) | |
| if ks: mask[i, k, ks] = 0 | |
| if trie.final[n]: mask[i, k, EOS] = 0 | |
| lp = lp + mask | |
| lp[:, :, PAD] = float("-inf"); lp[:, :, BOS] = float("-inf"); lp[:, :, UNK] = float("-inf") | |
| tot = (score[:, :, None] + lp).view(W, -1) | |
| top, idx = tot.topk(2 * K, -1) | |
| new_score = torch.full((W, K), float("-inf")); reorder = torch.zeros(W, K, dtype=torch.long); new_tok = torch.full((W, K), EOS) | |
| new_seqs = [[[] for _ in range(K)] for _ in range(W)]; new_nodes = [[None] * K for _ in range(W)] if trie else None | |
| alive_any = False | |
| for i in range(W): | |
| j = 0 | |
| for sc, ix in zip(top[i].tolist(), idx[i].tolist()): | |
| if sc == float("-inf"): break | |
| b, v = divmod(ix, V) | |
| if v == EOS: | |
| if len(done[i]) < nbest: done[i].append((dec_tgt(seqs[i][b]), sc)) | |
| continue | |
| if j < K: | |
| new_score[i, j] = sc; reorder[i, j] = i * K + b; new_tok[i, j] = v | |
| new_seqs[i][j] = seqs[i][b] + [v] | |
| if trie: new_nodes[i][j] = trie.kids[nodes[i][b]][v] if constrain[i] else None | |
| j += 1 | |
| # stop this word if enough finished and best alive can't beat worst finished | |
| if len(done[i]) >= nbest and (j == 0 or new_score[i, 0].item() < min(x[1] for x in done[i])): | |
| new_score[i] = float("-inf") | |
| elif j: alive_any = True | |
| if not alive_any: break | |
| flat = reorder.view(-1) | |
| for c in caches: | |
| c["sa"]["k"] = c["sa"]["k"][flat]; c["sa"]["v"] = c["sa"]["v"][flat] | |
| score = new_score; seqs = new_seqs; nodes = new_nodes | |
| tok = torch.cat([tok[flat], new_tok.view(-1, 1)], 1)[:, -1:] | |
| out = [] | |
| for i in range(W): | |
| d = {} | |
| for s_, sc in done[i]: | |
| if s_ not in d or sc > d[s_]: d[s_] = sc | |
| out.append(sorted(d.items(), key=lambda x: -x[1])[:nbest]) | |
| return out | |
| # ------------------------------------------------------------------ INT8 export / load | |
| def export_int8(model, path): | |
| """Linear/embedding matrices -> int8 with per-row fp16 scale; vectors (bias, LN) -> fp16. Single .npz-like blob.""" | |
| sd = model.state_dict(); meta = {"cfg": model.cfg, "tensors": []}; buf = io.BytesIO() | |
| for k, v in sd.items(): | |
| v = v.float().numpy() | |
| if v.ndim == 2: | |
| sc = np.abs(v).max(1, keepdims=True) / 127.0; sc[sc == 0] = 1 | |
| q = np.clip(np.round(v / sc), -127, 127).astype(np.int8) | |
| meta["tensors"].append([k, list(v.shape), "i8", buf.tell()]); buf.write(q.tobytes()); buf.write(sc.astype(np.float16).tobytes()) | |
| else: | |
| meta["tensors"].append([k, list(v.shape), "f16", buf.tell()]); buf.write(v.astype(np.float16).tobytes()) | |
| hdr = json.dumps(meta, separators=(",", ":")).encode() | |
| with open(path, "wb") as f: | |
| f.write(len(hdr).to_bytes(4, "little")); f.write(hdr); f.write(buf.getvalue()) | |
| def load_int8(path): | |
| b = open(path, "rb").read(); n = int.from_bytes(b[:4], "little"); meta = json.loads(b[4:4 + n]); data = b[4 + n:] | |
| m = XlitTransformer(**meta["cfg"]); sd = {} | |
| for k, shape, kind, off in meta["tensors"]: | |
| cnt = int(np.prod(shape)) | |
| if kind == "i8": | |
| q = np.frombuffer(data, np.int8, cnt, off).reshape(shape).astype(np.float32) | |
| sc = np.frombuffer(data, np.float16, shape[0], off + cnt).astype(np.float32)[:, None] | |
| sd[k] = torch.from_numpy(q * sc) | |
| else: | |
| sd[k] = torch.from_numpy(np.frombuffer(data, np.float16, cnt, off).astype(np.float32).reshape(shape)) | |
| m.load_state_dict(sd); m.eval(); return m | |
| def score_pairs(model, srcs, tgts): | |
| """Teacher-forced log P(tgt | src) for each pair (batched).""" | |
| if not srcs: return [] | |
| s = [enc_src(w) for w in srcs]; t = [enc_tgt(w) for w in tgts]; S = max(map(len, s)); T = max(map(len, t)) | |
| src = torch.tensor([x + [PAD] * (S - len(x)) for x in s]); tin = torch.tensor([[BOS] + x[:-1] + [PAD] * (T - len(x)) for x in t]) | |
| tout = torch.tensor([x + [PAD] * (T - len(x)) for x in t]) | |
| lp = F.log_softmax(model(src, tin), -1).gather(-1, tout[..., None])[..., 0] | |
| return (lp * (tout != PAD)).sum(-1).tolist() | |
| def beam_search_fast(model, words, beam=8, nbest=8, trie=None, maxlen=None, constrain=None): | |
| """Same search as beam_search (identical results), with the per-candidate bookkeeping vectorised: | |
| hypotheses kept as a token-history tensor; only EOS hits and trie-node updates touch Python.""" | |
| if not words: return [] | |
| src = [enc_src(w) for w in words]; S = max(map(len, src)) | |
| src = torch.tensor([x + [PAD] * (S - len(x)) for x in src]) | |
| mem, mm = model.encode(src); W = len(words); K = beam; V = len(TGT_VOCAB) | |
| mem = mem.repeat_interleave(K, 0); mm = mm.repeat_interleave(K, 0) | |
| caches = [{"sa": {}, "ca": {"static": False}} for _ in model.dec] | |
| score = torch.full((W, K), float("-inf")); score[:, 0] = 0 | |
| tok = torch.full((W * K, 1), BOS); hist = torch.zeros((W * K, 0), dtype=torch.long) | |
| if trie and constrain is None: constrain = [True] * W | |
| crow = [i for i in range(W) if trie and constrain[i]] | |
| nodes = {i: [0] * K for i in crow} | |
| if trie and not hasattr(trie, "_allow"): trie._allow = {} | |
| done = [[] for _ in range(W)]; alive = [True] * W | |
| L = maxlen or min(MAXLEN - 1, S + 8); NEG = float("-inf") | |
| for t in range(L): | |
| lp = model.step(tok, t, mem, mm, caches) | |
| if t == 0: | |
| for c in caches: c["ca"]["static"] = True | |
| lp = lp.view(W, K, V) | |
| if crow: | |
| mask = torch.full((len(crow), K, V), NEG) | |
| for j, i in enumerate(crow): | |
| for k in range(K): | |
| n = nodes[i][k] | |
| if n is None: continue | |
| a = trie._allow.get(n) | |
| if a is None: | |
| a = list(trie.kids[n].keys()) + ([EOS] if trie.final[n] else []); a = torch.tensor(a, dtype=torch.long); trie._allow[n] = a | |
| if len(a): mask[j, k, a] = 0 | |
| lp[crow] = lp[crow] + mask | |
| lp[:, :, PAD] = NEG; lp[:, :, BOS] = NEG; lp[:, :, UNK] = NEG | |
| tot = (score[:, :, None] + lp).view(W, -1) | |
| top, idx = tot.topk(2 * K, -1) | |
| b = idx // V; v = idx % V; fin = torch.isfinite(top) | |
| eos = (v == EOS) & fin | |
| if eos.any(): | |
| for r, c in eos.nonzero().tolist(): | |
| if alive[r] and len(done[r]) < nbest: | |
| done[r].append((dec_tgt(hist[r * K + b[r, c]].tolist()), top[r, c].item())) | |
| ne = (v != EOS) & fin; rank = ne.long().cumsum(1) - 1; keep = ne & (rank < K) | |
| rr, cc = keep.nonzero(as_tuple=True); kk = rank[rr, cc] | |
| new_score = torch.full((W, K), NEG); new_score[rr, kk] = top[rr, cc] | |
| reorder = (torch.arange(W)[:, None] * K).repeat(1, K); reorder[rr, kk] = rr * K + b[rr, cc] | |
| new_tok = torch.full((W, K), EOS); new_tok[rr, kk] = v[rr, cc] | |
| if crow: | |
| nn_ = {i: [None] * K for i in crow} | |
| bl, vl, kl, rl = b[rr, cc].tolist(), v[rr, cc].tolist(), kk.tolist(), rr.tolist() | |
| for r, bb, vv, k in zip(rl, bl, vl, kl): | |
| if r in nn_: nn_[r][k] = trie.kids[nodes[r][bb]][vv] | |
| nodes = nn_ | |
| nkeep = keep.sum(1).tolist(); best0 = new_score[:, 0].tolist(); any_alive = False | |
| for i in range(W): | |
| if not alive[i]: new_score[i] = NEG; continue | |
| if len(done[i]) >= nbest and (nkeep[i] == 0 or best0[i] < min(x[1] for x in done[i])): new_score[i] = NEG; alive[i] = False | |
| elif nkeep[i]: any_alive = True | |
| else: alive[i] = False | |
| if not any_alive: break | |
| flat = reorder.view(-1) | |
| for c in caches: | |
| c["sa"]["k"] = c["sa"]["k"][flat]; c["sa"]["v"] = c["sa"]["v"][flat] | |
| hist = torch.cat([hist[flat], new_tok.view(-1, 1)], 1) | |
| score = new_score; tok = new_tok.view(-1, 1) | |
| out = [] | |
| for i in range(W): | |
| d = {} | |
| for s_, sc in done[i]: | |
| if s_ not in d or sc > d[s_]: d[s_] = sc | |
| out.append(sorted(d.items(), key=lambda x: -x[1])[:nbest]) | |
| return out | |