quantum-cortex 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cortex_c2/__init__.py +161 -0
- cortex_c2/consolidation.py +146 -0
- cortex_c2/router_ablation.py +254 -0
- cortex_c2/router_v2.py +109 -0
- cortex_c2b/__init__.py +724 -0
- cortex_c2b/companion_demo.py +203 -0
- cortex_c2b/crypto.py +102 -0
- cortex_c2b/external_arm.py +591 -0
- cortex_c2b/hm_lm.py +983 -0
- cortex_c2b/hm_protocol.py +308 -0
- cortex_c2b/lifecycle.py +454 -0
- cortex_c2b/lm_bridge.py +338 -0
- cortex_c2b/organ_use.py +254 -0
- cortex_c2b/read_path.py +218 -0
- cortex_c2b/reconstruction_probe.py +471 -0
- cortex_c2b/write_path.py +176 -0
- cortex_data/__init__.py +439 -0
- cortex_data/relay.py +174 -0
- cortex_eval/__init__.py +20 -0
- cortex_eval/ablation_mqar.py +187 -0
- cortex_eval/confirmation_run.py +133 -0
- cortex_eval/domain_map.py +127 -0
- cortex_eval/external_baseline.py +148 -0
- cortex_eval/ingest_confirmation.py +194 -0
- cortex_eval/mqar.py +122 -0
- cortex_eval/multiseed.py +148 -0
- cortex_eval/recurrent_base.py +197 -0
- cortex_eval/recurrent_ladder.py +159 -0
- cortex_eval/resumable_ladder.py +231 -0
- cortex_eval/resumable_rb.py +207 -0
- cortex_eval/run_mqar.py +127 -0
- cortex_eval/serial_position.py +116 -0
- quantum_cortex-1.0.0.dist-info/METADATA +209 -0
- quantum_cortex-1.0.0.dist-info/RECORD +41 -0
- quantum_cortex-1.0.0.dist-info/WHEEL +5 -0
- quantum_cortex-1.0.0.dist-info/licenses/AUTHORS.md +30 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE +73 -0
- quantum_cortex-1.0.0.dist-info/licenses/LICENSE-DOCS.md +34 -0
- quantum_cortex-1.0.0.dist-info/licenses/NOTICE +50 -0
- quantum_cortex-1.0.0.dist-info/top_level.txt +5 -0
- train.py +885 -0
train.py
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
"""quantum-cortex — N1 training scaffold (single-file trainer, feature F2).
|
|
2
|
+
|
|
3
|
+
Vanilla GPT-2-style control model (the ONLY legitimate baseline per hub decision 019),
|
|
4
|
+
byte-level tokenizer with reserved oracle token ids (register NOW-1),
|
|
5
|
+
device-agnostic per ADR-002 (CUDA bf16/fp16, CPU fp32 for smoke tests only),
|
|
6
|
+
checkpoint/resume (EuroHPC plan §4), and first-class metrics per ADR-001:
|
|
7
|
+
every completed run appends one schema-v1 record to metrics/runs.jsonl and
|
|
8
|
+
regenerates metrics/LATEST.md. A run without its committed record does not exist.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python train.py --config configs/smoke_cpu.json
|
|
12
|
+
python train.py --config configs/kaggle_t4.json [--resume]
|
|
13
|
+
python train.py --regen-latest
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import dataclasses
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
import os
|
|
24
|
+
import platform
|
|
25
|
+
import subprocess
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
import torch
|
|
33
|
+
import torch.nn as nn
|
|
34
|
+
import torch.nn.functional as F
|
|
35
|
+
|
|
36
|
+
REPO_ROOT = Path(__file__).resolve().parent
|
|
37
|
+
SCHEMA_PATH = REPO_ROOT / "metrics" / "schema" / "run-v1.schema.json"
|
|
38
|
+
LEDGER_PATH = REPO_ROOT / "metrics" / "runs.jsonl"
|
|
39
|
+
LATEST_PATH = REPO_ROOT / "metrics" / "LATEST.md"
|
|
40
|
+
|
|
41
|
+
# ----------------------------------------------------------------------------- config
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Config:
|
|
46
|
+
# run
|
|
47
|
+
kind: str = "control" # control | ablation | combo
|
|
48
|
+
component_under_test: str | None = None # null for control; C1..C4 otherwise
|
|
49
|
+
notes: str | None = None
|
|
50
|
+
provider: str = "other" # kaggle | colab | local | eurohpc | trc | rented | other
|
|
51
|
+
# model (vanilla GPT-2-style control: LayerNorm, GELU, learned pos emb, tied embeddings)
|
|
52
|
+
n_layer: int = 8
|
|
53
|
+
n_head: int = 8
|
|
54
|
+
n_embd: int = 512
|
|
55
|
+
block_size: int = 1024
|
|
56
|
+
vocab_bytes: int = 256
|
|
57
|
+
reserved_oracle_tokens: int = 8 # NOW-1: token space reserved from day one
|
|
58
|
+
# data
|
|
59
|
+
data_mode: str = "synthetic" # synthetic | bin
|
|
60
|
+
bin_path: str = "data/train_bytes.bin"
|
|
61
|
+
dataset_id: str = "synthetic-v0"
|
|
62
|
+
data_slice: str = "seeded pattern, 5% noise"
|
|
63
|
+
val_fraction: float = 0.01
|
|
64
|
+
# training
|
|
65
|
+
max_tokens: int = 200_000
|
|
66
|
+
batch_size: int = 8
|
|
67
|
+
lr: float = 1e-3
|
|
68
|
+
min_lr_ratio: float = 0.1
|
|
69
|
+
warmup_steps: int = 20
|
|
70
|
+
weight_decay: float = 0.1
|
|
71
|
+
grad_clip: float = 1.0
|
|
72
|
+
seed: int = 1337
|
|
73
|
+
optimizer: str = "adamw" # F1 (NorMuon-class) arrives only via its own ablation
|
|
74
|
+
c2_variant: str = "none" # none | hopfield | delta (ADR-006: C2 associative layer, default off)
|
|
75
|
+
attn_local_conv: bool = False # the ladder's L3 convolution (depthwise, kernel 3, causal) on the attention's
|
|
76
|
+
# normalised input in every block: the control plus a local convolution, a declared
|
|
77
|
+
# reference (register Rev60, 2026-09-17); default off, transparent to the hash
|
|
78
|
+
attn_local_conv_init: str = "random" # "random" (the ladder's reference, row 32) | "identity": the centre tap at one,
|
|
79
|
+
# the others and the bias at zero, so a fine tune starts as its parent exactly
|
|
80
|
+
# (v8, the reader with a local convolution); transparent to the hash at its default
|
|
81
|
+
journal_familiarity_mark: bool = False # RES-23 (v9): every read window line carries the organ's own similarity for that
|
|
82
|
+
# line, `A(0.97):<payload>`, in training and at evaluation; default off, transparent
|
|
83
|
+
c2_mem_slots: int = 64 # hopfield: number of stored key/value patterns
|
|
84
|
+
c2_heads: int = 4 # delta: number of recurrent heads
|
|
85
|
+
c2_delta_l2_keys: bool = True # delta: L2-normalise q,k before the write (KDA-style stability)
|
|
86
|
+
c2_delta_state_clip: float = 0.0 # delta: optional |S| clip (0 = off); belt-and-braces guard
|
|
87
|
+
# journal in the decode loop (ADR-008), default off -- the model starts as N1 and learns to read
|
|
88
|
+
journal: str = "none" # none | read
|
|
89
|
+
journal_block: int = 5 # the block that carries the reader (one hippocampal port)
|
|
90
|
+
journal_k: int = 4 # episodes retrieved per read
|
|
91
|
+
journal_read_bytes: int = 256 # byte budget of the read window
|
|
92
|
+
journal_cue_dim: int = 64 # the organ's cue dimension (CUE_DIM)
|
|
93
|
+
journal_heads: int = 4 # heads of the reader's cross attention
|
|
94
|
+
journal_route: str = "pinned_on" # pinned_on | pinned_off | router (the H.M. arms pin; the router is measured apart)
|
|
95
|
+
journal_asym_weight: float = 2.0 # loss weight on the decision token of an abstention target (answering instead of abstaining costs more)
|
|
96
|
+
journal_neg_frac: float = 0.25 # share of curriculum examples on never planted entities (target <UNKNOWN>); v4 lever (run ce008745375a)
|
|
97
|
+
journal_paired_negatives: bool = False # v5 lever (run 6f10e8cbfebf): negatives are minimal pairs, the same planted fact with its
|
|
98
|
+
# episode withheld from the window, so the decision must depend on the read, not on a class prior
|
|
99
|
+
journal_paired_keep_shape: bool = False # v6 lever (run db028e6a4262, INVALID): the pair keeps k lines (k + 1 retrieved, the own
|
|
100
|
+
# dropped); as run in v5 the withheld window had k - 1 lines, a shape cue the protocol never shows
|
|
101
|
+
journal_match_head: bool = False # v7 lever, RES-21 (run 81cb13a684aa, INVALID: six data recipes left the discrimination at 20 to 25
|
|
102
|
+
# points): a head supervised on "the queried entity is in the window", coupled to the decision token
|
|
103
|
+
journal_match_loss_weight: float = 1.0 # weight of the head's own loss (binary cross entropy) in the curriculum steps
|
|
104
|
+
journal_match_coupling: float = 2.0 # logit bias on the decision token: + c * m on <CITE>, - c * m on <UNKNOWN>
|
|
105
|
+
journal_forced_frac: float = 0.5 # bootstrap: share of missed retrievals where the right episode is forced into the window, decays to 0
|
|
106
|
+
journal_curriculum_ratio: float = 0.5 # share of training steps spent on organ use examples (the rest: ordinary language modelling)
|
|
107
|
+
journal_contrastive_weight: float = 1.0
|
|
108
|
+
journal_pool_facts: int = 200 # planted facts per curriculum epoch
|
|
109
|
+
journal_pool_refresh: int = 100 # steps between two replantings under the current encoder
|
|
110
|
+
journal_train_seed: int = 100_000 # the training facts' generator seed (disjoint from the frozen protocol's 0 and 10_000)
|
|
111
|
+
journal_fresh_pool: bool = False # v2 lever (run dc34fcf000aa, INVALID): a NEW set of facts at every replant, so the only way
|
|
112
|
+
# down the loss is to read the window -- a fixed pool let the model memorise entity -> attribute
|
|
113
|
+
journal_lm_window: bool = False # v2 lever: language modelling steps also carry a retrieved window (gate on), so the model
|
|
114
|
+
# learns to ignore an irrelevant read on ordinary text (the skill arm measures exactly that)
|
|
115
|
+
journal_path: str | None = None # the sealed on-disk journal of the run (key from QUANTUM_CORTEX_JOURNAL_KEY); None = a memory scope
|
|
116
|
+
journal_hm_arm: bool = True # run the frozen protocol's LM arm at the end of the run
|
|
117
|
+
journal_hm_facts: int = 200 # the protocol's sizes (200 / 50 are the frozen defaults; smaller only for a CPU smoke)
|
|
118
|
+
journal_hm_negctrl: int = 50
|
|
119
|
+
parent_run_id: str | None = None # the checkpoint this run resumes from (a fine tune is a new run)
|
|
120
|
+
parent_ckpt: str | None = None # path of that checkpoint
|
|
121
|
+
# circuit breaker (ADR-006 D7): aggressive early-abort, thresholds declared pre-run
|
|
122
|
+
cb_enabled: bool = True # NaN/Inf abort is always on; divergence check needs a baseline
|
|
123
|
+
cb_divergence_mult: float = 2.0 # abort if loss > mult × baseline-at-step over a window
|
|
124
|
+
cb_window: int = 3 # consecutive divergent evals before abort
|
|
125
|
+
cb_baseline_losses: str | None = None # optional path to control's per-eval losses (jsonl or json list)
|
|
126
|
+
# io
|
|
127
|
+
out_dir: str = "runs/smoke"
|
|
128
|
+
log_every: int = 10
|
|
129
|
+
eval_every: int = 50
|
|
130
|
+
eval_batches: int = 8
|
|
131
|
+
ckpt_every: int = 200
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def vocab_size(self) -> int:
|
|
135
|
+
return self.vocab_bytes + self.reserved_oracle_tokens
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
CONFIG_HASH_EXCLUDE = {"provider", "notes", "out_dir", "log_every", "parent_ckpt"}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def load_config(path: str) -> Config:
|
|
142
|
+
cfg = Config()
|
|
143
|
+
overrides = json.loads(Path(path).read_text())
|
|
144
|
+
valid = {f.name for f in dataclasses.fields(Config)}
|
|
145
|
+
for k, v in overrides.items():
|
|
146
|
+
if k not in valid:
|
|
147
|
+
raise SystemExit(f"unknown config key: {k!r} (typo safety — honest data)")
|
|
148
|
+
setattr(cfg, k, v)
|
|
149
|
+
return cfg
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Levers added after runs were recorded: absent from the hash while at their default, so a
|
|
153
|
+
# configuration written before the lever existed keeps its hash (run dc34fcf000aa stays
|
|
154
|
+
# 6785ba1f8e213dce, 5f0a6d3ff4e8 stays b9e10b3e0a9e5938); moved, the lever enters the hash
|
|
155
|
+
# like any other field.
|
|
156
|
+
HASH_TRANSPARENT_AT_DEFAULT = {"journal_fresh_pool": False, "journal_lm_window": False, "journal_neg_frac": 0.25,
|
|
157
|
+
"journal_paired_negatives": False, "journal_paired_keep_shape": False,
|
|
158
|
+
"journal_match_head": False, "journal_match_loss_weight": 1.0, "journal_match_coupling": 2.0,
|
|
159
|
+
"attn_local_conv": False, "attn_local_conv_init": "random", "journal_familiarity_mark": False}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def config_hash(cfg: Config) -> str:
|
|
163
|
+
d = {k: v for k, v in dataclasses.asdict(cfg).items()
|
|
164
|
+
if k not in CONFIG_HASH_EXCLUDE and not (k in HASH_TRANSPARENT_AT_DEFAULT and v == HASH_TRANSPARENT_AT_DEFAULT[k])}
|
|
165
|
+
return hashlib.sha256(json.dumps(d, sort_keys=True).encode()).hexdigest()[:16]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ----------------------------------------------------------------------------- data
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def make_synthetic(cfg: Config, n_tokens: int) -> np.ndarray:
|
|
172
|
+
"""Deterministic learnable byte stream: repeated seeded pattern + 5% noise."""
|
|
173
|
+
rng = np.random.default_rng(cfg.seed)
|
|
174
|
+
pattern = rng.integers(0, cfg.vocab_bytes, size=4096, dtype=np.uint16)
|
|
175
|
+
reps = int(np.ceil(n_tokens / pattern.size)) + 1
|
|
176
|
+
arr = np.tile(pattern, reps)[: n_tokens]
|
|
177
|
+
noise = rng.random(arr.size) < 0.05
|
|
178
|
+
arr[noise] = rng.integers(0, cfg.vocab_bytes, size=int(noise.sum()), dtype=np.uint16)
|
|
179
|
+
return arr
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_data(cfg: Config) -> tuple[np.ndarray, np.ndarray]:
|
|
183
|
+
if cfg.data_mode == "synthetic":
|
|
184
|
+
total = int(cfg.max_tokens * 1.05) + cfg.block_size + 1
|
|
185
|
+
arr = make_synthetic(cfg, total)
|
|
186
|
+
elif cfg.data_mode == "bin":
|
|
187
|
+
p = (REPO_ROOT / cfg.bin_path).resolve()
|
|
188
|
+
if not p.exists():
|
|
189
|
+
raise SystemExit(f"bin dataset missing: {p} (build it — see notebooks/n1_kaggle.ipynb)")
|
|
190
|
+
arr = np.memmap(p, dtype=np.uint16, mode="r")
|
|
191
|
+
else:
|
|
192
|
+
raise SystemExit(f"unknown data_mode: {cfg.data_mode}")
|
|
193
|
+
split = int(len(arr) * (1.0 - cfg.val_fraction))
|
|
194
|
+
return arr[:split], arr[split:]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_batch(arr: np.ndarray, cfg: Config, gen: torch.Generator, device: torch.device):
|
|
198
|
+
hi = len(arr) - cfg.block_size - 1
|
|
199
|
+
ix = torch.randint(0, hi, (cfg.batch_size,), generator=gen)
|
|
200
|
+
x = torch.stack([torch.from_numpy(np.asarray(arr[i : i + cfg.block_size], dtype=np.int64)) for i in ix])
|
|
201
|
+
y = torch.stack([torch.from_numpy(np.asarray(arr[i + 1 : i + 1 + cfg.block_size], dtype=np.int64)) for i in ix])
|
|
202
|
+
return x.to(device, non_blocking=True), y.to(device, non_blocking=True)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ----------------------------------------------------------------------------- model
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HopfieldMemory(nn.Module):
|
|
209
|
+
"""Variant H (RES-1): modern-Hopfield content-addressable retrieval.
|
|
210
|
+
A learned bank of key/value patterns read by softmax attention (energy
|
|
211
|
+
descent). Zero-initialised output projection ⇒ at init this is a no-op,
|
|
212
|
+
so a fresh model with the flag on starts identical to control and learns
|
|
213
|
+
the memory from there. Third residual sub-block; default off (ADR-006).
|
|
214
|
+
"""
|
|
215
|
+
def __init__(self, cfg: "Config"):
|
|
216
|
+
super().__init__()
|
|
217
|
+
self.ln = nn.LayerNorm(cfg.n_embd)
|
|
218
|
+
self.q = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False)
|
|
219
|
+
self.keys = nn.Parameter(torch.randn(cfg.c2_mem_slots, cfg.n_embd) * 0.02)
|
|
220
|
+
self.vals = nn.Parameter(torch.randn(cfg.c2_mem_slots, cfg.n_embd) * 0.02)
|
|
221
|
+
self.out = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False)
|
|
222
|
+
nn.init.zeros_(self.out.weight) # no-op at init
|
|
223
|
+
self.scale = cfg.n_embd ** -0.5
|
|
224
|
+
|
|
225
|
+
def forward(self, x):
|
|
226
|
+
h = self.ln(x)
|
|
227
|
+
q = self.q(h) # (b,t,c)
|
|
228
|
+
att = torch.softmax((q @ self.keys.t()) * self.scale, dim=-1) # (b,t,slots)
|
|
229
|
+
read = att @ self.vals # (b,t,c)
|
|
230
|
+
return self.out(read)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class DeltaMemory(nn.Module):
|
|
234
|
+
"""Variant D (K1, KDA-class): a per-head recurrent state S updated by an
|
|
235
|
+
error-correcting rank-one write S += beta * k (v - S^T k)^T with a
|
|
236
|
+
per-channel forget gate. The Widrow-Hoff / delta-rule family. Chunk-free
|
|
237
|
+
sequential scan (correctness-first; a parallel scan is a later slice).
|
|
238
|
+
Zero-initialised output ⇒ no-op at init. Default off (ADR-006).
|
|
239
|
+
"""
|
|
240
|
+
def __init__(self, cfg: "Config"):
|
|
241
|
+
super().__init__()
|
|
242
|
+
self.h = cfg.c2_heads
|
|
243
|
+
self.dk = cfg.n_embd // cfg.c2_heads
|
|
244
|
+
self.ln = nn.LayerNorm(cfg.n_embd)
|
|
245
|
+
self.to_qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd, bias=False)
|
|
246
|
+
self.to_beta = nn.Linear(cfg.n_embd, cfg.c2_heads, bias=True)
|
|
247
|
+
self.to_gate = nn.Linear(cfg.n_embd, cfg.c2_heads * self.dk, bias=True)
|
|
248
|
+
self.out = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False)
|
|
249
|
+
nn.init.zeros_(self.out.weight) # no-op at init
|
|
250
|
+
self.l2_keys = cfg.c2_delta_l2_keys
|
|
251
|
+
self.state_clip = cfg.c2_delta_state_clip
|
|
252
|
+
|
|
253
|
+
def forward(self, x):
|
|
254
|
+
b, t, c = x.shape
|
|
255
|
+
z = self.ln(x)
|
|
256
|
+
q, k, v = self.to_qkv(z).split(c, dim=2)
|
|
257
|
+
q = q.view(b, t, self.h, self.dk)
|
|
258
|
+
k = k.view(b, t, self.h, self.dk)
|
|
259
|
+
v = v.view(b, t, self.h, self.dk)
|
|
260
|
+
# KDA-style stability: L2-normalise q,k so the rank-one write stays bounded
|
|
261
|
+
# (unnormalised keys let |S| grow unboundedly across the scan → the v1 divergence).
|
|
262
|
+
if getattr(self, "l2_keys", True):
|
|
263
|
+
q = torch.nn.functional.normalize(q, dim=-1, eps=1e-6)
|
|
264
|
+
k = torch.nn.functional.normalize(k, dim=-1, eps=1e-6)
|
|
265
|
+
beta = torch.sigmoid(self.to_beta(z)).view(b, t, self.h, 1) # write rate
|
|
266
|
+
gate = torch.sigmoid(self.to_gate(z)).view(b, t, self.h, self.dk) # per-channel forget
|
|
267
|
+
S = torch.zeros(b, self.h, self.dk, self.dk, device=x.device, dtype=x.dtype)
|
|
268
|
+
clip = getattr(self, "state_clip", 0.0)
|
|
269
|
+
outs = []
|
|
270
|
+
for i in range(t):
|
|
271
|
+
ki = k[:, i] # (b,h,dk)
|
|
272
|
+
vi = v[:, i]
|
|
273
|
+
qi = q[:, i]
|
|
274
|
+
gi = gate[:, i]
|
|
275
|
+
bi = beta[:, i]
|
|
276
|
+
# channel-wise forget on the key dimension (columns of S)
|
|
277
|
+
S = S * gi.unsqueeze(2)
|
|
278
|
+
u = torch.einsum('bhij,bhi->bhj', S, ki) # read S^T k
|
|
279
|
+
err = vi - u # prediction error
|
|
280
|
+
S = S + bi.unsqueeze(-1) * torch.einsum('bhi,bhj->bhij', ki, err)
|
|
281
|
+
if clip > 0.0: # optional belt-and-braces guard
|
|
282
|
+
S = torch.clamp(S, -clip, clip)
|
|
283
|
+
oi = torch.einsum('bhij,bhi->bhj', S, qi) # read with query
|
|
284
|
+
outs.append(oi.reshape(b, c))
|
|
285
|
+
o = torch.stack(outs, dim=1) # (b,t,c)
|
|
286
|
+
return self.out(o)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class Block(nn.Module):
|
|
290
|
+
def __init__(self, cfg: Config, layer_idx: int = -1):
|
|
291
|
+
super().__init__()
|
|
292
|
+
self.ln1 = nn.LayerNorm(cfg.n_embd)
|
|
293
|
+
self.attn = nn.Linear(cfg.n_embd, 3 * cfg.n_embd)
|
|
294
|
+
self.proj = nn.Linear(cfg.n_embd, cfg.n_embd)
|
|
295
|
+
self.ln2 = nn.LayerNorm(cfg.n_embd)
|
|
296
|
+
self.mlp_up = nn.Linear(cfg.n_embd, 4 * cfg.n_embd)
|
|
297
|
+
self.mlp_down = nn.Linear(4 * cfg.n_embd, cfg.n_embd)
|
|
298
|
+
self.n_head = cfg.n_head
|
|
299
|
+
# the control plus a local convolution (Rev60): L3's depthwise causal conv, kernel 3, on ln1(x), the residual untouched
|
|
300
|
+
self.conv = (nn.Conv1d(cfg.n_embd, cfg.n_embd, kernel_size=3, padding=2, groups=cfg.n_embd)
|
|
301
|
+
if cfg.attn_local_conv else None)
|
|
302
|
+
if self.conv is not None and getattr(cfg, "attn_local_conv_init", "random") == "identity":
|
|
303
|
+
with torch.no_grad(): # v8: the model starts as its parent exactly
|
|
304
|
+
self.conv.weight.zero_(); self.conv.weight[:, 0, 2] = 1.0 # the centre tap (causal crop keeps taps 0..2)
|
|
305
|
+
self.conv.bias.zero_()
|
|
306
|
+
# C2 associative layer (ADR-006), default off — a third residual sub-block
|
|
307
|
+
if cfg.c2_variant == "hopfield":
|
|
308
|
+
self.c2 = HopfieldMemory(cfg)
|
|
309
|
+
elif cfg.c2_variant == "delta":
|
|
310
|
+
self.c2 = DeltaMemory(cfg)
|
|
311
|
+
else:
|
|
312
|
+
self.c2 = None
|
|
313
|
+
# the journal reader (ADR-008), one block only, default off
|
|
314
|
+
self.reader = None
|
|
315
|
+
if cfg.journal == "read" and layer_idx == cfg.journal_block:
|
|
316
|
+
from cortex_c2b.lm_bridge import JournalReader
|
|
317
|
+
self.reader = JournalReader(cfg.n_embd, cfg.journal_heads, cfg.journal_read_bytes)
|
|
318
|
+
|
|
319
|
+
def forward(self, x, window=None, window_mask=None, gate: float = 1.0):
|
|
320
|
+
b, t, c = x.shape
|
|
321
|
+
h = self.ln1(x)
|
|
322
|
+
if self.conv is not None: # Rev60: causal crop, as L3 does it
|
|
323
|
+
h = self.conv(h.transpose(1, 2))[:, :, :t].transpose(1, 2)
|
|
324
|
+
q, k, v = self.attn(h).split(c, dim=2)
|
|
325
|
+
q = q.view(b, t, self.n_head, c // self.n_head).transpose(1, 2)
|
|
326
|
+
k = k.view(b, t, self.n_head, c // self.n_head).transpose(1, 2)
|
|
327
|
+
v = v.view(b, t, self.n_head, c // self.n_head).transpose(1, 2)
|
|
328
|
+
a = F.scaled_dot_product_attention(q, k, v, is_causal=True)
|
|
329
|
+
a = a.transpose(1, 2).contiguous().view(b, t, c)
|
|
330
|
+
x = x + self.proj(a)
|
|
331
|
+
if self.c2 is not None:
|
|
332
|
+
x = x + self.c2(x) # associative memory residual (no-op at init)
|
|
333
|
+
self.last_read_mass = None
|
|
334
|
+
self.last_pre = self.last_attended = None
|
|
335
|
+
if self.reader is not None and window is not None and gate != 0.0:
|
|
336
|
+
read, mass = self.reader(x, window, window_mask)
|
|
337
|
+
self.last_pre, self.last_attended = x, self.reader.last_attended # RES-21: what the head compares
|
|
338
|
+
x = x + gate * read # the journal as content, on the router's gate (no-op at init)
|
|
339
|
+
self.last_read_mass = mass
|
|
340
|
+
x = x + self.mlp_down(F.gelu(self.mlp_up(self.ln2(x))))
|
|
341
|
+
return x
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class VanillaGPT(nn.Module):
|
|
345
|
+
def __init__(self, cfg: Config):
|
|
346
|
+
super().__init__()
|
|
347
|
+
self.cfg = cfg
|
|
348
|
+
self.wte = nn.Embedding(cfg.vocab_size, cfg.n_embd)
|
|
349
|
+
self.wpe = nn.Embedding(cfg.block_size, cfg.n_embd)
|
|
350
|
+
self.blocks = nn.ModuleList([Block(cfg, i) for i in range(cfg.n_layer)])
|
|
351
|
+
self.ln_f = nn.LayerNorm(cfg.n_embd)
|
|
352
|
+
self.cue_encoder = None
|
|
353
|
+
if cfg.journal == "read":
|
|
354
|
+
from cortex_c2b.lm_bridge import CueEncoder
|
|
355
|
+
self.cue_encoder = CueEncoder(cfg.n_embd, cfg.journal_cue_dim)
|
|
356
|
+
self.match_head = None
|
|
357
|
+
if cfg.journal == "read" and cfg.journal_match_head:
|
|
358
|
+
from cortex_c2b.lm_bridge import MatchHead
|
|
359
|
+
self.match_head = MatchHead(cfg.n_embd)
|
|
360
|
+
self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)
|
|
361
|
+
self.head.weight = self.wte.weight # tied embeddings (vanilla)
|
|
362
|
+
self.apply(self._init)
|
|
363
|
+
for name, p in self.named_parameters(): # scaled residual init
|
|
364
|
+
if name.endswith("proj.weight") or name.endswith("mlp_down.weight"):
|
|
365
|
+
nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * cfg.n_layer))
|
|
366
|
+
# ADR-006: restore the C2 no-op AFTER global init overwrote it — the
|
|
367
|
+
# associative residual must start at ~zero so the model begins as the
|
|
368
|
+
# transformer and learns the memory from there. (A test asserts this.)
|
|
369
|
+
for blk in self.blocks:
|
|
370
|
+
if getattr(blk, "c2", None) is not None:
|
|
371
|
+
nn.init.zeros_(blk.c2.out.weight)
|
|
372
|
+
if getattr(blk, "reader", None) is not None: # ADR-008: the reader starts as a no-op too
|
|
373
|
+
nn.init.zeros_(blk.reader.out.weight)
|
|
374
|
+
nn.init.zeros_(blk.reader.null)
|
|
375
|
+
if self.match_head is not None: # RES-21: the head starts silent (m = 0, no bias)
|
|
376
|
+
nn.init.zeros_(self.match_head.out.weight); nn.init.zeros_(self.match_head.out.bias)
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _init(m):
|
|
380
|
+
if isinstance(m, nn.Linear):
|
|
381
|
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
|
382
|
+
if m.bias is not None:
|
|
383
|
+
nn.init.zeros_(m.bias)
|
|
384
|
+
elif isinstance(m, nn.Embedding):
|
|
385
|
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
|
386
|
+
|
|
387
|
+
def journal_hidden(self, idx):
|
|
388
|
+
"""The residual stream at the journal block, before the reader: what the
|
|
389
|
+
cue encoder pools. Without a journal, the stream at the last block."""
|
|
390
|
+
b, t = idx.shape
|
|
391
|
+
pos = torch.arange(t, device=idx.device)
|
|
392
|
+
x = self.wte(idx) + self.wpe(pos)
|
|
393
|
+
stop = self.cfg.journal_block if self.cfg.journal == "read" else len(self.blocks)
|
|
394
|
+
for blk in self.blocks[:stop]:
|
|
395
|
+
x = blk(x)
|
|
396
|
+
return x
|
|
397
|
+
|
|
398
|
+
def read_mass(self):
|
|
399
|
+
"""The reader's attention mass on the retrieved bytes at the last forward (invariant 8)."""
|
|
400
|
+
for blk in self.blocks:
|
|
401
|
+
if getattr(blk, "reader", None) is not None and blk.last_read_mass is not None:
|
|
402
|
+
return float(blk.last_read_mass.detach())
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
def forward(self, idx, targets=None, window_tokens=None, window_mask=None, gate: float = 1.0,
|
|
406
|
+
loss_weights=None, decision_pos=None, match_targets=None):
|
|
407
|
+
"""`window_tokens` (b, r): the read window's bytes, embedded with the model's
|
|
408
|
+
own `wte`; `gate`: the router's path 4 weight; `loss_weights` (b, t): per
|
|
409
|
+
token weights (the curriculum's answer-only, asymmetric loss). RES-21:
|
|
410
|
+
`decision_pos` (b,) the position predicting the decision token; the matching
|
|
411
|
+
head fires there only, biases the decision logits, and `match_targets` (b,)
|
|
412
|
+
adds its own loss."""
|
|
413
|
+
b, t = idx.shape
|
|
414
|
+
pos = torch.arange(t, device=idx.device)
|
|
415
|
+
x = self.wte(idx) + self.wpe(pos)
|
|
416
|
+
window = self.wte(window_tokens) if window_tokens is not None else None
|
|
417
|
+
for blk in self.blocks:
|
|
418
|
+
x = blk(x, window, window_mask, gate) if blk.reader is not None else blk(x)
|
|
419
|
+
logits = self.head(self.ln_f(x))
|
|
420
|
+
self.last_match_logit = self.last_match_loss = self.last_match_acc = None
|
|
421
|
+
if self.match_head is not None and decision_pos is not None and window is not None:
|
|
422
|
+
blk = self.blocks[self.cfg.journal_block]
|
|
423
|
+
if blk.last_attended is not None:
|
|
424
|
+
from cortex_c2b.lm_bridge import TOK_CITE, TOK_UNKNOWN
|
|
425
|
+
ar = torch.arange(b, device=idx.device)
|
|
426
|
+
m = self.match_head(blk.last_pre[ar, decision_pos], blk.last_attended[ar, decision_pos])
|
|
427
|
+
self.last_match_logit = m.detach()
|
|
428
|
+
if self.cfg.journal_match_coupling:
|
|
429
|
+
add = torch.zeros_like(logits)
|
|
430
|
+
add[ar, decision_pos, TOK_CITE] = self.cfg.journal_match_coupling * m.to(logits.dtype)
|
|
431
|
+
add[ar, decision_pos, TOK_UNKNOWN] = -self.cfg.journal_match_coupling * m.to(logits.dtype)
|
|
432
|
+
logits = logits + add
|
|
433
|
+
if match_targets is not None:
|
|
434
|
+
self.last_match_loss = F.binary_cross_entropy_with_logits(m.float(), match_targets.float())
|
|
435
|
+
self.last_match_acc = ((m > 0).float() == match_targets.float()).float().mean().detach()
|
|
436
|
+
loss = None
|
|
437
|
+
if targets is not None:
|
|
438
|
+
ce = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.reshape(-1), reduction="none")
|
|
439
|
+
if loss_weights is None:
|
|
440
|
+
loss = ce.mean()
|
|
441
|
+
else:
|
|
442
|
+
w = loss_weights.reshape(-1).to(ce.dtype)
|
|
443
|
+
loss = (ce * w).sum() / w.sum().clamp(min=1.0)
|
|
444
|
+
if self.last_match_loss is not None:
|
|
445
|
+
loss = loss + self.cfg.journal_match_loss_weight * self.last_match_loss
|
|
446
|
+
return logits, loss
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def load_parent(model: "VanillaGPT", ckpt_path: str | Path, device) -> tuple[str, list[str]]:
|
|
450
|
+
"""A fine tune starts from a parent checkpoint: every parent weight loads
|
|
451
|
+
unchanged (strict on the parent's keys); only the journal's own modules may
|
|
452
|
+
be new. Returns (parent run id, the keys that were initialised fresh)."""
|
|
453
|
+
ck = torch.load(ckpt_path, map_location=device, weights_only=False)
|
|
454
|
+
result = model.load_state_dict(ck["model"], strict=False)
|
|
455
|
+
if result.unexpected_keys:
|
|
456
|
+
raise SystemExit(f"parent checkpoint has keys this model lacks: {result.unexpected_keys[:5]}")
|
|
457
|
+
allowed = ("cue_encoder.", ".reader.", "match_head.")
|
|
458
|
+
if getattr(model.cfg, "attn_local_conv", False):
|
|
459
|
+
allowed = allowed + (".conv.",) # v8: the convolution is new next to the journal's modules
|
|
460
|
+
bad = [k for k in result.missing_keys if not any(a in k for a in allowed)]
|
|
461
|
+
if bad:
|
|
462
|
+
raise SystemExit(f"parent checkpoint lacks non-journal weights: {bad[:5]}")
|
|
463
|
+
return ck.get("run_id", "unknown"), list(result.missing_keys)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ----------------------------------------------------------------------------- ledger (ADR-001)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def git_commit() -> str:
|
|
470
|
+
try:
|
|
471
|
+
return subprocess.check_output(
|
|
472
|
+
["git", "rev-parse", "HEAD"], cwd=REPO_ROOT, stderr=subprocess.DEVNULL
|
|
473
|
+
).decode().strip()
|
|
474
|
+
except Exception:
|
|
475
|
+
return "uncommitted"
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def build_record(cfg: Config, chash: str, run_id: str, started_iso: str, params: int,
|
|
479
|
+
tokens_seen: int, steps: int, wall_s: float, final_loss: float | None,
|
|
480
|
+
val_ppl: float | None, status: str, anomalies: str | None, device: torch.device) -> dict:
|
|
481
|
+
lr_schedule = f"warmup{cfg.warmup_steps}+cosine(max={cfg.lr},min={cfg.min_lr_ratio}x)"
|
|
482
|
+
if device.type == "cuda":
|
|
483
|
+
hardware = torch.cuda.get_device_name(0)
|
|
484
|
+
precision = "bf16" if torch.cuda.is_bf16_supported() else "fp16"
|
|
485
|
+
gpu_hours = round(wall_s / 3600.0, 4)
|
|
486
|
+
else:
|
|
487
|
+
hardware = platform.processor() or platform.machine() or "cpu"
|
|
488
|
+
precision = "fp32"
|
|
489
|
+
gpu_hours = None
|
|
490
|
+
return {
|
|
491
|
+
"schema_version": "run-v1",
|
|
492
|
+
"run_id": run_id,
|
|
493
|
+
"timestamp_utc": started_iso,
|
|
494
|
+
"git_commit": git_commit(),
|
|
495
|
+
"config_hash": chash,
|
|
496
|
+
"run": {"kind": cfg.kind, "component_under_test": cfg.component_under_test,
|
|
497
|
+
"status": status, "anomalies": anomalies, "notes": cfg.notes},
|
|
498
|
+
"model": {"params_total": params,
|
|
499
|
+
"architecture_id": (("journal-" if cfg.journal == "read" else "")
|
|
500
|
+
+ (f"vanilla-{max(1, round(params / 1e6))}m-byte" if cfg.c2_variant == "none"
|
|
501
|
+
else f"c2{cfg.c2_variant}-{max(1, round(params / 1e6))}m-byte")),
|
|
502
|
+
"tokenizer_id": f"byte-v0+{cfg.reserved_oracle_tokens}oracle"},
|
|
503
|
+
"training": {"tokens_seen": tokens_seen, "dataset_id": cfg.dataset_id,
|
|
504
|
+
"data_slice": cfg.data_slice, "seed": cfg.seed, "steps": steps,
|
|
505
|
+
"batch_size": cfg.batch_size, "lr_schedule": lr_schedule,
|
|
506
|
+
"precision": precision, "parent_run_id": cfg.parent_run_id},
|
|
507
|
+
"compute": {"provider": cfg.provider, "hardware": hardware, "gpu_hours": gpu_hours},
|
|
508
|
+
"results": {"final_train_loss": final_loss, "val_perplexity": val_ppl,
|
|
509
|
+
"benchmarks": {"routing_specialization_mi": None, "mqar_accuracy": None,
|
|
510
|
+
"oracle_shift_recovery": None,
|
|
511
|
+
"invariant_compliance_rate": None, "standard_suite": None}},
|
|
512
|
+
"comparison": None,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def validate_record(record: dict) -> None:
|
|
517
|
+
try:
|
|
518
|
+
import jsonschema
|
|
519
|
+
except ImportError:
|
|
520
|
+
print("[ledger] jsonschema not installed — CI will validate this record")
|
|
521
|
+
return
|
|
522
|
+
schema = json.loads(SCHEMA_PATH.read_text())
|
|
523
|
+
jsonschema.validate(record, schema)
|
|
524
|
+
print("[ledger] record valid against run-v1")
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def append_record(record: dict) -> None:
|
|
528
|
+
LEDGER_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
529
|
+
with open(LEDGER_PATH, "a", encoding="utf-8", newline="\n") as f:
|
|
530
|
+
f.write(json.dumps(record, sort_keys=True) + "\n")
|
|
531
|
+
print(f"[ledger] appended run {record['run_id']} -> {LEDGER_PATH.relative_to(REPO_ROOT)}")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def regen_latest() -> None:
|
|
535
|
+
records = []
|
|
536
|
+
if LEDGER_PATH.exists():
|
|
537
|
+
for line in LEDGER_PATH.read_text().splitlines():
|
|
538
|
+
if line.strip():
|
|
539
|
+
records.append(json.loads(line))
|
|
540
|
+
lines = ["# LATEST — run ledger state (generated by train.py — never hand-edit)", "",
|
|
541
|
+
f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')} · records: {len(records)}", ""]
|
|
542
|
+
if not records:
|
|
543
|
+
lines.append("No completed runs yet (expected pre-N1 execution).")
|
|
544
|
+
else:
|
|
545
|
+
lines += ["| run_id | date (UTC) | kind | comp | arch | tokens | steps | val_ppl | status | provider |",
|
|
546
|
+
"|---|---|---|---|---|---|---|---|---|---|"]
|
|
547
|
+
for r in records:
|
|
548
|
+
ppl = r["results"]["val_perplexity"]
|
|
549
|
+
lines.append("| {} | {} | {} | {} | {} | {:,} | {} | {} | {} | {} |".format(
|
|
550
|
+
r["run_id"], r["timestamp_utc"][:10], r["run"]["kind"],
|
|
551
|
+
r["run"]["component_under_test"] or "-", r["model"]["architecture_id"],
|
|
552
|
+
r["training"]["tokens_seen"], r["training"]["steps"],
|
|
553
|
+
f"{ppl:.3f}" if isinstance(ppl, (int, float)) else "null",
|
|
554
|
+
r["run"]["status"], r["compute"]["provider"]))
|
|
555
|
+
done = [r for r in records if r["run"]["status"] == "completed"
|
|
556
|
+
and isinstance(r["results"]["val_perplexity"], (int, float))]
|
|
557
|
+
if done:
|
|
558
|
+
best = min(done, key=lambda r: r["results"]["val_perplexity"])
|
|
559
|
+
lines += ["", f"Best val_perplexity: **{best['results']['val_perplexity']:.3f}** (run {best['run_id']})"]
|
|
560
|
+
LATEST_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
561
|
+
print(f"[ledger] regenerated {LATEST_PATH.relative_to(REPO_ROOT)}")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# ----------------------------------------------------------------------------- train
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def lr_at(step: int, total: int, cfg: Config) -> float:
|
|
568
|
+
if step < cfg.warmup_steps:
|
|
569
|
+
return cfg.lr * (step + 1) / cfg.warmup_steps
|
|
570
|
+
p = (step - cfg.warmup_steps) / max(1, total - cfg.warmup_steps)
|
|
571
|
+
return cfg.lr * (cfg.min_lr_ratio + (1 - cfg.min_lr_ratio) * 0.5 * (1 + math.cos(math.pi * p)))
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def main() -> None:
|
|
575
|
+
ap = argparse.ArgumentParser()
|
|
576
|
+
ap.add_argument("--config", type=str, help="path to config json")
|
|
577
|
+
ap.add_argument("--resume", action="store_true")
|
|
578
|
+
ap.add_argument("--regen-latest", action="store_true")
|
|
579
|
+
ap.add_argument("--time-budget-min", type=float, default=None,
|
|
580
|
+
help="stop cleanly after N minutes of training: checkpoint + exit 0, no record; rerun with --resume (session-wall / preemption survival)")
|
|
581
|
+
ap.add_argument("--relay", default=None,
|
|
582
|
+
help="checkpoint relay (cortex_data/relay.py): push out_dir to this private Kaggle dataset slug after every checkpoint; "
|
|
583
|
+
"needs KAGGLE_USERNAME and KAGGLE_KEY in the environment, silently off without them")
|
|
584
|
+
args = ap.parse_args()
|
|
585
|
+
if args.regen_latest:
|
|
586
|
+
regen_latest()
|
|
587
|
+
return
|
|
588
|
+
if not args.config:
|
|
589
|
+
raise SystemExit("--config required (or --regen-latest)")
|
|
590
|
+
|
|
591
|
+
cfg = load_config(args.config)
|
|
592
|
+
chash = config_hash(cfg)
|
|
593
|
+
torch.manual_seed(cfg.seed)
|
|
594
|
+
np.random.seed(cfg.seed)
|
|
595
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
596
|
+
use_bf16 = device.type == "cuda" and torch.cuda.is_bf16_supported()
|
|
597
|
+
use_fp16 = device.type == "cuda" and not use_bf16
|
|
598
|
+
amp_dtype = torch.bfloat16 if use_bf16 else torch.float16
|
|
599
|
+
scaler = torch.amp.GradScaler("cuda", enabled=use_fp16)
|
|
600
|
+
|
|
601
|
+
train_arr, val_arr = load_data(cfg)
|
|
602
|
+
model = VanillaGPT(cfg).to(device)
|
|
603
|
+
params = sum(p.numel() for p in model.parameters())
|
|
604
|
+
# ---- ADR-008: a fine tune starts from a parent checkpoint (strict on the parent's keys)
|
|
605
|
+
if cfg.parent_ckpt:
|
|
606
|
+
parent_id, fresh = load_parent(model, REPO_ROOT / cfg.parent_ckpt, device)
|
|
607
|
+
if cfg.parent_run_id and cfg.parent_run_id != parent_id:
|
|
608
|
+
raise SystemExit(f"parent_run_id {cfg.parent_run_id} does not match the checkpoint's {parent_id}")
|
|
609
|
+
cfg.parent_run_id = parent_id
|
|
610
|
+
print(f"[parent] {parent_id}: parent weights loaded unchanged; {len(fresh)} journal tensors fresh (zero output)")
|
|
611
|
+
# ---- ADR-008: the journal in the decode loop (default off)
|
|
612
|
+
journal_ctx = None
|
|
613
|
+
if cfg.journal == "read":
|
|
614
|
+
from cortex_c2b import Journal, POLICY_STOP
|
|
615
|
+
from cortex_c2b.crypto import key_from_env
|
|
616
|
+
from cortex_c2b.lm_bridge import JournalBridge, contrastive_loss, journal_gate
|
|
617
|
+
from cortex_c2b.organ_use import training_facts, plant_pool, make_batch, batch_stats, collate, match_tensors
|
|
618
|
+
if cfg.journal_path and key_from_env() is None:
|
|
619
|
+
raise SystemExit("journal_path is set but QUANTUM_CORTEX_JOURNAL_KEY is not in the environment: the run's "
|
|
620
|
+
"journal is sealed by default (ADR-007 D8); set the key before training, not after")
|
|
621
|
+
facts, negatives, rep_facts = training_facts(cfg.journal_pool_facts, cfg.journal_train_seed)
|
|
622
|
+
print(f"[journal] curriculum facts {rep_facts['n_facts']} negatives {rep_facts['n_negatives']} "
|
|
623
|
+
f"generator {rep_facts['generator_hash']} collisions removed {len(rep_facts['removed_collisions'])}"
|
|
624
|
+
f" | fresh pool per replant: {cfg.journal_fresh_pool} | window on LM steps: {cfg.journal_lm_window}"
|
|
625
|
+
f" | negatives {cfg.journal_neg_frac} paired: {cfg.journal_paired_negatives}"
|
|
626
|
+
f" shape kept: {cfg.journal_paired_keep_shape}"
|
|
627
|
+
f" | match head: {cfg.journal_match_head}"
|
|
628
|
+
+ (f" (loss weight {cfg.journal_match_loss_weight}, coupling {cfg.journal_match_coupling})" if cfg.journal_match_head else ""))
|
|
629
|
+
def run_journal():
|
|
630
|
+
"""The run's own scope: sealed on disk (key from the environment), policy stop --
|
|
631
|
+
a measurement run never continues in memory unnoticed (ADR-007 D9)."""
|
|
632
|
+
if cfg.journal_path:
|
|
633
|
+
return Journal(REPO_ROOT / cfg.journal_path, key=key_from_env(), policy=POLICY_STOP)
|
|
634
|
+
return Journal()
|
|
635
|
+
journal_ctx = {"facts": facts, "negatives": negatives, "rng": np.random.default_rng(cfg.seed + 17),
|
|
636
|
+
"bridge": None, "planted_at": -1, "run_journal": run_journal, "refresh": 0}
|
|
637
|
+
if cfg.optimizer != "adamw":
|
|
638
|
+
raise SystemExit("only adamw in the control scaffold — F1 (NorMuon-class) arrives via its own ablation")
|
|
639
|
+
decay = [p for n, p in model.named_parameters() if p.dim() >= 2]
|
|
640
|
+
nodecay = [p for n, p in model.named_parameters() if p.dim() < 2]
|
|
641
|
+
opt = torch.optim.AdamW([{"params": decay, "weight_decay": cfg.weight_decay},
|
|
642
|
+
{"params": nodecay, "weight_decay": 0.0}],
|
|
643
|
+
lr=cfg.lr, betas=(0.9, 0.95))
|
|
644
|
+
|
|
645
|
+
tokens_per_step = cfg.batch_size * cfg.block_size
|
|
646
|
+
total_steps = max(1, cfg.max_tokens // tokens_per_step)
|
|
647
|
+
out_dir = REPO_ROOT / cfg.out_dir
|
|
648
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
649
|
+
ckpt_path = out_dir / "ckpt.pt"
|
|
650
|
+
|
|
651
|
+
start_step, tokens_seen = 0, 0
|
|
652
|
+
started_iso = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
653
|
+
run_id = hashlib.sha256(f"{chash}{started_iso}".encode()).hexdigest()[:12]
|
|
654
|
+
if args.resume and ckpt_path.exists():
|
|
655
|
+
ck = torch.load(ckpt_path, map_location=device, weights_only=False)
|
|
656
|
+
if ck["config_hash"] != chash:
|
|
657
|
+
raise SystemExit("resume refused: config_hash mismatch (honest data — change config, change run)")
|
|
658
|
+
model.load_state_dict(ck["model"]); opt.load_state_dict(ck["opt"])
|
|
659
|
+
start_step, tokens_seen = ck["step"] + 1, ck["tokens_seen"]
|
|
660
|
+
started_iso, run_id = ck["started_iso"], ck["run_id"]
|
|
661
|
+
print(f"[resume] step {start_step}/{total_steps} run {run_id}")
|
|
662
|
+
if start_step >= total_steps:
|
|
663
|
+
print(f"[resume] run {run_id} already complete — nothing to do (its record is in the ledger)")
|
|
664
|
+
return
|
|
665
|
+
elif args.resume:
|
|
666
|
+
print("[resume] no checkpoint found — starting fresh")
|
|
667
|
+
|
|
668
|
+
print(f"[run {run_id}] device={device.type} params={params:,} steps={total_steps} "
|
|
669
|
+
f"tokens/step={tokens_per_step:,} config_hash={chash}")
|
|
670
|
+
|
|
671
|
+
gen = torch.Generator().manual_seed(cfg.seed + start_step)
|
|
672
|
+
|
|
673
|
+
def evaluate() -> float:
|
|
674
|
+
model.eval()
|
|
675
|
+
with torch.no_grad():
|
|
676
|
+
losses = []
|
|
677
|
+
for _ in range(cfg.eval_batches):
|
|
678
|
+
x, y = get_batch(val_arr, cfg, gen, device)
|
|
679
|
+
with torch.autocast(device.type, dtype=amp_dtype, enabled=device.type == "cuda"):
|
|
680
|
+
_, l = model(x, y)
|
|
681
|
+
losses.append(l.item())
|
|
682
|
+
model.train()
|
|
683
|
+
return float(np.mean(losses))
|
|
684
|
+
|
|
685
|
+
t0, loss_val, first_loss = time.time(), None, None
|
|
686
|
+
val_loss = None
|
|
687
|
+
anomalies = None
|
|
688
|
+
status = "completed"
|
|
689
|
+
cb_divergent = 0
|
|
690
|
+
# build a baseline lookup for the divergence check (ADR-006 D7), if provided
|
|
691
|
+
baseline_at = None
|
|
692
|
+
if cfg.cb_baseline_losses:
|
|
693
|
+
try:
|
|
694
|
+
bp = (REPO_ROOT / cfg.cb_baseline_losses).resolve()
|
|
695
|
+
raw = bp.read_text().strip()
|
|
696
|
+
if raw.startswith("["):
|
|
697
|
+
_bvals = json.loads(raw) # [{"step":s,"val_loss":v}, ...] or [v, ...]
|
|
698
|
+
else:
|
|
699
|
+
_bvals = [json.loads(l) for l in raw.splitlines() if l.strip()]
|
|
700
|
+
_bmap = {}
|
|
701
|
+
for i, item in enumerate(_bvals):
|
|
702
|
+
if isinstance(item, dict):
|
|
703
|
+
_bmap[int(item["step"])] = float(item["val_loss"])
|
|
704
|
+
else:
|
|
705
|
+
_bmap[(i + 1) * cfg.eval_every] = float(item)
|
|
706
|
+
_bsteps = sorted(_bmap)
|
|
707
|
+
def baseline_at(s, _m=_bmap, _ss=_bsteps):
|
|
708
|
+
# nearest recorded baseline step <= s
|
|
709
|
+
cand = [k for k in _ss if k <= s]
|
|
710
|
+
return _m[cand[-1]] if cand else (_m[_ss[0]] if _ss else None)
|
|
711
|
+
print(f"[circuit-breaker] divergence check armed against {len(_bmap)} baseline points")
|
|
712
|
+
except Exception as e:
|
|
713
|
+
print(f"[circuit-breaker] baseline load failed ({e}); NaN/Inf abort still active")
|
|
714
|
+
baseline_at = None
|
|
715
|
+
try:
|
|
716
|
+
cur_step = start_step
|
|
717
|
+
for step in range(start_step, total_steps):
|
|
718
|
+
cur_step = step
|
|
719
|
+
for g in opt.param_groups:
|
|
720
|
+
g["lr"] = lr_at(step, total_steps, cfg)
|
|
721
|
+
curriculum_step = (journal_ctx is not None
|
|
722
|
+
and journal_ctx["rng"].random() < cfg.journal_curriculum_ratio)
|
|
723
|
+
if curriculum_step:
|
|
724
|
+
# ADR-008 invariant 6: organ use is taught; the pool is replanted under the
|
|
725
|
+
# current encoder every `journal_pool_refresh` steps (a memory scope)
|
|
726
|
+
if journal_ctx["bridge"] is None or step - journal_ctx["planted_at"] >= cfg.journal_pool_refresh:
|
|
727
|
+
if cfg.journal_fresh_pool and journal_ctx["refresh"] > 0:
|
|
728
|
+
# v2: never the same facts twice -- a new seed per replant, decontaminated like the first
|
|
729
|
+
journal_ctx["facts"], journal_ctx["negatives"], _ = training_facts(
|
|
730
|
+
cfg.journal_pool_facts, cfg.journal_train_seed + 2 * journal_ctx["refresh"])
|
|
731
|
+
journal_ctx["refresh"] += 1
|
|
732
|
+
jb = JournalBridge(model, Journal(), k=cfg.journal_k, budget_bytes=cfg.journal_read_bytes,
|
|
733
|
+
seed=cfg.seed, shuffle_seed=cfg.seed + step, device=device,
|
|
734
|
+
mark=cfg.journal_familiarity_mark)
|
|
735
|
+
model.eval(); plant_pool(jb, journal_ctx["facts"], now=float(step)); model.train()
|
|
736
|
+
journal_ctx["bridge"], journal_ctx["planted_at"] = jb, step
|
|
737
|
+
jb = journal_ctx["bridge"]
|
|
738
|
+
progress = step / max(1, total_steps)
|
|
739
|
+
forced = cfg.journal_forced_frac * max(0.0, 1.0 - progress) # the bootstrap decays to zero
|
|
740
|
+
model.eval()
|
|
741
|
+
examples = make_batch(jb, journal_ctx["facts"], journal_ctx["negatives"], cfg.batch_size,
|
|
742
|
+
journal_ctx["rng"], negatives_frac=cfg.journal_neg_frac, forced_frac=forced,
|
|
743
|
+
paired=cfg.journal_paired_negatives, keep_shape=cfg.journal_paired_keep_shape)
|
|
744
|
+
model.train()
|
|
745
|
+
x, y, w, wt, wm, qseq, sseq = collate(jb, examples, cfg.journal_asym_weight, cfg.journal_read_bytes)
|
|
746
|
+
mt, dpos = match_tensors(examples, x.device) if cfg.journal_match_head else (None, None)
|
|
747
|
+
gate = journal_gate(cfg.journal_route) if cfg.journal_route != "router" else 1.0 # a learned router is measured apart (D9 held)
|
|
748
|
+
with torch.autocast(device.type, dtype=amp_dtype, enabled=device.type == "cuda"):
|
|
749
|
+
_, loss_ce = model(x, y, window_tokens=wt, window_mask=wm, gate=gate, loss_weights=w,
|
|
750
|
+
decision_pos=dpos, match_targets=mt)
|
|
751
|
+
loss_c = contrastive_loss(jb.cue_tensor(qseq), jb.cue_tensor(sseq))
|
|
752
|
+
loss = loss_ce + cfg.journal_contrastive_weight * loss_c
|
|
753
|
+
journal_ctx["last_stats"] = {**batch_stats(examples), "loss_ce": float(loss_ce.detach()), "loss_contrastive": float(loss_c.detach()),
|
|
754
|
+
"match_bce": (float(model.last_match_loss.detach()) if model.last_match_loss is not None else None),
|
|
755
|
+
"match_acc": (float(model.last_match_acc) if model.last_match_acc is not None else None)}
|
|
756
|
+
else:
|
|
757
|
+
x, y = get_batch(train_arr, cfg, gen, device)
|
|
758
|
+
wt = wm = None
|
|
759
|
+
if journal_ctx is not None and cfg.journal_lm_window and journal_ctx["bridge"] is not None:
|
|
760
|
+
# v2: the reader stays on during language modelling, fed with whatever the journal returns
|
|
761
|
+
# for each span, so an irrelevant read costs nothing after training (the e_S arm)
|
|
762
|
+
jb = journal_ctx["bridge"]
|
|
763
|
+
model.eval()
|
|
764
|
+
cues = jb.cues([row.tolist() for row in x])
|
|
765
|
+
windows = [jb.read(c).window for c in cues]
|
|
766
|
+
model.train()
|
|
767
|
+
wt, wm = jb.window_tensors(windows, cfg.journal_read_bytes)
|
|
768
|
+
with torch.autocast(device.type, dtype=amp_dtype, enabled=device.type == "cuda"):
|
|
769
|
+
_, loss = model(x, y, window_tokens=wt, window_mask=wm, gate=1.0)
|
|
770
|
+
opt.zero_grad(set_to_none=True)
|
|
771
|
+
scaler.scale(loss).backward()
|
|
772
|
+
scaler.unscale_(opt)
|
|
773
|
+
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip)
|
|
774
|
+
scaler.step(opt)
|
|
775
|
+
scaler.update()
|
|
776
|
+
loss_val = loss.item()
|
|
777
|
+
# circuit breaker — hard criterion: NaN/Inf is instant death (ADR-006 D7)
|
|
778
|
+
if cfg.cb_enabled and (math.isnan(loss_val) or math.isinf(loss_val)):
|
|
779
|
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step,
|
|
780
|
+
"tokens_seen": tokens_seen, "config_hash": chash,
|
|
781
|
+
"started_iso": started_iso, "run_id": run_id}, ckpt_path)
|
|
782
|
+
anomalies = f"NaN/Inf loss at step {step} — circuit breaker (hard abort)"
|
|
783
|
+
print(f"[circuit-breaker] {anomalies}")
|
|
784
|
+
status = "aborted"
|
|
785
|
+
cur_step = step
|
|
786
|
+
break
|
|
787
|
+
if first_loss is None:
|
|
788
|
+
first_loss = loss_val
|
|
789
|
+
tokens_seen += tokens_per_step
|
|
790
|
+
if step % cfg.log_every == 0 or step == total_steps - 1:
|
|
791
|
+
tps = tokens_seen / max(1e-9, time.time() - t0)
|
|
792
|
+
print(f"step {step}/{total_steps} loss {loss_val:.4f} lr {opt.param_groups[0]['lr']:.2e} tok/s {tps:,.0f}")
|
|
793
|
+
if journal_ctx is not None and journal_ctx.get("last_stats"):
|
|
794
|
+
st = journal_ctx["last_stats"]
|
|
795
|
+
print(f" journal: hit {st['retrieval_hit']:.2f} abstain-target {st['abstain_target_rate']:.2f} "
|
|
796
|
+
f"forced {st['n_forced']} neg {st['n_negative']} ce {st['loss_ce']:.3f} contrastive {st['loss_contrastive']:.3f}"
|
|
797
|
+
+ (f" match bce {st['match_bce']:.3f} acc {st['match_acc']:.2f}" if st.get("match_bce") is not None else ""))
|
|
798
|
+
if step and step % cfg.eval_every == 0:
|
|
799
|
+
val_loss = evaluate()
|
|
800
|
+
print(f" eval: val_loss {val_loss:.4f} ppl {math.exp(val_loss):.3f}")
|
|
801
|
+
# circuit breaker — divergence criterion vs a control baseline (ADR-006 D7)
|
|
802
|
+
if cfg.cb_enabled and baseline_at is not None:
|
|
803
|
+
b = baseline_at(step)
|
|
804
|
+
if b is not None and val_loss > cfg.cb_divergence_mult * b:
|
|
805
|
+
cb_divergent += 1
|
|
806
|
+
print(f"[circuit-breaker] divergent eval {cb_divergent}/{cfg.cb_window} "
|
|
807
|
+
f"(val {val_loss:.4f} > {cfg.cb_divergence_mult}× baseline {b:.4f} at step {step})")
|
|
808
|
+
else:
|
|
809
|
+
cb_divergent = 0
|
|
810
|
+
if cb_divergent >= cfg.cb_window:
|
|
811
|
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step,
|
|
812
|
+
"tokens_seen": tokens_seen, "config_hash": chash,
|
|
813
|
+
"started_iso": started_iso, "run_id": run_id}, ckpt_path)
|
|
814
|
+
anomalies = (f"diverged at step {step}: val {val_loss:.4f} > "
|
|
815
|
+
f"{cfg.cb_divergence_mult}× baseline {b:.4f} over {cfg.cb_window} evals "
|
|
816
|
+
f"— circuit breaker")
|
|
817
|
+
print(f"[circuit-breaker] {anomalies}")
|
|
818
|
+
status = "aborted"
|
|
819
|
+
cur_step = step
|
|
820
|
+
break
|
|
821
|
+
if step and step % cfg.ckpt_every == 0:
|
|
822
|
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step,
|
|
823
|
+
"tokens_seen": tokens_seen, "config_hash": chash,
|
|
824
|
+
"started_iso": started_iso, "run_id": run_id}, ckpt_path)
|
|
825
|
+
if args.relay:
|
|
826
|
+
from cortex_data import relay
|
|
827
|
+
if relay.push_async(out_dir, args.relay, f"step {step}"):
|
|
828
|
+
print(f"[relay] pushing {out_dir} -> {args.relay} (step {step})")
|
|
829
|
+
if args.time_budget_min is not None and (time.time() - t0) / 60.0 >= args.time_budget_min:
|
|
830
|
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step,
|
|
831
|
+
"tokens_seen": tokens_seen, "config_hash": chash,
|
|
832
|
+
"started_iso": started_iso, "run_id": run_id}, ckpt_path)
|
|
833
|
+
if args.relay:
|
|
834
|
+
from cortex_data import relay
|
|
835
|
+
relay.push(out_dir, args.relay, f"budget stop at step {step}")
|
|
836
|
+
print(f"[budget] {args.time_budget_min:.0f} min reached at step {step}/{total_steps} — "
|
|
837
|
+
f"checkpoint saved, exiting cleanly (no record); rerun with --resume to continue")
|
|
838
|
+
return
|
|
839
|
+
except KeyboardInterrupt:
|
|
840
|
+
status, anomalies = "aborted", "KeyboardInterrupt — checkpoint holds last saved step"
|
|
841
|
+
val_loss = evaluate() if status == "completed" else val_loss
|
|
842
|
+
wall = time.time() - t0
|
|
843
|
+
if status == "completed" and first_loss is not None and loss_val is not None and loss_val >= first_loss:
|
|
844
|
+
anomalies = f"train loss did not decrease ({first_loss:.4f} -> {loss_val:.4f})"
|
|
845
|
+
val_ppl = round(math.exp(val_loss), 4) if isinstance(val_loss, float) else None
|
|
846
|
+
torch.save({"model": model.state_dict(), "opt": opt.state_dict(),
|
|
847
|
+
"step": (total_steps - 1) if status == "completed" else cur_step,
|
|
848
|
+
"tokens_seen": tokens_seen, "config_hash": chash,
|
|
849
|
+
"started_iso": started_iso, "run_id": run_id}, ckpt_path)
|
|
850
|
+
record = build_record(cfg, chash, run_id, started_iso, params, tokens_seen,
|
|
851
|
+
total_steps if status == "completed" else start_step,
|
|
852
|
+
wall, round(loss_val, 4) if loss_val is not None else None,
|
|
853
|
+
val_ppl, status, anomalies, device)
|
|
854
|
+
if journal_ctx is not None and cfg.journal_hm_arm and status == "completed":
|
|
855
|
+
# ADR-008 invariant 9: the frozen protocol's LM arm, its result in the record and as a file
|
|
856
|
+
from cortex_c2b.hm_lm import run_hm_lm
|
|
857
|
+
vb = [get_batch(val_arr, cfg, gen, device) for _ in range(cfg.eval_batches)]
|
|
858
|
+
hm = run_hm_lm(model, journal_ctx["run_journal"], n_facts=cfg.journal_hm_facts, n_negctrl=cfg.journal_hm_negctrl,
|
|
859
|
+
k=cfg.journal_k, budget=cfg.journal_read_bytes, window_len=cfg.journal_read_bytes,
|
|
860
|
+
val_batches=vb, on_disk=bool(cfg.journal_path), mark=cfg.journal_familiarity_mark)
|
|
861
|
+
(out_dir / "hm-lm.json").write_text(json.dumps(hm, indent=2) + "\n", encoding="utf-8")
|
|
862
|
+
kept = REPO_ROOT / "metrics" / "mqar" / f"hm-lm-{run_id}.json" # runs/ is never committed; this is
|
|
863
|
+
kept.parent.mkdir(parents=True, exist_ok=True) # the artefact a RESULTS row cites
|
|
864
|
+
kept.write_text(json.dumps(hm, indent=2) + "\n", encoding="utf-8")
|
|
865
|
+
suite = {k: (float(v) if isinstance(v, bool) else v) for k, v in hm.items()
|
|
866
|
+
if k.startswith("hm_") and isinstance(v, (int, float)) and v is not None}
|
|
867
|
+
suite["hm_persistent"] = float(hm["persistent"]); suite["hm_claimable"] = float(hm["claimable"])
|
|
868
|
+
record["results"]["benchmarks"]["standard_suite"] = suite
|
|
869
|
+
print(f"[hm-lm] {hm['verdict']} | recall on {hm['hm_recall_on']:.3f} off {hm['hm_recall_off']:.3f} "
|
|
870
|
+
f"skill delta {hm['hm_skill_delta']:.4f} invalid citation {hm['hm_invalid_citation_on']:.3f} "
|
|
871
|
+
f"| retained: {kept}")
|
|
872
|
+
validate_record(record)
|
|
873
|
+
append_record(record)
|
|
874
|
+
if args.relay:
|
|
875
|
+
from cortex_data import relay
|
|
876
|
+
relay.wait()
|
|
877
|
+
if relay.push(out_dir, args.relay, "final"):
|
|
878
|
+
print(f"[relay] final push of {out_dir} -> {args.relay}")
|
|
879
|
+
regen_latest()
|
|
880
|
+
print("\n[record — copy this line into metrics/runs.jsonl on the machine that commits]")
|
|
881
|
+
print(json.dumps(record, sort_keys=True))
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
if __name__ == "__main__":
|
|
885
|
+
main()
|