Monody-Exp / tiny_gdn /ngram.py
kerzgrr's picture
Monody-Exp: monody-pass-7 EMA weights
5c63f8f verified
Raw History Blame Contribute Delete
11.2 kB
"""Hashed n-gram addressing shared by Senryu-2's model, its host table store, and serving.
Each table row is addressed with integer ops that give the same result on CPU
and CUDA: canonical token ids (NFKC-normalized, lowercased, stripped surface
forms) are multiplied by odd 31-bit constants, XOR-combined, mixed, and reduced
modulo a distinct prime per table. Token ids stay below 2**16, so every
product stays below 2**47 and int64 never overflows.
Tables live in one concatenated ``[total_rows, embedding_dim]`` tensor; table
``t`` owns rows ``[offset_t, offset_t + rows_t)``. The prime row counts, the
multipliers, and the canonical map are saved beside every table file and
checked on load, so a table can never be read with a different address map.
"""
from __future__ import annotations
import unicodedata
from dataclasses import dataclass
from pathlib import Path
import numpy as np
import torch
from safetensors import safe_open
from safetensors.torch import save_file
from tokenizers import Tokenizer
from tiny_gdn.config import TinyGDNConfig
NGRAM_TABLES_FILE = "ngram_tables.safetensors"
NGRAM_TABLES_EMA_FILE = "ngram_tables_ema.safetensors"
NGRAM_OPTIMIZER_FILE = "ngram_optimizer.safetensors"
PRIME_STRIDE = 4099
HASH_SEED = 0x6B696D6F
MIX_SHIFT = 29
LOAD_CHUNK_ROWS = 1 << 20
_MASK64 = (1 << 64) - 1
_MILLER_RABIN_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37)
def _splitmix64(state: int) -> tuple[int, int]:
state = (state + 0x9E3779B97F4A7C15) & _MASK64
mixed = ((state ^ (state >> 30)) * 0xBF58476D1CE4E5B9) & _MASK64
mixed = ((mixed ^ (mixed >> 27)) * 0x94D049BB133111EB) & _MASK64
return state, mixed ^ (mixed >> 31)
def _is_prime(value: int) -> bool:
if value < 2:
return False
for base in _MILLER_RABIN_BASES:
if value % base == 0:
return value == base
odd, twos = value - 1, 0
while odd % 2 == 0:
odd //= 2
twos += 1
for base in _MILLER_RABIN_BASES:
witness = pow(base, odd, value)
if witness in (1, value - 1):
continue
for _ in range(twos - 1):
witness = witness * witness % value
if witness == value - 1:
break
else:
return False
return True
def _next_prime(value: int) -> int:
while not _is_prime(value):
value += 1
return value
@dataclass(frozen=True)
class NGramLayout:
"""Where every n-gram of every order lands in the concatenated tables."""
orders: tuple[int, ...]
heads_per_order: int
table_rows: tuple[int, ...]
table_offsets: tuple[int, ...]
multipliers: tuple[tuple[int, ...], ...]
embedding_dim: int
pad_token_id: int
@classmethod
def from_config(cls, config: TinyGDNConfig) -> NGramLayout:
if not config.ngram_orders:
raise ValueError("config has no n-gram memory")
rows: list[int] = []
candidate = config.ngram_table_rows
for _ in range(config.ngram_table_count):
candidate = _next_prime(candidate + PRIME_STRIDE)
rows.append(candidate)
offsets = tuple(int(value) for value in np.cumsum([0, *rows[:-1]]))
state = HASH_SEED
multipliers: list[tuple[int, ...]] = []
max_order = max(config.ngram_orders)
for _ in range(config.ngram_table_count):
table: list[int] = []
for _ in range(max_order):
state, value = _splitmix64(state)
table.append((value >> 33) | (1 << 30) | 1)
multipliers.append(tuple(table))
return cls(
orders=tuple(config.ngram_orders),
heads_per_order=config.ngram_heads_per_order,
table_rows=tuple(rows),
table_offsets=offsets,
multipliers=tuple(multipliers),
embedding_dim=config.ngram_embedding_dim,
pad_token_id=config.bos_token_id,
)
@property
def table_count(self) -> int:
return len(self.table_rows)
@property
def total_rows(self) -> int:
return self.table_offsets[-1] + self.table_rows[-1]
@property
def max_order(self) -> int:
return max(self.orders)
@property
def memory_dim(self) -> int:
return self.table_count * self.embedding_dim
@property
def table_orders(self) -> tuple[int, ...]:
return tuple(order for order in self.orders for _ in range(self.heads_per_order))
def metadata_tensors(self) -> dict[str, torch.Tensor]:
return {
"table_rows": torch.tensor(self.table_rows, dtype=torch.int64),
"hash_multipliers": torch.tensor(self.multipliers, dtype=torch.int64),
}
def check_metadata(self, tensors: dict[str, torch.Tensor], source: Path) -> None:
for name, expected in self.metadata_tensors().items():
found = tensors.get(name)
if found is None or not torch.equal(found.cpu(), expected):
raise ValueError(f"{source} was written for a different n-gram layout ({name})")
def canonical_token_ids(tokenizer: Tokenizer, vocab_size: int) -> torch.Tensor:
"""Map every token id to the smallest id sharing its normalized surface form.
Normalization is NFKC, then strip, then lowercase. Special tokens keep
their own ids, whitespace-only tokens are only NFKC-merged, and tokens
that decode to partial UTF-8 stay distinct.
"""
special = set(tokenizer.get_added_tokens_decoder())
canonical = np.arange(vocab_size, dtype=np.int64)
first_by_key: dict[tuple[str, str], int] = {}
for token_id in range(min(vocab_size, tokenizer.get_vocab_size())):
if token_id in special:
continue
text = tokenizer.decode([token_id], skip_special_tokens=False)
if "\ufffd" in text:
key = ("bytes", tokenizer.id_to_token(token_id) or str(token_id))
else:
normalized = unicodedata.normalize("NFKC", text)
folded = normalized.strip().lower()
key = ("text", folded) if folded else ("space", normalized)
canonical[token_id] = first_by_key.setdefault(key, token_id)
return torch.from_numpy(canonical)
def document_distance(tokens: torch.Tensor, bos_token_id: int, row_starts: torch.Tensor | None = None) -> torch.Tensor:
"""Offset of every token in a flat ``[N]`` stream from its document's first token.
Documents start at position 0, at every BOS token, and at ``row_starts``.
"""
starts = tokens == bos_token_id
starts[0] = True
if row_starts is not None:
starts[row_starts] = True
positions = torch.arange(tokens.numel(), device=tokens.device)
first = torch.where(starts, positions, torch.zeros_like(positions)).cummax(dim=0).values
return positions - first
def ngram_row_indices(
canonical: torch.Tensor,
layout: NGramLayout,
*,
distance: torch.Tensor | None = None,
history: torch.Tensor | None = None,
) -> torch.Tensor:
"""Global table rows ``[..., L, tables]`` for the n-grams ending at each token.
``canonical`` holds int64 canonical ids ``[..., L]``. Tokens before a
document start read as the pad id: with ``distance`` (same shape) a
document starts wherever the distance is zero; with ``history`` (the
``max_order - 1`` canonical ids before position 0) the sequence continues
a cached one; with neither, position 0 starts the only document.
"""
span = layout.max_order
length = canonical.shape[-1]
pad = layout.pad_token_id
if history is not None:
if history.shape[-1] != span - 1:
raise ValueError(f"history must hold {span - 1} tokens")
extended = torch.cat([history, canonical], dim=-1)
shifted = [extended[..., span - 1 - back : span - 1 - back + length] for back in range(span)]
else:
shifted = [canonical]
for back in range(1, span):
if back >= length:
previous = torch.full_like(canonical, pad)
else:
padding = canonical.new_full((*canonical.shape[:-1], back), pad)
previous = torch.cat([padding, canonical[..., :-back]], dim=-1)
if distance is not None:
previous = torch.where(distance >= back, previous, pad)
shifted.append(previous)
columns = []
for table, order in enumerate(layout.table_orders):
multipliers = layout.multipliers[table]
mixed = shifted[0] * multipliers[0]
for back in range(1, order):
mixed = mixed ^ (shifted[back] * multipliers[back])
mixed = mixed ^ (mixed >> MIX_SHIFT)
columns.append(torch.remainder(mixed, layout.table_rows[table]) + layout.table_offsets[table])
return torch.stack(columns, dim=-1)
def save_table_file(path: Path, tables: torch.Tensor, layout: NGramLayout, canonical: torch.Tensor) -> None:
if tables.shape != (layout.total_rows, layout.embedding_dim):
raise ValueError(f"tables have shape {tuple(tables.shape)}, layout needs {(layout.total_rows, layout.embedding_dim)}")
save_file(
{
"tables": tables.contiguous(),
"canonical_ids": canonical.to(dtype=torch.int64, device="cpu").contiguous(),
**layout.metadata_tensors(),
},
str(path),
)
def read_table_metadata(path: Path, layout: NGramLayout) -> torch.Tensor:
"""Check ``path`` against ``layout`` and return its canonical token map."""
with safe_open(str(path), framework="pt", device="cpu") as handle:
metadata = {name: handle.get_tensor(name) for name in ("table_rows", "hash_multipliers")}
layout.check_metadata(metadata, path)
# get_tensor returns a view of a mapping of the whole file, which a kept
# view would hold open; FUSE mounts such as ntfs-3g then cannot delete it.
return handle.get_tensor("canonical_ids").clone()
def copy_rows_from_file(path: Path, name: str, destination: torch.Tensor) -> None:
"""Copy tensor ``name`` of a safetensors file into ``destination`` in row chunks."""
with safe_open(str(path), framework="pt", device="cpu") as handle:
source = handle.get_slice(name)
shape = tuple(source.get_shape())
if shape != tuple(destination.shape):
raise ValueError(f"{path} holds {name} of shape {shape}, expected {tuple(destination.shape)}")
for start in range(0, shape[0], LOAD_CHUNK_ROWS):
stop = min(start + LOAD_CHUNK_ROWS, shape[0])
destination[start:stop].copy_(source[start:stop])
def load_table_file(
path: Path,
layout: NGramLayout,
*,
device: torch.device | str = "cpu",
dtype: torch.dtype = torch.float32,
out: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""``(tables, canonical_ids)`` from ``path``, copied in row chunks to bound host memory."""
canonical = read_table_metadata(path, layout)
tables = (
torch.empty((layout.total_rows, layout.embedding_dim), dtype=dtype, device=device)
if out is None
else out
)
copy_rows_from_file(path, "tables", tables)
return tables, canonical