"""Shared helpers for the M1 notebooks (notes 0101-0110). Everything is fp64 CPU torch. The "language model" is a deliberately tiny autoregressive policy whose *entire* sequence space can be enumerated, so every expectation in the notes can be computed EXACTLY (no Monte Carlo) and compared against the hand-derived formula to ~1e-15. Monte Carlo checks are kept for the few identities that are about sampling (and are reported against 4 standard errors). Policy: pi_theta(a | prev, t) = softmax(theta_tok[prev] + theta_pos[t])_a - vocabulary {0,..,V-1} where token V-1 is EOS - `prev` is the previous token (BOS = index V for t=0) - a sequence stops at EOS or after L tokens (truncation, no EOS) This is a valid autoregressive factorisation pi(o) = prod_t pi(o_t | o_ tol {tol:.1e}" def maxabs(a, b): return float((torch.as_tensor(a) - torch.as_tensor(b)).abs().max()) class TinyLM: def __init__(self, V=4, L=4, seed=0, scale=1.0): self.V, self.L = V, L self.EOS = V - 1 g = torch.Generator().manual_seed(seed) self.n_tok = (V + 1) * V # rows: prev in {0..V-1, BOS} self.n_pos = L * V self.theta0 = scale * torch.randn(self.n_tok + self.n_pos, generator=g) self.seqs, self.tokens, self.mask = self._enumerate() self.N = len(self.seqs) self.lengths = self.mask.sum(1) # ---- enumeration of the whole sequence space -------------------------------- def _enumerate(self): V, L, EOS = self.V, self.L, self.EOS seqs = [] def rec(prefix): if len(prefix) == L or (prefix and prefix[-1] == EOS): seqs.append(tuple(prefix)); return for a in range(V): rec(prefix + [a]) rec([]) tokens = torch.full((len(seqs), L), EOS, dtype=torch.long) mask = torch.zeros(len(seqs), L) for i, s in enumerate(seqs): tokens[i, :len(s)] = torch.tensor(s) mask[i, :len(s)] = 1.0 return seqs, tokens, mask def _logits_tables(self, theta): V, L = self.V, self.L tok = theta[:self.n_tok].reshape(V + 1, V) pos = theta[self.n_tok:].reshape(L, V) return tok, pos def token_logprobs(self, theta): """[N, L] log pi(o_t | o_=2.""" R = torch.zeros(lm.N) for i, s in enumerate(lm.seqs): if s[-1] == lm.EOS and len(s) >= 2 and (sum(s[:-1]) % 2 == 1): R[i] = 1.0 return R def flat_grad(scalar, theta): g, = torch.autograd.grad(scalar, theta, retain_graph=True, allow_unused=False) return g def score_matrix(lm, theta): """S[o] = grad_theta log pi(o) as an [N, P] matrix (autograd, one backward per sequence).""" th = theta.clone().requires_grad_(True) lp = lm.seq_logprob(th) rows = [torch.autograd.grad(lp[i], th, retain_graph=True)[0] for i in range(lm.N)] return torch.stack(rows) def token_score_tensor(lm, theta): """T[o, t] = grad_theta log pi(o_t | o_