Download ptr/models.py from eac123/ptr-recurrent-depth: direct link, hf CLI and curl.
- Browser
- Download file 5.08 kB
-
https://huggingface.co/eac123/ptr-recurrent-depth/resolve/main/ptr/models.py
- Command line
-
hf download hf://eac123/ptr-recurrent-depth/ptr/models.py
-
curl -L -o models.py https://huggingface.co/eac123/ptr-recurrent-depth/resolve/main/ptr/models.py
5.08 kB
| """Two small transformer architectures for pointer chasing. | |
| PtrTransformer(S, d_model, n_heads, d_ff, depth): | |
| - `depth` pre-LN transformer blocks (GPT-2 style, bidirectional attention). | |
| - forward(f, x, k, n_iters): the SAME `depth` blocks are applied `n_iters` | |
| times, with a learned per-iteration step embedding added to the hidden | |
| state before each unroll. n_iters=1 -> conventional fixed-depth model. | |
| depth=1, n_iters=R -> classic weight-tied recurrent (looped) transformer. | |
| Stability for long unrolls (Li et al. 2604.07822): zero-init the output | |
| projections (attn_out, mlp_out) of every block so each block is the identity | |
| at initialisation and gradients propagate cleanly through all unrolls. | |
| Parameter/FLOP accounting lives in flops.py; the matched-budget logic is: | |
| per-step training FLOPs ~ n_iters * depth * (block cost) | |
| parameter count ~ depth * block params (+ shared embeddings/head) | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| class Block(nn.Module): | |
| def __init__(self, d_model, n_heads, d_ff, zero_init=True): | |
| super().__init__() | |
| assert d_model % n_heads == 0 | |
| self.n_heads = n_heads | |
| self.d_model = d_model | |
| self.head_dim = d_model // n_heads | |
| self.ln1 = nn.LayerNorm(d_model) | |
| self.ln2 = nn.LayerNorm(d_model) | |
| self.qkv = nn.Linear(d_model, 3 * d_model) | |
| self.attn_out = nn.Linear(d_model, d_model) | |
| self.mlp_in = nn.Linear(d_model, d_ff) | |
| self.mlp_out = nn.Linear(d_ff, d_model) | |
| if zero_init: | |
| nn.init.zeros_(self.attn_out.weight) | |
| nn.init.zeros_(self.attn_out.bias) | |
| nn.init.zeros_(self.mlp_out.weight) | |
| nn.init.zeros_(self.mlp_out.bias) | |
| def forward(self, h): | |
| B, T, _ = h.shape | |
| q, k, v = self.qkv(self.ln1(h)).chunk(3, dim=-1) | |
| q = q.view(B, T, self.n_heads, self.head_dim).transpose(1, 2) | |
| k = k.view(B, T, self.n_heads, self.head_dim).transpose(1, 2) | |
| v = v.view(B, T, self.n_heads, self.head_dim).transpose(1, 2) | |
| att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim) | |
| att = att.softmax(dim=-1) | |
| o = (att @ v).transpose(1, 2).reshape(B, T, self.d_model) | |
| h = h + self.attn_out(o) | |
| h = h + self.mlp_out(F.gelu(self.mlp_in(self.ln2(h)))) | |
| return h | |
| class PtrTransformer(nn.Module): | |
| """Input sequence: [f(0..S-1), x, k]. Readout at the LAST position. | |
| f: LongTensor [B, S] of state ids; x: [B]; k: [B] (1-indexed hop count, | |
| embedded via a dedicated table of size K_MAX+2; k > 8 is OOD by design). | |
| """ | |
| K_MAX = 64 | |
| def __init__(self, S, d_model, n_heads, d_ff, depth, stem_depth=0, | |
| max_len=None, zero_init=True): | |
| super().__init__() | |
| self.S = S | |
| self.depth = depth | |
| self.stem_depth = stem_depth | |
| self.d_model = d_model | |
| max_len = max_len or (S + 2) | |
| self.state_emb = nn.Embedding(S, d_model) | |
| self.pos_emb = nn.Embedding(max_len, d_model) | |
| self.k_emb = nn.Embedding(self.K_MAX + 2, d_model) | |
| # step embeddings must cover the largest unroll count used anywhere: | |
| # aligned variants evaluate at R = k + margin, up to 64 + 14 | |
| self.step_emb = nn.Embedding(self.K_MAX + 2 + 14, d_model) | |
| # untied stem: applied ONCE before the recurrent loop (never unrolled) | |
| self.stem = nn.ModuleList( | |
| Block(d_model, n_heads, d_ff, zero_init=False) | |
| for _ in range(stem_depth) | |
| ) | |
| self.blocks = nn.ModuleList( | |
| Block(d_model, n_heads, d_ff, zero_init=zero_init) | |
| for _ in range(depth) | |
| ) | |
| self.ln_f = nn.LayerNorm(d_model) | |
| self.head = nn.Linear(d_model, S, bias=False) | |
| def forward(self, f, x, k, n_iters=1, return_all=False): | |
| B, S = f.shape | |
| T = S + 2 | |
| tok = torch.cat( | |
| [self.state_emb(f), | |
| self.state_emb(x)[:, None, :], | |
| self.k_emb(k)[:, None, :]], | |
| dim=1, | |
| ) | |
| pos = torch.arange(T, device=f.device) | |
| h = tok + self.pos_emb(pos)[None] | |
| for blk in self.stem: | |
| h = blk(h) | |
| outs = [] | |
| for t in range(n_iters): | |
| step = torch.full((B,), t, device=f.device, dtype=torch.long) | |
| h = h + self.step_emb(step)[:, None, :] | |
| for blk in self.blocks: | |
| h = blk(h) | |
| if return_all: | |
| outs.append(self.head(self.ln_f(h[:, -1]))) | |
| logits = self.head(self.ln_f(h[:, -1])) | |
| if return_all: | |
| return logits, torch.stack(outs, dim=1) # [B, n_iters, S] | |
| return logits | |
| def build_model(arch, S=64, d_model=128, n_heads=4, d_ff=512, depth=4, | |
| stem_depth=0, zero_init=True, **_): | |
| """arch 'fixed' -> depth blocks applied once. | |
| arch 'rec' -> same blocks applied n_iters times (chosen at call time).""" | |
| return PtrTransformer(S, d_model, n_heads, d_ff, depth, | |
| stem_depth=stem_depth, zero_init=zero_init) |