alpha-er / tokenization_alpha.py
ajaxdavis's picture
alpha-er: 100M model trained on a from-scratch CUDA-free GPU stack
d7562c8 verified
Raw
History Blame Contribute Delete
3.63 kB
"""alpha-er's byte-level BPE tokenizer (vocab 12,288).
GPT-2-style byte-level BPE, with the vocabulary laid out as:
0 - 255 raw bytes, in byte order, under GPT-2's bytes->unicode mapping
256 - 258 <|user|>, <|assistant|>, <|end_of_text|>
259 + learned merges, where merges[i] produces token 259 + i
Being byte-level means every input encodes — there is no unknown token and no
normalisation step to disagree about. Decoding maps each vocabulary character
back to its byte and interprets the result as UTF-8, so a sequence cut mid
multi-byte character degrades to a replacement character rather than throwing.
"""
from __future__ import annotations
import json
from functools import lru_cache
@lru_cache(1)
def _byte_maps():
"""GPT-2's reversible bytes<->unicode table."""
bs = (list(range(ord("!"), ord("~") + 1))
+ list(range(ord("\xa1"), ord("\xac") + 1))
+ list(range(ord("\xae"), ord("\xff") + 1)))
cs = bs[:]
n = 0
for b in range(256):
if b not in bs:
bs.append(b); cs.append(256 + n); n += 1
b2u = {b: chr(c) for b, c in zip(bs, cs)}
return b2u, {v: k for k, v in b2u.items()}
class AlphaErTokenizer:
def __init__(self, artifacts: dict):
self.vocab = artifacts["vocab"]
self.merges = [tuple(m) for m in artifacts["merges"]]
self.specials = artifacts["specialTokens"]
self.n_special = len(self.specials)
self.first_merge_id = 256 + self.n_special
# rank[(a,b)] = the token id the pair becomes; lower id = earlier merge.
self.rank = {pair: self.first_merge_id + i for i, pair in enumerate(self.merges)}
self.special_ids = {s: 256 + i for i, s in enumerate(self.specials)}
@classmethod
def from_file(cls, path: str) -> "AlphaErTokenizer":
return cls(json.load(open(path)))
def _encode_chunk(self, text: str) -> list[int]:
b2u, _ = _byte_maps()
ids = list(text.encode("utf-8")) # ids 0-255 ARE the bytes
while len(ids) > 1:
best, best_at = None, -1
for i in range(len(ids) - 1):
r = self.rank.get((ids[i], ids[i + 1]))
if r is not None and (best is None or r < best):
best, best_at = r, i
if best is None:
break
ids[best_at:best_at + 2] = [best]
return ids
def encode(self, text: str) -> list[int]:
"""Split on special tokens first so they survive as single ids."""
parts, out = [text], None
for s in self.specials:
nxt = []
for p in parts:
if isinstance(p, int):
nxt.append(p); continue
bits = p.split(s)
for i, bit in enumerate(bits):
if i: nxt.append(self.special_ids[s])
if bit: nxt.append(bit)
parts = nxt
out = []
for p in parts:
out.extend([p] if isinstance(p, int) else self._encode_chunk(p))
return out
def decode(self, ids) -> str:
_, u2b = _byte_maps()
buf, text = bytearray(), []
for i in ids:
tok = self.vocab[i]
if i < 256 + self.n_special and i >= 256: # a special token
text.append(buf.decode("utf-8", "replace")); buf = bytearray()
text.append(tok)
continue
for ch in tok:
buf.append(u2b.get(ch, ord("?") if ord(ch) < 256 else 63))
text.append(buf.decode("utf-8", "replace"))
return "".join(text)