File size: 5,898 Bytes
bcc8d35 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """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_<t)
(with a Markov-1 context) so every identity about the general case holds verbatim.
"""
import os, sys, math, itertools
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "") # never touch the GPUs
import torch
torch.set_default_dtype(torch.float64)
torch.set_num_threads(2)
CHECKS = []
def check(name, err, tol, kind="exact"):
"""Record + assert a numerical identity. kind='exact' (fp64 arithmetic) or 'mc' (4-sigma Monte Carlo)."""
err = float(err)
ok = err <= tol
CHECKS.append((name, err, tol, kind, ok))
print(f"CHECK|{kind}|{name}|{err:.3e}|tol={tol:.1e}|{'PASS' if ok else 'FAIL'}")
assert ok, f"{name}: err {err:.3e} > 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_<t) for every enumerated sequence (0 where masked)."""
V, L = self.V, self.L
tok, pos = self._logits_tables(theta)
prev = torch.cat([torch.full((self.N, 1), V, dtype=torch.long), self.tokens[:, :-1]], 1) # BOS = V
logits = tok[prev] + pos[None, :, :] # [N, L, V]
lp = torch.log_softmax(logits, -1)
lp = lp.gather(-1, self.tokens[..., None]).squeeze(-1)
return lp * self.mask
def seq_logprob(self, theta):
return self.token_logprobs(theta).sum(1)
def probs(self, theta):
return self.seq_logprob(theta).exp()
# ---- naive, loop-based reference (used to cross-check the vectorised code) --
def seq_logprob_naive(self, theta, seq):
V = self.V
tok, pos = self._logits_tables(theta)
lp, prev = 0.0, V
for t, a in enumerate(seq):
logits = tok[prev] + pos[t]
z = logits - logits.max()
lp = lp + (z[a] - torch.log(torch.exp(z).sum()))
prev = a
return lp
# ---- exact state machinery for value functions ------------------------------
def prefixes(self):
"""All non-terminal prefixes (the MDP states), shortest first."""
out = [()]
for k in range(1, self.L):
out += [p for p in itertools.product(range(self.V - 1), repeat=k)] # non-EOS tokens only
return out
def cond_probs(self, theta, prefix):
V = self.V
tok, pos = self._logits_tables(theta)
prev = V if len(prefix) == 0 else prefix[-1]
return torch.softmax(tok[prev] + pos[len(prefix)], -1)
def default_reward(lm):
"""Binary verifier-style reward: success iff the sequence ends in EOS and its token sum is odd and length>=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_<t) as [N, L, P]."""
th = theta.clone().requires_grad_(True)
lp = lm.token_logprobs(th)
P = theta.numel()
out = torch.zeros(lm.N, lm.L, P)
for i in range(lm.N):
for t in range(int(lm.lengths[i])):
out[i, t] = torch.autograd.grad(lp[i, t], th, retain_graph=True)[0]
return out
def perturb(theta, delta, seed=1):
g = torch.Generator().manual_seed(seed)
d = torch.randn(theta.numel(), generator=g)
return theta + delta * d / d.norm()
|