WP31 PRISME: phases 0-3 implementation, tests, data pipeline, training scripts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
30 changed files +20,001 −0
added
.gitignore
+24 −0
@@ -0,0 +1,24 @@ | |||||
| 1 | +.venv/ | ||||
| 2 | +__pycache__/ | ||||
| 3 | +*.pyc | ||||
| 4 | +.pytest_cache/ | ||||
| 5 | +.hypothesis/ | ||||
| 6 | +experiments/runs/**/*.pt | ||||
| 7 | +experiments/runs/**/*.safetensors | ||||
| 8 | +experiments/runs/**/*.bin | ||||
| 9 | +data/raw/ | ||||
| 10 | +data/cache/ | ||||
| 11 | +data/processed/*.bin | ||||
| 12 | +data/processed/*.npy | ||||
| 13 | +*.log.gz | ||||
| 14 | +paper/*.aux | ||||
| 15 | +paper/*.bbl | ||||
| 16 | +paper/*.blg | ||||
| 17 | +paper/*.fls | ||||
| 18 | +paper/*.fdb_latexmk | ||||
| 19 | +paper/*.out | ||||
| 20 | +paper/*.toc | ||||
| 21 | +paper/*.synctex.gz | ||||
| 22 | +paper/*.lof | ||||
| 23 | +paper/*.lot | ||||
| 24 | +.DS_Store | ||||
added
CLAUDE.md
+15 −0
@@ -0,0 +1,15 @@ | |||||
| 1 | +# CLAUDE.md — WP31: PRISME-SLM (execution mandate) | ||||
| 2 | + | ||||
| 3 | +This repository executes UQO Working Paper 31, *PRISME: Intervention-Supervised Sparse Workspace Editing for | ||||
| 4 | +Small Language Models* (author: Simon-Pierre Boucher, UQO). The full research mandate (phases 0–15, immutable | ||||
| 5 | +rules, hypotheses H1–H7, baselines, ablations, paper structure) was supplied by the author on 2026-10-29 and is | ||||
| 6 | +reproduced in `docs/MANDATE.md`. Working rules that bind every session in this repository: | ||||
| 7 | + | ||||
| 8 | +1. Scientific truth first: never fabricate runs, metrics, citations, hardware or proofs. Every number in the | ||||
| 9 | + manuscript traces to a run id in `experiments/runs/` and `results/claim_registry.csv`. | ||||
| 10 | +2. Phases are executed in order with acceptance gates logged in `PROGRESS.md`; failures go to `FAILURES.md`, | ||||
| 11 | + design changes to `DECISIONS.md`. Unfavourable runs are never deleted. | ||||
| 12 | +3. Minimal falsifiable version first (no persistent memory, no RAG, no MoE, no RL, no tool use). | ||||
| 13 | +4. Resource boundaries: laptop (M5 Max) + existing MacLustr SSH access only; no purchases, no new credentials. | ||||
| 14 | +5. Independent train / calibration / dev / locked-test splits; test splits are not read before | ||||
| 15 | + `protocol/preregistration.md` is frozen (see `protocol/locked_test_manifest.json`). | ||||
added
DECISIONS.md
+13 −0
@@ -0,0 +1,13 @@ | |||||
| 1 | +# DECISIONS.md — design decisions (dated) | ||||
| 2 | + | ||||
| 3 | +| id | date | decision | rationale | | ||||
| 4 | +|---|---|---|---| | ||||
| 5 | +| D1 | 2026-10-29 | Repository lives at `Recherche-UQO/ia/prisme-slm` (new `ia/` category of the UQO series); Python 3.12 venv via `uv`, PyTorch 2.15 (CPU/MPS), fp32 training. | Series convention; bf16 autocast gave no speed-up on MPS (15.3k vs 15.0k tok/s on M3 Ultra). | | ||||
| 6 | +| D2 | 2026-10-29 | A dedicated `bank_norm` RMSNorm precedes the bank/z projections. | The pre-final-norm residual stream has growing norm; projecting it raw would couple editor dynamics to residual scale. Costs d parameters. | | ||||
| 7 | +| D3 | 2026-10-29 | Workspace/controller parameter counts are NOT forced to the brief's figures. | The brief's figures are reproduced exactly by a closed form (`4dw+Mw+17w²+6wf_w+15.5w+4`); the implemented design needs 7 fewer w×w projections per config (bank is read directly; readout uses raw slot values; controller shares one state MLP across the 24 actions so that scoring costs O(A·w²) not O(A·w·f_w)). Reconciliation reported in `results/tables/param_counts.json`. | | ||||
| 8 | +| D4 | 2026-10-29 | STOP is absorbing in forced-action training too. | Needed for incremental/full-prefix equivalence (test revealed the mismatch). | | ||||
| 9 | +| D5 | 2026-10-29 | Cost proxy c(a) in the controller's Q is the analytic ops ratio of the action (not a learned ĉ); the ĉ head is kept as a diagnostic regressor. | Per-action cost is deterministic given k(a) and T; learning it adds noise without information. | | ||||
| 10 | +| D6 | 2026-10-29 | Synthetic splits are deduplicated by exact text across splits (train written first); eval items colliding with train are dropped and counted. | Low-depth cells have few distinct items; 995 train dups, 256 dev_iid dups removed. | | ||||
| 11 | +| D7 | 2026-10-29 | Phase 4 uses a character-level tokenizer and a 2.1M-parameter backbone trained from scratch on the synthetic train split. | Self-contained learnability test independent of the web corpus/tokenizer. | | ||||
| 12 | +| D8 | 2026-10-29 | 103M pilot backbone: 196.6M tokens (12k steps × 32 × 512) of FineWeb-Edu, one seed; parameter-matched plain decoder baseline (ffn 2144, 102.16M) trained with the identical recipe on a second node. | ~3.7 h per run at 14.8k tok/s on an M3 Ultra; seeds are varied at the editor/controller stage where the pilot's decisive comparison lives. | | ||||
| 13 | +| D9 | 2026-10-29 | `readout_scale`: Δ = readout_scale · U(r(S_f) − r(S0)), with readout_scale a fixed constant set once at bootstrap to the measured rms of the backbone residual stream (5.6 for the tiny backbone). | Scale mismatch blocked the U = 0 bootstrap (see FAILURES). The final RMSNorm makes logits invariant to the joint scale of h and Δ, so this is a reparametrisation of U's learning rate; identity properties are unchanged (tests pass). | | ||||
added
FAILURES.md
+7 −0
@@ -0,0 +1,7 @@ | |||||
| 1 | +# FAILURES.md — failed hypotheses, tests and attempts (never deleted) | ||||
| 2 | + | ||||
| 3 | +| date | what failed | evidence | resolution | | ||||
| 4 | +|---|---|---|---| | ||||
| 5 | +| 2026-10-29 | `test_incremental_equals_full_prefix_forced`: cached decoding ≠ full-prefix forward. | pytest, atol 1e-4 | Root cause: forced-action forward let edits resume after STOP; made STOP absorbing (D4). | | ||||
| 6 | +| 2026-10-29 | rsync exclusion `experiments` also dropped `configs/experiments/`; baseline launch on M3U96b failed twice (`FileNotFoundError`). | `experiments/logs/pretrain_enlarged102m_seed1.log` (first attempts) | anchored pattern `/experiments`. | | ||||
| 7 | +| 2026-10-29 | Stage B bootstrap degeneracy (brief §13.1 confirmed): with U = 0 and lr 1e-3, 700 steps of forced mixed-budget training left val loss identical at every budget (0.4144 / 0.4143 / 0.4143 / 0.4146 / 0.4164 for budgets 0/1/2/4/8); grad-norm 0.01–0.02. Diagnosis: scale mismatch — backbone residual rms 5.64 vs readout difference rms 0.06, so U must grow by ×100 before edits can affect logits. | `experiments/runs/p4_editor_v1_seed01_*/metrics.jsonl` (stopped at step ~750) | D9: fixed non-learned `readout_scale` = measured backbone residual rms at bootstrap (keeps U = 0 init and exact identity; equivalent to a scale-aware learning rate). Variants with nonzero U init (std 0.02) and lr 3e-3 kept running on M2U64 / M4M64c as evidence. | | ||||
added
Makefile
+10 −0
@@ -0,0 +1,10 @@ | |||||
| 1 | +PY=.venv/bin/python | ||||
| 2 | +.PHONY: env test audit params flops paper figures smoke | ||||
| 3 | +env: ; uv venv --python 3.12 .venv && uv pip install -p .venv/bin/python -r requirements-lock.txt | ||||
| 4 | +test: ; $(PY) -m pytest tests -q | ||||
| 5 | +audit: ; $(PY) scripts/environment_audit.py --smoke | ||||
| 6 | +params: ; $(PY) scripts/count_params.py --all-configs --device meta | ||||
| 7 | +flops: ; $(PY) scripts/estimate_flops.py --config configs/prisme_459m.yaml --T 4096 | ||||
| 8 | +smoke: ; $(PY) -m training.pretrain --config configs/experiments/pretrain_103m.yaml --dry-run | ||||
| 9 | +figures: ; $(PY) scripts/reproduce_figures.py --results results/raw/ | ||||
| 10 | +paper: ; cd paper && latexmk -pdf -interaction=nonstopmode main.tex | ||||
added
PROGRESS.md
+10 −0
@@ -0,0 +1,10 @@ | |||||
| 1 | +# PROGRESS.md — WP31 PRISME phase log | ||||
| 2 | + | ||||
| 3 | +| date | phase | status | commands / evidence | measurements | gate | blockers / next | | ||||
| 4 | +|---|---|---|---|---|---|---| | ||||
| 5 | +| 2026-10-29 | 0 env audit | done | `scripts/environment_audit.py --smoke` → `hardware/device_inventory.json`; `scripts/cluster_audit.sh` (read-only SSH) | laptop M5 Max 18c/40 GPU/48 GB, torch 2.15 MPS+CPU smoke OK; 12 LAN nodes reachable incl. 2× M3 Ultra 96 GB, M2 Ultra 64 GB, 3× M4 Max 64 GB | PASS | — | | ||||
| 6 | +| 2026-10-29 | 1 literature | done | `literature/papers.bib` (77 verified entries), `comparison_matrix.csv`, `novelty_audit.md` | all 12 mandated refs exist incl. RecurTrace 2609.03379 and ELR 2610.12582; C3 (differential zero-init bypass) judged not novel; C2 novel only in its multi-action+propensity form | PASS | preregistration frozen before any test split is read (see protocol/) | | ||||
| 7 | +| 2026-10-29 | 2 backbone + counts | done | `scripts/count_params.py --all-configs --device meta`; `scripts/estimate_flops.py` | backbone counts match brief exactly (99,502,848 / 446,744,064 / 1,149,339,648); workspace 2,631,173 vs brief 3,088,260 (reconciled, D3); ops table reproduces brief within 1.3 % | PASS | — | | ||||
| 8 | +| 2026-10-29 | 3 invariants | done | `python -m pytest tests -q` → 22 passed | 15 mandated invariants + controller target-freedom + dense ablation + 5 cache-equivalence tests | PASS | — | | ||||
| 9 | +| 2026-10-29 | 4 learnability | running | `training.train_editor --config configs/experiments/p4_tiny_backbone.yaml --device mps` (laptop) | — | — | then editor bootstrap on frozen tiny backbone | | ||||
| 10 | +| 2026-10-29 | 8 pilot (Stage A) | running | M3U96a: `pretrain_103m.yaml` (run `pretrain103m_seed01_*`); M3U96b: `pretrain_enlarged_102m.yaml` | 14.8k tok/s, 196.6M tokens planned (~3.7 h each) | — | editor/controller stages after completion | | ||||
added
baselines/__init__.py
+0 −0
added
baselines/global_recurrence.py
+78 −0
@@ -0,0 +1,78 @@ | |||||
| 1 | +"""Global latent-recurrence adapter (Huginn/Ouro/ELR-*inspired*, simplified): the whole top representation is | ||||
| 2 | +refined r times by ONE shared block operating at workspace width w, with a zero-initialised residual back to d. | ||||
| 3 | + | ||||
| 4 | +Same frozen backbone, same forced mixed-budget training recipe and same budget semantics as PRISME; the only | ||||
| 5 | +difference is that every "action" rewrites the entire latent (global, dense) instead of 1-2 of 16 slots, and | ||||
| 6 | +there is no slot selection to learn. Disclosed simplifications: no prelude/coda retraining, no truncated | ||||
| 7 | +backprop through time, adapter-level recurrence on a frozen backbone rather than from-scratch recurrent pretraining. | ||||
| 8 | +Parameter count is matched to the PRISME workspace (ffn width chosen in the experiment config).""" | ||||
| 9 | +from __future__ import annotations | ||||
| 10 | + | ||||
| 11 | +import torch | ||||
| 12 | +import torch.nn as nn | ||||
| 13 | + | ||||
| 14 | +from prisme.attention import RMSNorm, SwiGLU | ||||
| 15 | +from prisme.backbone import Backbone | ||||
| 16 | +from prisme.config import PrismeConfig | ||||
| 17 | +import torch.nn.functional as F | ||||
| 18 | + | ||||
| 19 | + | ||||
| 20 | +class RecurrentBlock(nn.Module): | ||||
| 21 | + def __init__(self, w: int, heads: int, ffn: int, eps: float): | ||||
| 22 | + super().__init__() | ||||
| 23 | + self.h, self.hd = heads, w // heads | ||||
| 24 | + self.n1 = RMSNorm(w, eps); self.q = nn.Linear(w, w, bias=False); self.k = nn.Linear(w, w, bias=False) | ||||
| 25 | + self.v = nn.Linear(w, w, bias=False); self.o = nn.Linear(w, w, bias=False) | ||||
| 26 | + self.n2 = RMSNorm(w, eps); self.ffn = SwiGLU(w, ffn) | ||||
| 27 | + | ||||
| 28 | + def forward(self, x, ctx_mask): | ||||
| 29 | + B, T, w = x.shape | ||||
| 30 | + xn = self.n1(x) | ||||
| 31 | + q = self.q(xn).view(B, T, self.h, self.hd).transpose(1, 2) | ||||
| 32 | + k = self.k(xn).view(B, T, self.h, self.hd).transpose(1, 2) | ||||
| 33 | + v = self.v(xn).view(B, T, self.h, self.hd).transpose(1, 2) | ||||
| 34 | + a = F.scaled_dot_product_attention(q, k, v, attn_mask=ctx_mask) | ||||
| 35 | + x = x + self.o(a.transpose(1, 2).reshape(B, T, w)) | ||||
| 36 | + return x + self.ffn(self.n2(x)) | ||||
| 37 | + | ||||
| 38 | + | ||||
| 39 | +class GlobalRecurrenceModel(nn.Module): | ||||
| 40 | + """Interface-compatible with PrismeModel.forward(tokens, actions): any nonterminal action = one global | ||||
| 41 | + refinement step; STOP = no step. n_edits semantics identical.""" | ||||
| 42 | + | ||||
| 43 | + def __init__(self, cfg: PrismeConfig, ffn_w: int | None = None, heads: int | None = None): | ||||
| 44 | + super().__init__() | ||||
| 45 | + self.cfg = cfg | ||||
| 46 | + self.backbone = Backbone(cfg) | ||||
| 47 | + w = cfg.w | ||||
| 48 | + self.bank_norm = RMSNorm(cfg.d_model, cfg.norm_eps) | ||||
| 49 | + self.down = nn.Linear(cfg.d_model, w, bias=False) | ||||
| 50 | + self.block = RecurrentBlock(w, heads or cfg.n_editor_heads, ffn_w or cfg.ffn_w, cfg.norm_eps) | ||||
| 51 | + self.ctx_proj = nn.Linear(2, w, bias=True) | ||||
| 52 | + self.up = nn.Linear(w, cfg.d_model, bias=False) | ||||
| 53 | + nn.init.zeros_(self.up.weight); nn.init.zeros_(self.ctx_proj.weight); nn.init.zeros_(self.ctx_proj.bias) | ||||
| 54 | + self.controller = nn.Identity() # placeholder so parameter-group helpers work | ||||
| 55 | + | ||||
| 56 | + def forward(self, tokens, actions=None, return_state=False): | ||||
| 57 | + cfg = self.cfg | ||||
| 58 | + H = self.backbone(tokens) | ||||
| 59 | + if actions is None or actions.shape[-1] == 0: | ||||
| 60 | + return {"logits": self.backbone.head(H), "H": H, "n_edits": None} | ||||
| 61 | + B, T, R = actions.shape | ||||
| 62 | + x0 = self.down(self.bank_norm(H)) | ||||
| 63 | + x = x0 | ||||
| 64 | + mask = torch.ones(T, T, dtype=torch.bool, device=H.device).tril().view(1, 1, T, T) | ||||
| 65 | + stopped = torch.zeros(B, T, dtype=torch.bool, device=H.device) | ||||
| 66 | + n_edits = torch.zeros(B, T, dtype=torch.long, device=H.device) | ||||
| 67 | + for r in range(R): | ||||
| 68 | + a = torch.where(stopped, torch.full_like(actions[:, :, r], cfg.stop_action), actions[:, :, r]) | ||||
| 69 | + act = (a != cfg.stop_action) | ||||
| 70 | + stopped |= ~act | ||||
| 71 | + ctx = torch.stack([torch.full((B, T), r / cfg.r_max, device=H.device), torch.full((B, T), (R - r - 1) / cfg.r_max, device=H.device)], -1) | ||||
| 72 | + xn = self.block(x + self.ctx_proj(ctx), mask) | ||||
| 73 | + x = torch.where(act.unsqueeze(-1), xn, x) # positions that stopped keep their state | ||||
| 74 | + n_edits += act.long() | ||||
| 75 | + delta = self.up(x - x0) | ||||
| 76 | + edited = n_edits > 0 | ||||
| 77 | + delta = torch.where(edited.unsqueeze(-1), delta, torch.zeros_like(delta)) | ||||
| 78 | + return {"logits": self.backbone.head(H + delta), "H": H, "n_edits": n_edits, "delta_out": delta} | ||||
added
baselines/routers.py
+54 −0
@@ -0,0 +1,54 @@ | |||||
| 1 | +"""Reference routing policies that share PRISME's editor (actions chosen without the learned value controller). | ||||
| 2 | +All are target-free and deployable; they differ only in how the action is picked at each step. | ||||
| 3 | + random uniform over the 24 nonterminal actions, fixed budget b | ||||
| 4 | + uniform round-robin single-slot schedule SINGLE_0, SINGLE_1, ... (deterministic, fixed budget b) | ||||
| 5 | + fixed_pair PAIR_0_1 repeated (fixed budget b) | ||||
| 6 | + entropy continue (random action) while the current predictive entropy exceeds a threshold, else STOP | ||||
| 7 | + norm continue while the last write moved the slot by more than a threshold (RMS), else STOP | ||||
| 8 | +""" | ||||
| 9 | +from __future__ import annotations | ||||
| 10 | + | ||||
| 11 | +import torch | ||||
| 12 | + | ||||
| 13 | +from prisme.model import PrismeModel | ||||
| 14 | + | ||||
| 15 | + | ||||
| 16 | +@torch.no_grad() | ||||
| 17 | +def rollout(model: PrismeModel, tokens, policy: str, budget: int, threshold: float = 0.0, seed: int = 0): | ||||
| 18 | + cfg = model.cfg | ||||
| 19 | + ctx = model.encode(tokens) | ||||
| 20 | + S = ctx.S0 | ||||
| 21 | + B, T = tokens.shape | ||||
| 22 | + g = torch.Generator(device="cpu").manual_seed(seed) | ||||
| 23 | + stopped = torch.zeros(B, T, dtype=torch.bool, device=tokens.device) | ||||
| 24 | + n_edits = torch.zeros(B, T, dtype=torch.long, device=tokens.device) | ||||
| 25 | + trace = torch.full((B, T, budget), cfg.stop_action, dtype=torch.long, device=tokens.device) | ||||
| 26 | + last_rms = torch.full((B, T), float("inf"), device=tokens.device) | ||||
| 27 | + for r in range(budget): | ||||
| 28 | + if policy == "random": | ||||
| 29 | + a = torch.randint(0, cfg.n_actions, (B, T), generator=g).to(tokens.device) | ||||
| 30 | + elif policy == "uniform": | ||||
| 31 | + a = torch.full((B, T), r % cfg.n_slots, device=tokens.device) | ||||
| 32 | + elif policy == "fixed_pair": | ||||
| 33 | + a = torch.full((B, T), cfg.n_slots + (r % cfg.n_pairs), device=tokens.device) | ||||
| 34 | + elif policy == "entropy": | ||||
| 35 | + logits, _ = model.logits_from_state(S, ctx, n_edits > 0) | ||||
| 36 | + p = torch.softmax(logits.float(), -1) | ||||
| 37 | + ent = -(p * torch.log(p.clamp_min(1e-12))).sum(-1) | ||||
| 38 | + a = torch.randint(0, cfg.n_actions, (B, T), generator=g).to(tokens.device) | ||||
| 39 | + a = torch.where(ent > threshold, a, torch.full_like(a, cfg.stop_action)) | ||||
| 40 | + elif policy == "norm": | ||||
| 41 | + a = torch.randint(0, cfg.n_actions, (B, T), generator=g).to(tokens.device) | ||||
| 42 | + a = torch.where(last_rms > threshold, a, torch.full_like(a, cfg.stop_action)) | ||||
| 43 | + else: | ||||
| 44 | + raise ValueError(policy) | ||||
| 45 | + a = torch.where(stopped, torch.full_like(a, cfg.stop_action), a) | ||||
| 46 | + stopped |= a == cfg.stop_action | ||||
| 47 | + trace[:, :, r] = a | ||||
| 48 | + S_new, info = model.apply_action(S, ctx, a, r, budget - r - 1) | ||||
| 49 | + moved = (info["eta"].squeeze(-1) * info["delta_rms"] * info["scale"]) # [B,T,k] realised rms step per lane | ||||
| 50 | + last_rms = torch.where(a != cfg.stop_action, moved.max(-1).values, last_rms) | ||||
| 51 | + S = S_new | ||||
| 52 | + n_edits += (a != cfg.stop_action).long() | ||||
| 53 | + logits, _ = model.logits_from_state(S, ctx, n_edits > 0) | ||||
| 54 | + return {"logits": logits, "actions": trace, "n_edits": n_edits} | ||||
added
configs/baselines/transformer_enlarged_102m.yaml
+23 −0
@@ -0,0 +1,23 @@ | |||||
| 1 | +# PRISME reference configuration: prisme_103m (see docs/architecture_spec.md) | ||||
| 2 | +model: | ||||
| 3 | + name: transformer_enlarged_102m | ||||
| 4 | + vocab_size: 32768 | ||||
| 5 | + d_model: 768 | ||||
| 6 | + n_layers: 12 | ||||
| 7 | + n_heads: 12 | ||||
| 8 | + n_kv_heads: 3 | ||||
| 9 | + ffn_width: 2144 # parameter-matched plain decoder: +2.65M params ~= PRISME workspace+controller (2.63M) | ||||
| 10 | + max_seq_len: 4096 | ||||
| 11 | + rope_theta: 10000.0 | ||||
| 12 | + norm_eps: 1.0e-5 | ||||
| 13 | + tie_embeddings: true | ||||
| 14 | + use_workspace: false | ||||
| 15 | + w: 256 | ||||
| 16 | + ffn_w: 768 | ||||
| 17 | + n_slots: 16 | ||||
| 18 | + n_editor_blocks: 2 | ||||
| 19 | + n_editor_heads: 4 | ||||
| 20 | + r_max: 8 | ||||
| 21 | + eta_max: 0.25 | ||||
| 22 | + clip_c: 1.0 | ||||
| 23 | + readout_zero_init: true | ||||
added
configs/experiments/controller_p4.yaml
+14 −0
@@ -0,0 +1,14 @@ | |||||
| 1 | +# Phase 6 (tiny scale): value controller fit on logged labels; (lambda, kappa) tuned on calibration rollouts. | ||||
| 2 | +name: controller_p4 | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: prisme | ||||
| 5 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +checkpoint: CHECKPOINT_DIR | ||||
| 8 | +labels: {train: LABELS_TRAIN, calib: LABELS_CALIB} | ||||
| 9 | +train: {steps: 3000, lr: 1.0e-3, weight_decay: 0.01, alpha: 1.0, beta: 0.5, gamma: 0.1, huber_delta: 0.1, log_every: 100, eval_every: 1000, seq_len: 320} | ||||
| 10 | +lambda_grid: [0.0, 0.01, 0.03, 0.1, 0.3] | ||||
| 11 | +kappa_grid: [0.0, 0.5] | ||||
| 12 | +sweep_batches: 10 | ||||
| 13 | +max_budget: 8 | ||||
| 14 | +selection_rule: max_loss_reduction_per_ops | ||||
added
configs/experiments/labels_p4.yaml
+13 −0
@@ -0,0 +1,13 @@ | |||||
| 1 | +# Phase 5 (tiny scale): counterfactual label collection with the fixed Phase 4 editor checkpoint. | ||||
| 2 | +name: labels_p4 | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: prisme | ||||
| 5 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +checkpoint: CHECKPOINT_DIR | ||||
| 8 | +use_policy: false | ||||
| 9 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: answer} | ||||
| 10 | +collect: {batch_size: 32, seq_len: 320, max_prefix: 3, audit_every: 4} | ||||
| 11 | +partitions: | ||||
| 12 | + train: {split: train, n_batches: 120, stream_seed: 11} | ||||
| 13 | + calib: {split: calib, n_batches: 30, stream_seed: 12} | ||||
added
configs/experiments/p4_editor_bootstrap.yaml
+16 −0
@@ -0,0 +1,16 @@ | |||||
| 1 | +# Phase 4 step 2: Stage B editor bootstrap on the FROZEN tiny backbone (v1), forced mixed budgets {0,1,2,4,8}. | ||||
| 2 | +name: p4_editor_v1_scaled | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: prisme | ||||
| 5 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +init: {backbone_checkpoint: experiments/runs/p4_tiny_backbone_seed01_20261029232626/checkpoint, freeze_backbone: true, auto_readout_scale: true} | ||||
| 8 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: answer, | ||||
| 9 | + eval_splits: [dev_iid, dev_template, dev_depth, dev_compose]} | ||||
| 10 | +budgets: [0, 1, 2, 4, 8] | ||||
| 11 | +budget_probs: [0.2, 0.2, 0.2, 0.2, 0.2] | ||||
| 12 | +budget_per: position | ||||
| 13 | +train: {batch_size: 32, seq_len: 320, steps: 3000, lr: 1.0e-3, warmup: 100, weight_decay: 0.1, grad_clip: 1.0, | ||||
| 14 | + log_every: 50, eval_every: 500, eval_batches: 8, ckpt_every: 1500} | ||||
| 15 | +em_budgets: [0, 2, 8] | ||||
| 16 | +em_max_items: 400 | ||||
added
configs/experiments/p4_editor_bootstrap_lr3e3.yaml
+16 −0
@@ -0,0 +1,16 @@ | |||||
| 1 | +# Phase 4 step 2: Stage B editor bootstrap on the FROZEN tiny backbone (v1), forced mixed budgets {0,1,2,4,8}. | ||||
| 2 | +name: p4_editor_v1_lr3e3_b64 | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: prisme | ||||
| 5 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +init: {backbone_checkpoint: experiments/runs/p4_tiny_backbone_seed01_20261029232626/checkpoint, freeze_backbone: true} | ||||
| 8 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: answer, | ||||
| 9 | + eval_splits: [dev_iid, dev_template, dev_depth, dev_compose]} | ||||
| 10 | +budgets: [0, 1, 2, 4, 8] | ||||
| 11 | +budget_probs: [0.2, 0.2, 0.2, 0.2, 0.2] | ||||
| 12 | +budget_per: position | ||||
| 13 | +train: {batch_size: 64, seq_len: 320, steps: 2000, lr: 3.0e-3, warmup: 100, weight_decay: 0.1, grad_clip: 1.0, | ||||
| 14 | + log_every: 50, eval_every: 500, eval_batches: 8, ckpt_every: 1500} | ||||
| 15 | +em_budgets: [0, 2, 8] | ||||
| 16 | +em_max_items: 400 | ||||
added
configs/experiments/p4_editor_bootstrap_nonzeroU.yaml
+17 −0
@@ -0,0 +1,17 @@ | |||||
| 1 | +# Phase 4 step 2: Stage B editor bootstrap on the FROZEN tiny backbone (v1), forced mixed budgets {0,1,2,4,8}. | ||||
| 2 | +name: p4_editor_v1_nonzeroU | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: prisme | ||||
| 5 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +init: {backbone_checkpoint: experiments/runs/p4_tiny_backbone_seed01_20261029232626/checkpoint, freeze_backbone: true} | ||||
| 8 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: answer, | ||||
| 9 | + eval_splits: [dev_iid, dev_template, dev_depth, dev_compose]} | ||||
| 10 | +budgets: [0, 1, 2, 4, 8] | ||||
| 11 | +budget_probs: [0.2, 0.2, 0.2, 0.2, 0.2] | ||||
| 12 | +budget_per: position | ||||
| 13 | +train: {batch_size: 32, seq_len: 320, steps: 2000, lr: 1.0e-3, warmup: 100, weight_decay: 0.1, grad_clip: 1.0, | ||||
| 14 | + log_every: 50, eval_every: 500, eval_batches: 8, ckpt_every: 1500} | ||||
| 15 | +em_budgets: [0, 2, 8] | ||||
| 16 | +em_max_items: 400 | ||||
| 17 | +model_overrides: {readout_zero_init: false} | ||||
added
configs/experiments/p4_tiny_backbone.yaml
+12 −0
@@ -0,0 +1,12 @@ | |||||
| 1 | +# Phase 4 step 1: tiny backbone pretrained from scratch on the synthetic TRAIN split (char-level), workspace off. | ||||
| 2 | +name: p4_tiny_backbone | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: backbone_only | ||||
| 5 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +init: {freeze_backbone: false} | ||||
| 8 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: all, | ||||
| 9 | + eval_splits: [dev_iid, dev_template, dev_depth, dev_compose]} | ||||
| 10 | +train: {batch_size: 64, seq_len: 320, steps: 6000, lr: 1.0e-3, warmup: 200, weight_decay: 0.1, grad_clip: 1.0, | ||||
| 11 | + log_every: 50, eval_every: 500, eval_batches: 10, ckpt_every: 2000} | ||||
| 12 | +em_max_items: 600 | ||||
added
configs/experiments/p4_tiny_backbone_v2.yaml
+12 −0
@@ -0,0 +1,12 @@ | |||||
| 1 | +# Phase 4 step 1 (v2): stronger tiny backbone (≈5.6M params) from scratch on the synthetic TRAIN split, char-level. | ||||
| 2 | +name: p4_tiny_backbone_v2 | ||||
| 3 | +seed: 1 | ||||
| 4 | +mode: backbone_only | ||||
| 5 | +model: {name: tiny_p4v2, vocab_size: 128, d_model: 256, n_layers: 6, n_heads: 8, n_kv_heads: 4, ffn_width: 768, | ||||
| 6 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 7 | +init: {freeze_backbone: false} | ||||
| 8 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: all, | ||||
| 9 | + eval_splits: [dev_iid, dev_template, dev_depth, dev_compose]} | ||||
| 10 | +train: {batch_size: 64, seq_len: 320, steps: 15000, lr: 1.0e-3, warmup: 300, weight_decay: 0.1, grad_clip: 1.0, | ||||
| 11 | + log_every: 100, eval_every: 1000, eval_batches: 10, ckpt_every: 5000} | ||||
| 12 | +em_max_items: 800 | ||||
added
configs/experiments/pretrain_103m.yaml
+18 −0
@@ -0,0 +1,18 @@ | |||||
| 1 | +name: pretrain103m | ||||
| 2 | +seed: 1 | ||||
| 3 | +model: configs/prisme_103m.yaml | ||||
| 4 | +data: | ||||
| 5 | + train_bin: data/processed/fineweb_edu_300m/train.bin | ||||
| 6 | + val_bin: data/processed/fineweb_edu_300m/val.bin | ||||
| 7 | +train: | ||||
| 8 | + batch_size: 32 | ||||
| 9 | + seq_len: 512 | ||||
| 10 | + steps: 12000 # 32*512*12000 = 196.6M tokens (pilot budget; see protocol/preregistration.md) | ||||
| 11 | + lr: 6.0e-4 | ||||
| 12 | + warmup: 300 | ||||
| 13 | + weight_decay: 0.1 | ||||
| 14 | + grad_clip: 1.0 | ||||
| 15 | + log_every: 20 | ||||
| 16 | + eval_every: 500 | ||||
| 17 | + eval_batches: 20 | ||||
| 18 | + ckpt_every: 1000 | ||||
added
configs/experiments/pretrain_enlarged_102m.yaml
+18 −0
@@ -0,0 +1,18 @@ | |||||
| 1 | +name: pretrain_enlarged102m | ||||
| 2 | +seed: 1 | ||||
| 3 | +model: configs/baselines/transformer_enlarged_102m.yaml | ||||
| 4 | +data: | ||||
| 5 | + train_bin: data/processed/fineweb_edu_300m/train.bin | ||||
| 6 | + val_bin: data/processed/fineweb_edu_300m/val.bin | ||||
| 7 | +train: | ||||
| 8 | + batch_size: 32 | ||||
| 9 | + seq_len: 512 | ||||
| 10 | + steps: 12000 # 32*512*12000 = 196.6M tokens (pilot budget; see protocol/preregistration.md) | ||||
| 11 | + lr: 6.0e-4 | ||||
| 12 | + warmup: 300 | ||||
| 13 | + weight_decay: 0.1 | ||||
| 14 | + grad_clip: 1.0 | ||||
| 15 | + log_every: 20 | ||||
| 16 | + eval_every: 500 | ||||
| 17 | + eval_batches: 20 | ||||
| 18 | + ckpt_every: 1000 | ||||
added
configs/experiments/routing_p4.yaml
+13 −0
@@ -0,0 +1,13 @@ | |||||
| 1 | +# Routing comparison (tiny scale) on dev splits with the LOCKED controller checkpoint. | ||||
| 2 | +seed: 1 | ||||
| 3 | +mode: prisme | ||||
| 4 | +model: {name: tiny_p4, vocab_size: 128, d_model: 192, n_layers: 4, n_heads: 6, n_kv_heads: 2, ffn_width: 512, | ||||
| 5 | + max_seq_len: 320, w: 64, ffn_w: 192, n_slots: 16, n_editor_heads: 2, r_max: 8} | ||||
| 6 | +checkpoint: CHECKPOINT_DIR | ||||
| 7 | +data: {kind: synthetic, tokenizer: char, splits_dir: data/splits/synthetic_v1, loss_span: answer, split: dev_iid} | ||||
| 8 | +batch_size: 32 | ||||
| 9 | +seq_len: 320 | ||||
| 10 | +n_batches: 20 | ||||
| 11 | +budgets: [1, 2, 4, 8] | ||||
| 12 | +oracle_batches: 4 | ||||
| 13 | +two_step_batches: 1 | ||||
added
configs/prisme_103m.yaml
+23 −0
@@ -0,0 +1,23 @@ | |||||
| 1 | +# PRISME reference configuration: prisme_103m (see docs/architecture_spec.md) | ||||
| 2 | +model: | ||||
| 3 | + name: prisme_103m | ||||
| 4 | + vocab_size: 32768 | ||||
| 5 | + d_model: 768 | ||||
| 6 | + n_layers: 12 | ||||
| 7 | + n_heads: 12 | ||||
| 8 | + n_kv_heads: 3 | ||||
| 9 | + ffn_width: 2048 | ||||
| 10 | + max_seq_len: 4096 | ||||
| 11 | + rope_theta: 10000.0 | ||||
| 12 | + norm_eps: 1.0e-5 | ||||
| 13 | + tie_embeddings: true | ||||
| 14 | + use_workspace: true | ||||
| 15 | + w: 256 | ||||
| 16 | + ffn_w: 768 | ||||
| 17 | + n_slots: 16 | ||||
| 18 | + n_editor_blocks: 2 | ||||
| 19 | + n_editor_heads: 4 | ||||
| 20 | + r_max: 8 | ||||
| 21 | + eta_max: 0.25 | ||||
| 22 | + clip_c: 1.0 | ||||
| 23 | + readout_zero_init: true | ||||
added
configs/prisme_1180m.yaml
+23 −0
@@ -0,0 +1,23 @@ | |||||
| 1 | +# PRISME reference configuration: prisme_1180m (see docs/architecture_spec.md) | ||||
| 2 | +model: | ||||
| 3 | + name: prisme_1180m | ||||
| 4 | + vocab_size: 32768 | ||||
| 5 | + d_model: 2048 | ||||
| 6 | + n_layers: 24 | ||||
| 7 | + n_heads: 32 | ||||
| 8 | + n_kv_heads: 8 | ||||
| 9 | + ffn_width: 5632 | ||||
| 10 | + max_seq_len: 4096 | ||||
| 11 | + rope_theta: 10000.0 | ||||
| 12 | + norm_eps: 1.0e-5 | ||||
| 13 | + tie_embeddings: true | ||||
| 14 | + use_workspace: true | ||||
| 15 | + w: 768 | ||||
| 16 | + ffn_w: 2048 | ||||
| 17 | + n_slots: 16 | ||||
| 18 | + n_editor_blocks: 2 | ||||
| 19 | + n_editor_heads: 12 | ||||
| 20 | + r_max: 8 | ||||
| 21 | + eta_max: 0.25 | ||||
| 22 | + clip_c: 1.0 | ||||
| 23 | + readout_zero_init: true | ||||
added
configs/prisme_459m.yaml
+23 −0
@@ -0,0 +1,23 @@ | |||||
| 1 | +# PRISME reference configuration: prisme_459m (see docs/architecture_spec.md) | ||||
| 2 | +model: | ||||
| 3 | + name: prisme_459m | ||||
| 4 | + vocab_size: 32768 | ||||
| 5 | + d_model: 1536 | ||||
| 6 | + n_layers: 16 | ||||
| 7 | + n_heads: 24 | ||||
| 8 | + n_kv_heads: 6 | ||||
| 9 | + ffn_width: 4096 | ||||
| 10 | + max_seq_len: 4096 | ||||
| 11 | + rope_theta: 10000.0 | ||||
| 12 | + norm_eps: 1.0e-5 | ||||
| 13 | + tie_embeddings: true | ||||
| 14 | + use_workspace: true | ||||
| 15 | + w: 512 | ||||
| 16 | + ffn_w: 1536 | ||||
| 17 | + n_slots: 16 | ||||
| 18 | + n_editor_blocks: 2 | ||||
| 19 | + n_editor_heads: 8 | ||||
| 20 | + r_max: 8 | ||||
| 21 | + eta_max: 0.25 | ||||
| 22 | + clip_c: 1.0 | ||||
| 23 | + readout_zero_init: true | ||||
added
data/__init__.py
+0 −0
added
data/char_tokenizer.py
+29 −0
@@ -0,0 +1,29 @@ | |||||
| 1 | +"""Self-contained character-level tokenizer for the Phase 4 learnability experiments (no corpus dependency).""" | ||||
| 2 | +from __future__ import annotations | ||||
| 3 | + | ||||
| 4 | +import json | ||||
| 5 | +from pathlib import Path | ||||
| 6 | + | ||||
| 7 | +PRINTABLE = [chr(i) for i in range(32, 127)] | ||||
| 8 | + | ||||
| 9 | + | ||||
| 10 | +class CharTokenizer: | ||||
| 11 | + def __init__(self): | ||||
| 12 | + self.specials = ["<eot>", "<pad>"] | ||||
| 13 | + self.itos = self.specials + PRINTABLE | ||||
| 14 | + self.stoi = {s: i for i, s in enumerate(self.itos)} | ||||
| 15 | + self.eot_id = 0 | ||||
| 16 | + self.pad_id = 1 | ||||
| 17 | + | ||||
| 18 | + @property | ||||
| 19 | + def vocab_size(self) -> int: | ||||
| 20 | + return len(self.itos) | ||||
| 21 | + | ||||
| 22 | + def encode(self, text: str) -> list[int]: | ||||
| 23 | + return [self.stoi.get(ch, self.stoi["?"]) for ch in text] | ||||
| 24 | + | ||||
| 25 | + def decode(self, ids) -> str: | ||||
| 26 | + return "".join(self.itos[i] if i >= len(self.specials) else "" for i in ids) | ||||
| 27 | + | ||||
| 28 | + def save(self, path): | ||||
| 29 | + Path(path).write_text(json.dumps({"itos": self.itos})) | ||||
added
data/generators/__init__.py
+0 −0
added
data/generators/synthetic.py
+236 −0
@@ -0,0 +1,236 @@ | |||||
| 1 | +"""Verifiable synthetic task generators with programmatic ground truth. | ||||
| 2 | + | ||||
| 3 | +Families (each item is a dict: family, template, depth, prompt, answer, text, meta): | ||||
| 4 | + arith multi-step arithmetic chains (depth = number of operations) | ||||
| 5 | + bind variable binding + dependent substitution (depth = chain length) | ||||
| 6 | + fsm register / finite-state updates (depth = number of updates) | ||||
| 7 | + excl conjunctive constraints with distractors (depth = number of constraints) | ||||
| 8 | + sched rule-based selection under a budget and exclusions (depth = number of candidates) | ||||
| 9 | + compose held-out composition: bind chain feeding an arithmetic expression (depth = total steps) | ||||
| 10 | + | ||||
| 11 | +Splitting is done BEFORE sampling: by (family, template id, depth regime, seed stream). Train uses templates | ||||
| 12 | +T0-T2 and depths <= max_train_depth; held-out "template" items use templates T3-T4; held-out "depth" items use | ||||
| 13 | +depths > max_train_depth; the compose family is entirely held out from training unless explicitly requested. | ||||
| 14 | +Text format: "<prompt> Answer: <answer>" ; the answer is the only supervised span for exact-match evaluation. | ||||
| 15 | +""" | ||||
| 16 | +from __future__ import annotations | ||||
| 17 | + | ||||
| 18 | +import hashlib | ||||
| 19 | +import json | ||||
| 20 | +import random | ||||
| 21 | +from dataclasses import dataclass, asdict | ||||
| 22 | +from typing import Callable | ||||
| 23 | + | ||||
| 24 | +OPS = {"+": lambda a, b: a + b, "-": lambda a, b: a - b, "*": lambda a, b: a * b} | ||||
| 25 | + | ||||
| 26 | +# Phrasing templates: index 0..2 = train, 3..4 = held-out paraphrases. | ||||
| 27 | +TEMPLATES = { | ||||
| 28 | + "arith": ["Compute {expr}.", "What is {expr}?", "Evaluate the expression {expr}.", "Calculate {expr} step by step and give the result.", "Find the value of {expr}."], | ||||
| 29 | + "bind": ["Let {chain}. What is {var}?", "Given {chain}, compute {var}.", "{chain}. Find {var}.", "Suppose {chain}. Then {var} equals what?", "With {chain}, the value of {var} is?"], | ||||
| 30 | + "fsm": ["Start with register {init}. {ops}. What is the register?", "The counter begins at {init}. {ops}. Final counter?", "Initial value {init}. {ops}. Result?", "A register holds {init}. Apply: {ops}. What does it hold now?", "Begin at {init}; {ops}; final value?"], | ||||
| 31 | + "excl": ["Items: {items}. Keep the items that are {rules}. Which items remain?", "Consider {items}. Select those that are {rules}. List them.", "{items}. Filter: {rules}. Remaining items?", "Given the list {items}, keep only the ones {rules}. Answer with the names.", "From {items}, which satisfy: {rules}?"], | ||||
| 32 | + "sched": ["Budget {budget}. Tasks: {tasks}. Rule: {rule}. Choose the affordable task with the highest value.", "You have {budget} units. Options: {tasks}. Constraint: {rule}. Which affordable option has the largest value?", "Capacity {budget}. Candidates: {tasks}. Exclusion: {rule}. Pick the best affordable candidate.", "With a budget of {budget}, among {tasks}, under the rule {rule}, which task maximises value while staying affordable?", "Budget is {budget}. {tasks}. {rule}. Best affordable task by value?"], | ||||
| 33 | + "compose": ["Let {chain}. Compute {expr}.", "Given {chain}, what is {expr}?", "{chain}. Evaluate {expr}.", "Suppose {chain}. Then find {expr}.", "With {chain}, calculate {expr}."], | ||||
| 34 | +} | ||||
| 35 | + | ||||
| 36 | + | ||||
| 37 | +def _rng(family: str, template: int, depth: int, seed: int, idx: int) -> random.Random: | ||||
| 38 | + key = f"{family}|{template}|{depth}|{seed}|{idx}" | ||||
| 39 | + return random.Random(int(hashlib.sha256(key.encode()).hexdigest()[:16], 16)) | ||||
| 40 | + | ||||
| 41 | + | ||||
| 42 | +def gen_arith(r: random.Random, depth: int, template: int): | ||||
| 43 | + vals = [r.randint(1, 20)] | ||||
| 44 | + ops = [] | ||||
| 45 | + expr = str(vals[0]) | ||||
| 46 | + cur = vals[0] | ||||
| 47 | + for _ in range(depth): | ||||
| 48 | + op = r.choice(["+", "-", "*"]) | ||||
| 49 | + b = r.randint(1, 9) if op == "*" else r.randint(1, 30) | ||||
| 50 | + cur = OPS[op](cur, b) # left-to-right evaluation (stated below) | ||||
| 51 | + expr += f" {op} {b}" | ||||
| 52 | + ops.append(op) | ||||
| 53 | + prompt = TEMPLATES["arith"][template].format(expr=expr) + " Evaluate left to right." | ||||
| 54 | + return prompt, str(cur), {"ops": "".join(ops)} | ||||
| 55 | + | ||||
| 56 | + | ||||
| 57 | +def gen_bind(r: random.Random, depth: int, template: int): | ||||
| 58 | + names = r.sample(list("abcdefghjkmnpqrstuvwxyz"), depth + 1) | ||||
| 59 | + vals = {names[0]: r.randint(1, 15)} | ||||
| 60 | + parts = [f"{names[0]} = {vals[names[0]]}"] | ||||
| 61 | + for i in range(1, depth + 1): | ||||
| 62 | + op = r.choice(["+", "-", "*"]) | ||||
| 63 | + b = r.randint(1, 5) if op == "*" else r.randint(1, 12) | ||||
| 64 | + src = names[i - 1] | ||||
| 65 | + vals[names[i]] = OPS[op](vals[src], b) | ||||
| 66 | + parts.append(f"{names[i]} = {src} {op} {b}") | ||||
| 67 | + # distractor: an unused variable | ||||
| 68 | + d = r.choice([n for n in "ABCDEFG"]) | ||||
| 69 | + parts.insert(r.randint(1, len(parts)), f"{d} = {r.randint(1, 40)}") | ||||
| 70 | + chain = ", ".join(parts) | ||||
| 71 | + target = names[depth] | ||||
| 72 | + prompt = TEMPLATES["bind"][template].format(chain=chain, var=target) | ||||
| 73 | + return prompt, str(vals[target]), {"target": target} | ||||
| 74 | + | ||||
| 75 | + | ||||
| 76 | +def gen_fsm(r: random.Random, depth: int, template: int): | ||||
| 77 | + init = r.randint(0, 10) | ||||
| 78 | + cur = init | ||||
| 79 | + steps = [] | ||||
| 80 | + for _ in range(depth): | ||||
| 81 | + op = r.choice(["add", "sub", "double", "reset", "halve"]) | ||||
| 82 | + if op == "add": | ||||
| 83 | + b = r.randint(1, 9); cur += b; steps.append(f"add {b}") | ||||
| 84 | + elif op == "sub": | ||||
| 85 | + b = r.randint(1, 9); cur -= b; steps.append(f"subtract {b}") | ||||
| 86 | + elif op == "double": | ||||
| 87 | + cur *= 2; steps.append("double it") | ||||
| 88 | + elif op == "halve": | ||||
| 89 | + cur = cur // 2; steps.append("halve it (round down)") | ||||
| 90 | + else: | ||||
| 91 | + b = r.randint(0, 9); cur = b; steps.append(f"reset to {b}") | ||||
| 92 | + prompt = TEMPLATES["fsm"][template].format(init=init, ops=", then ".join(steps)) | ||||
| 93 | + return prompt, str(cur), {} | ||||
| 94 | + | ||||
| 95 | + | ||||
| 96 | +def gen_excl(r: random.Random, depth: int, template: int): | ||||
| 97 | + n = r.randint(4, 7) | ||||
| 98 | + names = r.sample(["apple", "pear", "plum", "fig", "kiwi", "lime", "date", "yam", "nut", "bean"], n) | ||||
| 99 | + items = {nm: (r.randint(1, 30), r.choice(["red", "green", "blue"])) for nm in names} | ||||
| 100 | + rules = [] | ||||
| 101 | + def keep(v, c): | ||||
| 102 | + return True | ||||
| 103 | + preds: list[Callable] = [] | ||||
| 104 | + for _ in range(depth): | ||||
| 105 | + kind = r.choice(["gt", "lt", "color", "even"]) | ||||
| 106 | + if kind == "gt": | ||||
| 107 | + t = r.randint(5, 25); rules.append(f"greater than {t}"); preds.append(lambda v, c, t=t: v > t) | ||||
| 108 | + elif kind == "lt": | ||||
| 109 | + t = r.randint(5, 25); rules.append(f"smaller than {t}"); preds.append(lambda v, c, t=t: v < t) | ||||
| 110 | + elif kind == "color": | ||||
| 111 | + col = r.choice(["red", "green", "blue"]); rules.append(f"not {col}"); preds.append(lambda v, c, col=col: c != col) | ||||
| 112 | + else: | ||||
| 113 | + rules.append("even"); preds.append(lambda v, c: v % 2 == 0) | ||||
| 114 | + remaining = [nm for nm in names if all(p(*items[nm]) for p in preds)] | ||||
| 115 | + items_s = ", ".join(f"{nm} ({v}, {c})" for nm, (v, c) in items.items()) | ||||
| 116 | + prompt = TEMPLATES["excl"][template].format(items=items_s, rules=" and ".join(rules)) | ||||
| 117 | + ans = " ".join(remaining) if remaining else "none" | ||||
| 118 | + return prompt, ans, {"n_items": n, "n_remaining": len(remaining)} | ||||
| 119 | + | ||||
| 120 | + | ||||
| 121 | +def gen_sched(r: random.Random, depth: int, template: int): | ||||
| 122 | + names = [chr(ord("A") + i) for i in range(depth)] | ||||
| 123 | + tasks = {nm: (r.randint(1, 12), r.randint(1, 20)) for nm in names} | ||||
| 124 | + budget = r.randint(4, 12) | ||||
| 125 | + kind = r.choice(["cost_gt", "value_lt", "none"]) | ||||
| 126 | + if kind == "cost_gt": | ||||
| 127 | + t = r.randint(3, 10); rule = f"exclude tasks with cost greater than {t}"; ok = lambda c, v: c <= t | ||||
| 128 | + elif kind == "value_lt": | ||||
| 129 | + t = r.randint(3, 10); rule = f"exclude tasks with value below {t}"; ok = lambda c, v: v >= t | ||||
| 130 | + else: | ||||
| 131 | + rule = "no exclusion"; ok = lambda c, v: True | ||||
| 132 | + feas = [(v, nm) for nm, (c, v) in tasks.items() if c <= budget and ok(c, v)] | ||||
| 133 | + if not feas: | ||||
| 134 | + ans = "none" | ||||
| 135 | + else: | ||||
| 136 | + best = max(v for v, _ in feas) | ||||
| 137 | + ans = sorted(nm for v, nm in feas if v == best)[0] | ||||
| 138 | + tasks_s = ", ".join(f"{nm} (cost {c}, value {v})" for nm, (c, v) in tasks.items()) | ||||
| 139 | + prompt = TEMPLATES["sched"][template].format(budget=budget, tasks=tasks_s, rule=rule) | ||||
| 140 | + return prompt, ans, {"n_feasible": len(feas)} | ||||
| 141 | + | ||||
| 142 | + | ||||
| 143 | +def gen_compose(r: random.Random, depth: int, template: int): | ||||
| 144 | + # bind chain of length depth-2 feeding a 2-op arithmetic expression over two chain variables | ||||
| 145 | + k = max(1, depth - 2) | ||||
| 146 | + names = r.sample(list("abcdefghjkmnpqrstuvwxyz"), k + 1) | ||||
| 147 | + vals = {names[0]: r.randint(1, 10)} | ||||
| 148 | + parts = [f"{names[0]} = {vals[names[0]]}"] | ||||
| 149 | + for i in range(1, k + 1): | ||||
| 150 | + op = r.choice(["+", "-"]); b = r.randint(1, 9); src = names[i - 1] | ||||
| 151 | + vals[names[i]] = OPS[op](vals[src], b); parts.append(f"{names[i]} = {src} {op} {b}") | ||||
| 152 | + v1, v2 = r.sample(names, 2) if len(names) > 1 else (names[0], names[0]) | ||||
| 153 | + op1, op2 = r.choice(["+", "-", "*"]), r.choice(["+", "-"]) | ||||
| 154 | + c = r.randint(1, 9) | ||||
| 155 | + val = OPS[op2](OPS[op1](vals[v1], vals[v2]), c) | ||||
| 156 | + expr = f"{v1} {op1} {v2} {op2} {c} (left to right)" | ||||
| 157 | + prompt = TEMPLATES["compose"][template].format(chain=", ".join(parts), expr=expr) | ||||
| 158 | + return prompt, str(val), {} | ||||
| 159 | + | ||||
| 160 | + | ||||
| 161 | +GENERATORS = {"arith": gen_arith, "bind": gen_bind, "fsm": gen_fsm, "excl": gen_excl, "sched": gen_sched, "compose": gen_compose} | ||||
| 162 | + | ||||
| 163 | + | ||||
| 164 | +def make_item(family: str, template: int, depth: int, seed: int, idx: int) -> dict: | ||||
| 165 | + r = _rng(family, template, depth, seed, idx) | ||||
| 166 | + prompt, answer, meta = GENERATORS[family](r, depth, template) | ||||
| 167 | + text = f"{prompt} Answer: {answer}" | ||||
| 168 | + return {"family": family, "template": template, "depth": depth, "seed": seed, "idx": idx, | ||||
| 169 | + "prompt": prompt + " Answer:", "answer": " " + answer, "text": text, "meta": meta, | ||||
| 170 | + "uid": hashlib.sha256(text.encode()).hexdigest()[:16]} | ||||
| 171 | + | ||||
| 172 | + | ||||
| 173 | +@dataclass | ||||
| 174 | +class SplitSpec: | ||||
| 175 | + name: str | ||||
| 176 | + families: tuple[str, ...] | ||||
| 177 | + templates: tuple[int, ...] | ||||
| 178 | + depths: tuple[int, ...] | ||||
| 179 | + seed: int | ||||
| 180 | + n_per_cell: int | ||||
| 181 | + | ||||
| 182 | + | ||||
| 183 | +def generate_split(spec: SplitSpec) -> list[dict]: | ||||
| 184 | + items = [] | ||||
| 185 | + for fam in spec.families: | ||||
| 186 | + for tpl in spec.templates: | ||||
| 187 | + for dep in spec.depths: | ||||
| 188 | + for i in range(spec.n_per_cell): | ||||
| 189 | + items.append(make_item(fam, tpl, dep, spec.seed, i)) | ||||
| 190 | + return items | ||||
| 191 | + | ||||
| 192 | + | ||||
| 193 | +def default_splits(n_train_per_cell: int = 400, n_eval_per_cell: int = 60) -> dict[str, SplitSpec]: | ||||
| 194 | + """Train: 5 families, templates 0-2, depths 1-4, seed 1. Dev: seed 2 same cells (IID generalisation). | ||||
| 195 | + Held-out template: templates 3-4. Held-out depth: depths 5-6. Compose: fully held-out family. | ||||
| 196 | + Final test: seed 9 across all eval regimes (LOCKED until preregistration is frozen).""" | ||||
| 197 | + fams = ("arith", "bind", "fsm", "excl", "sched") | ||||
| 198 | + return { | ||||
| 199 | + "train": SplitSpec("train", fams, (0, 1, 2), (1, 2, 3, 4), 1, n_train_per_cell), | ||||
| 200 | + "calib": SplitSpec("calib", fams, (0, 1, 2), (1, 2, 3, 4), 3, n_eval_per_cell // 2), | ||||
| 201 | + "dev_iid": SplitSpec("dev_iid", fams, (0, 1, 2), (1, 2, 3, 4), 2, n_eval_per_cell), | ||||
| 202 | + "dev_template": SplitSpec("dev_template", fams, (3, 4), (1, 2, 3, 4), 2, n_eval_per_cell), | ||||
| 203 | + "dev_depth": SplitSpec("dev_depth", fams, (0, 1, 2), (5, 6), 2, n_eval_per_cell), | ||||
| 204 | + "dev_compose": SplitSpec("dev_compose", ("compose",), (0, 1, 2), (3, 4, 5), 2, n_eval_per_cell), | ||||
| 205 | + "test_iid": SplitSpec("test_iid", fams, (0, 1, 2), (1, 2, 3, 4), 9, n_eval_per_cell), | ||||
| 206 | + "test_template": SplitSpec("test_template", fams, (3, 4), (1, 2, 3, 4), 9, n_eval_per_cell), | ||||
| 207 | + "test_depth": SplitSpec("test_depth", fams, (0, 1, 2), (5, 6), 9, n_eval_per_cell), | ||||
| 208 | + "test_compose": SplitSpec("test_compose", ("compose",), (0, 1, 2), (3, 4, 5), 9, n_eval_per_cell), | ||||
| 209 | + } | ||||
| 210 | + | ||||
| 211 | + | ||||
| 212 | +def write_splits(out_dir: str, n_train_per_cell: int = 400, n_eval_per_cell: int = 60) -> dict: | ||||
| 213 | + from pathlib import Path | ||||
| 214 | + out = Path(out_dir); out.mkdir(parents=True, exist_ok=True) | ||||
| 215 | + manifest = {} | ||||
| 216 | + all_uids: dict[str, str] = {} | ||||
| 217 | + for name, spec in default_splits(n_train_per_cell, n_eval_per_cell).items(): | ||||
| 218 | + items = generate_split(spec) | ||||
| 219 | + # cross-split exact-duplicate removal: an item whose full text already appeared in an earlier split | ||||
| 220 | + # (train is written first) is DROPPED from the later split; within-split duplicates are dropped too. | ||||
| 221 | + kept, dups = [], 0 | ||||
| 222 | + for it in items: | ||||
| 223 | + if it["uid"] in all_uids: | ||||
| 224 | + dups += 1 | ||||
| 225 | + continue | ||||
| 226 | + all_uids[it["uid"]] = name | ||||
| 227 | + kept.append(it) | ||||
| 228 | + items = kept | ||||
| 229 | + p = out / f"{name}.jsonl" | ||||
| 230 | + with open(p, "w") as f: | ||||
| 231 | + for it in items: | ||||
| 232 | + f.write(json.dumps(it) + "\n") | ||||
| 233 | + manifest[name] = {"spec": asdict(spec), "n": len(items), "duplicates_dropped": dups, | ||||
| 234 | + "sha256": hashlib.sha256(p.read_bytes()).hexdigest(), "locked": name.startswith("test")} | ||||
| 235 | + (out / "manifest.json").write_text(json.dumps(manifest, indent=2)) | ||||
| 236 | + return manifest | ||||
added
data/processed/fineweb_edu_300m/manifest.json
+48 −0
@@ -0,0 +1,48 @@ | |||||
| 1 | +{ | ||||
| 2 | + "source": "HuggingFaceFW/fineweb-edu sample-10BT (streaming, canonical shard order)", | ||||
| 3 | + "license": "ODC-By 1.0 (https://opendatacommons.org/licenses/by/1-0/)", | ||||
| 4 | + "prepared_at": "2026-10-29T23:19:53-0400", | ||||
| 5 | + "args": { | ||||
| 6 | + "out": "data/processed/fineweb_edu_300m", | ||||
| 7 | + "target_train_tokens": 300000000, | ||||
| 8 | + "vocab_size": 32768, | ||||
| 9 | + "tokenizer_docs": 60000, | ||||
| 10 | + "val_pct": 0.01, | ||||
| 11 | + "test_pct": 0.01, | ||||
| 12 | + "max_doc_chars": 40000 | ||||
| 13 | + }, | ||||
| 14 | + "docs_streamed": 291647, | ||||
| 15 | + "docs": { | ||||
| 16 | + "train": 285604, | ||||
| 17 | + "val": 2957, | ||||
| 18 | + "test": 2973 | ||||
| 19 | + }, | ||||
| 20 | + "chars": { | ||||
| 21 | + "train": 1290001347, | ||||
| 22 | + "val": 12988848, | ||||
| 23 | + "test": 12933032 | ||||
| 24 | + }, | ||||
| 25 | + "splits": { | ||||
| 26 | + "train": { | ||||
| 27 | + "tokens": 289959006, | ||||
| 28 | + "docs": 285604, | ||||
| 29 | + "sha256": "e931e83c4ba6e62a2125e50a5390e8df78913dbed31af805d8af17fafad95123", | ||||
| 30 | + "locked": false | ||||
| 31 | + }, | ||||
| 32 | + "val": { | ||||
| 33 | + "tokens": 2913824, | ||||
| 34 | + "docs": 2957, | ||||
| 35 | + "sha256": "0565e3f4a4028b241f40ef07f20bc68b24e45d99e147860be066e3667d1532be", | ||||
| 36 | + "locked": false | ||||
| 37 | + }, | ||||
| 38 | + "test": { | ||||
| 39 | + "tokens": 2883436, | ||||
| 40 | + "docs": 2973, | ||||
| 41 | + "sha256": "1ce9ca3ab6d659fe08042844fc0589951e09386ee79d2ef76504ffaaebb0947b", | ||||
| 42 | + "locked": true | ||||
| 43 | + } | ||||
| 44 | + }, | ||||
| 45 | + "tokenizer_sha256": "5a78df618767945743623d5ff238d035839d02388b4c53c548a437b3d8a206ee", | ||||
| 46 | + "eot_id": 0, | ||||
| 47 | + "dtype": "uint16" | ||||
| 48 | +} | ||||
| \ No newline at end of file | |||||
added
data/processed/fineweb_edu_300m/test.bin
+0 −0
Binary file not shown.
added
data/processed/fineweb_edu_300m/tokenizer.json
+19236 −0
Large diff (19,236 lines) is not loaded by default to keep the page fast.Load diff
Diff truncated — file too large.