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
cortex_data/__init__.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""cortex_data — the data composition layer (ADR-005, Slice 1).
|
|
2
|
+
|
|
3
|
+
A data mix is a DAG of typed, composable operators. Each operator is a
|
|
4
|
+
lazy, pull-based iterator over byte tokens (uint16 in 0..255 plus reserved
|
|
5
|
+
oracle ids); nothing intermediate is materialised. Every node has a
|
|
6
|
+
deterministic hash; the sink hash is the mix_hash — two runs are comparable
|
|
7
|
+
iff they share it (ADR-005 D2). The engine is written from scratch (D8) and
|
|
8
|
+
orchestrates licensed sources via credited recipes.
|
|
9
|
+
|
|
10
|
+
Slice 1 implements exactly what N2 needs: the operator interface, the hashed
|
|
11
|
+
DAG manifest, lazy `source` / `decontaminate` / `mix`, compilation to a
|
|
12
|
+
uint16 .bin the trainer already reads, and the five-invariant harness (tests).
|
|
13
|
+
Memoisation, Bloom-at-scale and the data-β estimator are later slices.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Iterator, Sequence
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
TOKEN_DTYPE = np.uint16 # matches train.py's memmap
|
|
26
|
+
_CHUNK = 1 << 16 # streaming granularity (tokens)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _hash_obj(obj) -> str:
|
|
30
|
+
"""Deterministic short hash of a JSON-able object."""
|
|
31
|
+
return hashlib.sha256(
|
|
32
|
+
json.dumps(obj, sort_keys=True, separators=(",", ":")).encode()
|
|
33
|
+
).hexdigest()[:16]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------- #
|
|
37
|
+
# Operator base #
|
|
38
|
+
# --------------------------------------------------------------------------- #
|
|
39
|
+
class Op:
|
|
40
|
+
"""A typed, composable, lazy operator over a stream of uint16 tokens.
|
|
41
|
+
|
|
42
|
+
Subclasses implement `stream()` (a fresh iterator over token chunks,
|
|
43
|
+
each a 1-D uint16 ndarray) and `_spec()` (the JSON-able description used
|
|
44
|
+
for hashing). `node_hash` is f(spec, input hashes) — content addressing.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
op_type: str = "op"
|
|
48
|
+
inputs: Sequence["Op"] = ()
|
|
49
|
+
|
|
50
|
+
def _spec(self) -> dict:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
def stream(self, *, seed: int) -> Iterator[np.ndarray]:
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
# -- hashing -------------------------------------------------------------
|
|
57
|
+
def node_hash(self) -> str:
|
|
58
|
+
return _hash_obj(
|
|
59
|
+
{
|
|
60
|
+
"op": self.op_type,
|
|
61
|
+
"spec": self._spec(),
|
|
62
|
+
"inputs": [c.node_hash() for c in self.inputs],
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def manifest(self) -> dict:
|
|
67
|
+
"""The DAG rooted at this node, as a JSON-able recipe (ADR-005 D2)."""
|
|
68
|
+
return {
|
|
69
|
+
"op": self.op_type,
|
|
70
|
+
"spec": self._spec(),
|
|
71
|
+
"hash": self.node_hash(),
|
|
72
|
+
"inputs": [c.manifest() for c in self.inputs],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# -- materialisation -----------------------------------------------------
|
|
76
|
+
def to_bin(self, path: str | Path, *, seed: int, max_tokens: int | None = None) -> int:
|
|
77
|
+
"""Stream this node to a uint16 .bin the trainer reads. Returns token count.
|
|
78
|
+
|
|
79
|
+
Only the sink is materialised (ADR-005 D3: no intermediate files).
|
|
80
|
+
"""
|
|
81
|
+
path = Path(path)
|
|
82
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
written = 0
|
|
84
|
+
with open(path, "wb") as fh:
|
|
85
|
+
for chunk in self.stream(seed=seed):
|
|
86
|
+
if max_tokens is not None and written + len(chunk) > max_tokens:
|
|
87
|
+
chunk = chunk[: max_tokens - written]
|
|
88
|
+
if len(chunk):
|
|
89
|
+
fh.write(np.ascontiguousarray(chunk, dtype=TOKEN_DTYPE).tobytes())
|
|
90
|
+
written += len(chunk)
|
|
91
|
+
if max_tokens is not None and written >= max_tokens:
|
|
92
|
+
break
|
|
93
|
+
return written
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --------------------------------------------------------------------------- #
|
|
97
|
+
# Sources #
|
|
98
|
+
# --------------------------------------------------------------------------- #
|
|
99
|
+
@dataclass
|
|
100
|
+
class BinSource(Op):
|
|
101
|
+
"""A licensed corpus already compiled to a uint16 .bin, content-addressed.
|
|
102
|
+
|
|
103
|
+
`dataset_id` and `license` are declared for provenance/attribution (D8).
|
|
104
|
+
`content_sha` pins the exact bytes; if absent it is computed on first use.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
path: str
|
|
108
|
+
dataset_id: str
|
|
109
|
+
license: str
|
|
110
|
+
slice: str = ""
|
|
111
|
+
content_sha: str | None = None
|
|
112
|
+
op_type: str = field(default="source", init=False)
|
|
113
|
+
|
|
114
|
+
def _resolve(self) -> Path:
|
|
115
|
+
return Path(self.path)
|
|
116
|
+
|
|
117
|
+
def _sha(self) -> str:
|
|
118
|
+
if self.content_sha:
|
|
119
|
+
return self.content_sha
|
|
120
|
+
p = self._resolve()
|
|
121
|
+
if not p.exists():
|
|
122
|
+
# hash the spec instead so the DAG is still deterministic offline
|
|
123
|
+
return "unresolved:" + _hash_obj({"path": self.path, "id": self.dataset_id})
|
|
124
|
+
h = hashlib.sha256()
|
|
125
|
+
with open(p, "rb") as fh:
|
|
126
|
+
for block in iter(lambda: fh.read(1 << 20), b""):
|
|
127
|
+
h.update(block)
|
|
128
|
+
self.content_sha = h.hexdigest()[:16]
|
|
129
|
+
return self.content_sha
|
|
130
|
+
|
|
131
|
+
def _spec(self) -> dict:
|
|
132
|
+
return {
|
|
133
|
+
"dataset_id": self.dataset_id,
|
|
134
|
+
"license": self.license,
|
|
135
|
+
"slice": self.slice,
|
|
136
|
+
"content_sha": self._sha(),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
def stream(self, *, seed: int) -> Iterator[np.ndarray]:
|
|
140
|
+
p = self._resolve()
|
|
141
|
+
if not p.exists():
|
|
142
|
+
raise FileNotFoundError(f"source .bin missing: {p} ({self.dataset_id})")
|
|
143
|
+
arr = np.memmap(p, dtype=TOKEN_DTYPE, mode="r")
|
|
144
|
+
for i in range(0, len(arr), _CHUNK):
|
|
145
|
+
yield np.asarray(arr[i : i + _CHUNK])
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class ArraySource(Op):
|
|
150
|
+
"""An in-memory token array — used by tests and small deterministic inputs."""
|
|
151
|
+
|
|
152
|
+
tokens: np.ndarray
|
|
153
|
+
name: str = "array"
|
|
154
|
+
op_type: str = field(default="source", init=False)
|
|
155
|
+
|
|
156
|
+
def _spec(self) -> dict:
|
|
157
|
+
return {"name": self.name, "content_sha": _hash_obj(self.tokens.tolist())}
|
|
158
|
+
|
|
159
|
+
def stream(self, *, seed: int) -> Iterator[np.ndarray]:
|
|
160
|
+
arr = np.asarray(self.tokens, dtype=TOKEN_DTYPE)
|
|
161
|
+
for i in range(0, len(arr), _CHUNK):
|
|
162
|
+
yield arr[i : i + _CHUNK]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------------------- #
|
|
166
|
+
# Combinators #
|
|
167
|
+
# --------------------------------------------------------------------------- #
|
|
168
|
+
@dataclass
|
|
169
|
+
class Mix(Op):
|
|
170
|
+
"""Proportional sampling WITHOUT replacement (ADR-005 D6).
|
|
171
|
+
|
|
172
|
+
Each source contributes a fraction of the final stream proportional to its
|
|
173
|
+
weight, drawn without replacement from that source's tokens. This makes the
|
|
174
|
+
identity and duplication invariants (D7) hold, and is the seam a future
|
|
175
|
+
data-β resolver (RES-14) slots into: weights are *resolved* values here.
|
|
176
|
+
|
|
177
|
+
Determinism: block order and per-source sampling are driven by `seed`.
|
|
178
|
+
Weight resolution is a constant today (D5); the resolver seam is the
|
|
179
|
+
`_resolved_weights()` method, overridable later without touching callers.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
sources: Sequence[Op]
|
|
183
|
+
weights: Sequence[float]
|
|
184
|
+
block: int = 4096 # interleave granularity (tokens per draw)
|
|
185
|
+
total: int | None = None # target output budget; default = weighted mean of
|
|
186
|
+
# source lengths (so mixing does not inflate size)
|
|
187
|
+
op_type: str = field(default="mix", init=False)
|
|
188
|
+
|
|
189
|
+
def __post_init__(self):
|
|
190
|
+
if len(self.sources) != len(self.weights):
|
|
191
|
+
raise ValueError("mix: sources and weights length mismatch")
|
|
192
|
+
if not self.sources:
|
|
193
|
+
raise ValueError("mix: at least one source required")
|
|
194
|
+
self.inputs = tuple(self.sources)
|
|
195
|
+
|
|
196
|
+
def _resolved_weights(self) -> list[float]:
|
|
197
|
+
"""The RES-14 seam. Today: normalise the declared constants."""
|
|
198
|
+
w = np.asarray(self.weights, dtype=np.float64)
|
|
199
|
+
if (w < 0).any():
|
|
200
|
+
raise ValueError("mix: weights must be non-negative")
|
|
201
|
+
s = w.sum()
|
|
202
|
+
if s <= 0:
|
|
203
|
+
raise ValueError("mix: weights sum to zero")
|
|
204
|
+
return (w / s).tolist()
|
|
205
|
+
|
|
206
|
+
def _spec(self) -> dict:
|
|
207
|
+
return {"weights": self._resolved_weights(), "block": self.block}
|
|
208
|
+
|
|
209
|
+
def _materialise_inputs(self, seed: int) -> list[np.ndarray]:
|
|
210
|
+
out = []
|
|
211
|
+
for c in self.sources:
|
|
212
|
+
parts = list(c.stream(seed=seed))
|
|
213
|
+
out.append(
|
|
214
|
+
np.concatenate(parts) if parts else np.empty(0, dtype=TOKEN_DTYPE)
|
|
215
|
+
)
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
def stream(self, *, seed: int) -> Iterator[np.ndarray]:
|
|
219
|
+
weights = self._resolved_weights()
|
|
220
|
+
pools = self._materialise_inputs(seed)
|
|
221
|
+
n = len(pools)
|
|
222
|
+
|
|
223
|
+
# single-source identity fast-path: mix([S]) is exactly S (D7 identity)
|
|
224
|
+
if n == 1:
|
|
225
|
+
for i in range(0, len(pools[0]), _CHUNK):
|
|
226
|
+
yield pools[0][i : i + _CHUNK]
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
# --- signature-aware redundancy folding (the bit-vector insight) ------
|
|
230
|
+
# Two sources with the same content signature are the SAME data. We fold
|
|
231
|
+
# them by content hash and combine their weights, so a budget is spread
|
|
232
|
+
# over the REAL union of tokens, never over overlapping re-draws. This is
|
|
233
|
+
# what makes mix([S,S], budget=|S|) a multiset-equivalent of S: the
|
|
234
|
+
# duplicate is recognised, not re-sampled. (ADR-005 D6; RES-15 phase-1
|
|
235
|
+
# provenance / RES-16 content-addressed consolidation.)
|
|
236
|
+
sigs: dict[str, int] = {}
|
|
237
|
+
folded_pools: list[np.ndarray] = []
|
|
238
|
+
folded_w: list[float] = []
|
|
239
|
+
for pool, w in zip(pools, weights):
|
|
240
|
+
sig = _hash_obj(pool.tolist())
|
|
241
|
+
if sig in sigs:
|
|
242
|
+
folded_w[sigs[sig]] += w
|
|
243
|
+
else:
|
|
244
|
+
sigs[sig] = len(folded_pools)
|
|
245
|
+
folded_pools.append(pool)
|
|
246
|
+
folded_w.append(w)
|
|
247
|
+
pools, weights = folded_pools, folded_w
|
|
248
|
+
m = len(pools)
|
|
249
|
+
s = sum(weights)
|
|
250
|
+
weights = [w / s for w in weights]
|
|
251
|
+
|
|
252
|
+
if m == 1: # everything folded to one source → it IS that source
|
|
253
|
+
budget = int(self.total) if self.total is not None else len(pools[0])
|
|
254
|
+
budget = min(budget, len(pools[0]))
|
|
255
|
+
for i in range(0, budget, _CHUNK):
|
|
256
|
+
yield pools[0][i : min(i + _CHUNK, budget)]
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# Output budget (D6): mixing redistributes a budget, it does not inflate
|
|
260
|
+
# size. Default = weighted mean of (deduplicated) source lengths.
|
|
261
|
+
if self.total is not None:
|
|
262
|
+
budget = int(self.total)
|
|
263
|
+
else:
|
|
264
|
+
budget = int(round(sum(w * len(p) for w, p in zip(weights, pools))))
|
|
265
|
+
|
|
266
|
+
# Per-source target, uniform WITHOUT replacement over that source's real
|
|
267
|
+
# tokens (uniform stride, not front-loaded blocks — so a source is
|
|
268
|
+
# covered evenly, and identical sources already folded above).
|
|
269
|
+
targets = [min(int(round(w * budget)), len(p)) for w, p in zip(weights, pools)]
|
|
270
|
+
drift = budget - sum(targets)
|
|
271
|
+
# push leftover onto the largest-capacity source, capped
|
|
272
|
+
order = sorted(range(m), key=lambda i: len(pools[i]) - targets[i], reverse=True)
|
|
273
|
+
k = 0
|
|
274
|
+
while drift > 0 and k < m:
|
|
275
|
+
i = order[k]
|
|
276
|
+
room = len(pools[i]) - targets[i]
|
|
277
|
+
add = min(drift, room)
|
|
278
|
+
targets[i] += add
|
|
279
|
+
drift -= add
|
|
280
|
+
k += 1
|
|
281
|
+
|
|
282
|
+
# deterministic interleave of evenly-strided samples from each source
|
|
283
|
+
picks = []
|
|
284
|
+
for i, t in enumerate(targets):
|
|
285
|
+
if t <= 0:
|
|
286
|
+
continue
|
|
287
|
+
idx = np.linspace(0, len(pools[i]) - 1, num=t, dtype=np.int64)
|
|
288
|
+
picks.append((i, idx))
|
|
289
|
+
order_rng = np.random.default_rng(seed ^ 0x9E3779B9)
|
|
290
|
+
cursors = {i: 0 for i, _ in picks}
|
|
291
|
+
remaining = {i: len(idx) for i, idx in picks}
|
|
292
|
+
idx_of = {i: idx for i, idx in picks}
|
|
293
|
+
buf: list[np.ndarray] = []
|
|
294
|
+
buf_len = 0
|
|
295
|
+
total_out = sum(remaining.values())
|
|
296
|
+
produced = 0
|
|
297
|
+
while produced < total_out and any(r > 0 for r in remaining.values()):
|
|
298
|
+
live = [i for i in remaining if remaining[i] > 0]
|
|
299
|
+
probs = np.asarray([remaining[i] for i in live], dtype=np.float64)
|
|
300
|
+
probs /= probs.sum()
|
|
301
|
+
pick = live[int(order_rng.choice(len(live), p=probs))]
|
|
302
|
+
take = min(self.block, remaining[pick])
|
|
303
|
+
sel = idx_of[pick][cursors[pick] : cursors[pick] + take]
|
|
304
|
+
seg = pools[pick][sel]
|
|
305
|
+
cursors[pick] += take
|
|
306
|
+
remaining[pick] -= take
|
|
307
|
+
produced += take
|
|
308
|
+
buf.append(seg)
|
|
309
|
+
buf_len += take
|
|
310
|
+
if buf_len >= _CHUNK:
|
|
311
|
+
yield np.concatenate(buf)
|
|
312
|
+
buf, buf_len = [], 0
|
|
313
|
+
if buf:
|
|
314
|
+
yield np.concatenate(buf)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# --------------------------------------------------------------------------- #
|
|
318
|
+
# Transforms #
|
|
319
|
+
# --------------------------------------------------------------------------- #
|
|
320
|
+
@dataclass
|
|
321
|
+
class Decontaminate(Op):
|
|
322
|
+
"""First-class decontamination operator (ADR-005 D4).
|
|
323
|
+
|
|
324
|
+
Slice 1: exact n-gram membership against provided eval-set token sequences.
|
|
325
|
+
Emits a contamination report (`last_report`) and, in `strict` mode, raises
|
|
326
|
+
if any eval n-gram is found in the source. The Bloom-filter algebra
|
|
327
|
+
(OR/AND/XOR at scale) is a later slice; the interface here is stable.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
source: Op
|
|
331
|
+
eval_ngrams: Sequence[Sequence[int]] = ()
|
|
332
|
+
n: int = 13
|
|
333
|
+
strict: bool = True
|
|
334
|
+
op_type: str = field(default="decontaminate", init=False)
|
|
335
|
+
last_report: dict = field(default_factory=dict, init=False, compare=False)
|
|
336
|
+
|
|
337
|
+
def __post_init__(self):
|
|
338
|
+
self.inputs = (self.source,)
|
|
339
|
+
|
|
340
|
+
def _spec(self) -> dict:
|
|
341
|
+
# hash the eval-set signature and policy, not the raw grams
|
|
342
|
+
sig = _hash_obj([list(g) for g in self.eval_ngrams])
|
|
343
|
+
return {"n": self.n, "strict": self.strict, "eval_sig": sig}
|
|
344
|
+
|
|
345
|
+
def _eval_set(self) -> set[bytes]:
|
|
346
|
+
out = set()
|
|
347
|
+
for g in self.eval_ngrams:
|
|
348
|
+
arr = np.asarray(g, dtype=TOKEN_DTYPE)
|
|
349
|
+
out.add(arr.tobytes())
|
|
350
|
+
return out
|
|
351
|
+
|
|
352
|
+
def stream(self, *, seed: int) -> Iterator[np.ndarray]:
|
|
353
|
+
evals = self._eval_set()
|
|
354
|
+
hits = 0
|
|
355
|
+
checked = 0
|
|
356
|
+
# stream through; check rolling n-grams across chunk boundaries
|
|
357
|
+
carry = np.empty(0, dtype=TOKEN_DTYPE)
|
|
358
|
+
for chunk in self.source.stream(seed=seed):
|
|
359
|
+
buf = np.concatenate([carry, chunk]) if len(carry) else chunk
|
|
360
|
+
if evals and len(buf) >= self.n:
|
|
361
|
+
for i in range(len(buf) - self.n + 1):
|
|
362
|
+
checked += 1
|
|
363
|
+
if buf[i : i + self.n].tobytes() in evals:
|
|
364
|
+
hits += 1
|
|
365
|
+
# keep the last n-1 tokens to catch grams spanning the boundary
|
|
366
|
+
carry = buf[-(self.n - 1):] if self.n > 1 else np.empty(0, dtype=TOKEN_DTYPE)
|
|
367
|
+
yield chunk # decontaminate is pass-through in Slice 1 (report-only)
|
|
368
|
+
self.last_report = {"n": self.n, "eval_ngrams": len(evals),
|
|
369
|
+
"checked": checked, "hits": hits}
|
|
370
|
+
if self.strict and hits > 0:
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"decontamination failed: {hits} eval n-gram(s) present in source "
|
|
373
|
+
f"(n={self.n}). This mix is not comparable until cleaned."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# --------------------------------------------------------------------------- #
|
|
378
|
+
# Manifest I/O #
|
|
379
|
+
# --------------------------------------------------------------------------- #
|
|
380
|
+
def write_manifest(root: Op, path: str | Path) -> str:
|
|
381
|
+
"""Write the DAG manifest (the reproducible recipe) and return mix_hash."""
|
|
382
|
+
path = Path(path)
|
|
383
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
384
|
+
mh = root.node_hash()
|
|
385
|
+
doc = {"mix_hash": mh, "dag": root.manifest()}
|
|
386
|
+
path.write_text(json.dumps(doc, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
387
|
+
return mh
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def mix_hash(root: Op) -> str:
|
|
391
|
+
"""The sink hash — two runs are comparable iff equal (ADR-005 D2)."""
|
|
392
|
+
return root.node_hash()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# --------------------------------------------------------------------------- #
|
|
396
|
+
# Manifest -> DAG (build an Op tree from a JSON recipe, ADR-005 D2) #
|
|
397
|
+
# --------------------------------------------------------------------------- #
|
|
398
|
+
def from_manifest(doc: dict) -> "Op":
|
|
399
|
+
"""Build an operator DAG from a mix manifest (the reproducible recipe).
|
|
400
|
+
|
|
401
|
+
Schema (minimal, Slice 1):
|
|
402
|
+
{"op": "source", "path": "...", "dataset_id": "...", "license": "...",
|
|
403
|
+
"slice": "...", "content_sha": "..."?}
|
|
404
|
+
{"op": "decontaminate", "input": <node>, "n": 13, "strict": true,
|
|
405
|
+
"eval_ngrams": [[...], ...]?}
|
|
406
|
+
{"op": "mix", "inputs": [<node>, ...], "weights": [...], "block": 4096,
|
|
407
|
+
"total": null?}
|
|
408
|
+
A bare {"dag": <node>, ...} wrapper is unwrapped.
|
|
409
|
+
"""
|
|
410
|
+
if "dag" in doc and "op" not in doc:
|
|
411
|
+
doc = doc["dag"]
|
|
412
|
+
op = doc["op"]
|
|
413
|
+
if op == "source":
|
|
414
|
+
return BinSource(
|
|
415
|
+
path=doc["path"], dataset_id=doc["dataset_id"],
|
|
416
|
+
license=doc.get("license", "unspecified"),
|
|
417
|
+
slice=doc.get("slice", ""), content_sha=doc.get("content_sha"),
|
|
418
|
+
)
|
|
419
|
+
if op == "decontaminate":
|
|
420
|
+
return Decontaminate(
|
|
421
|
+
from_manifest(doc["input"]),
|
|
422
|
+
eval_ngrams=[list(g) for g in doc.get("eval_ngrams", [])],
|
|
423
|
+
n=int(doc.get("n", 13)), strict=bool(doc.get("strict", True)),
|
|
424
|
+
)
|
|
425
|
+
if op == "mix":
|
|
426
|
+
return Mix(
|
|
427
|
+
[from_manifest(c) for c in doc["inputs"]],
|
|
428
|
+
list(doc["weights"]),
|
|
429
|
+
block=int(doc.get("block", 4096)),
|
|
430
|
+
total=doc.get("total"),
|
|
431
|
+
)
|
|
432
|
+
raise ValueError(f"from_manifest: unknown op '{op}'")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def load_manifest(path: str | Path) -> tuple["Op", str]:
|
|
436
|
+
"""Load a mix manifest file → (dag, mix_hash)."""
|
|
437
|
+
doc = json.loads(Path(path).read_text())
|
|
438
|
+
dag = from_manifest(doc)
|
|
439
|
+
return dag, dag.node_hash()
|
cortex_data/relay.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""The checkpoint relay (2026-09-16): a store that survives the container.
|
|
2
|
+
|
|
3
|
+
Kaggle relaunches an interrupted session in a FRESH container: `/kaggle/working` is empty,
|
|
4
|
+
the runs directory kept outside the clone (the resume of 2026-09-13) is gone, and a run of
|
|
5
|
+
several hours restarts from step 0 (v7, run `7e343011489f`, relaunched at step 1740). The only
|
|
6
|
+
stores that outlive a container are a version's Output, written at the END of a session, and
|
|
7
|
+
Kaggle datasets, which the API can update from inside a running session.
|
|
8
|
+
|
|
9
|
+
This module pushes a directory (a run's checkpoint and sealed journal, or a ladder's unit
|
|
10
|
+
files) to a PRIVATE Kaggle dataset after every checkpoint or unit, as ONE tar file inside a
|
|
11
|
+
staging directory (no directory mode subtleties), and pulls the latest version back before a
|
|
12
|
+
resume. Everything is optional: without the two secrets (`KAGGLE_USERNAME`, `KAGGLE_KEY`) or
|
|
13
|
+
without the `kaggle` command, `available()` is False and the callers run as before.
|
|
14
|
+
|
|
15
|
+
relay.pull("quantum-cortex-relay-journal-n1-v7", "runs/journal-n1-v7") # before the resume lookup
|
|
16
|
+
relay.push_async("runs/journal-n1-v7", "quantum-cortex-relay-journal-n1-v7", "step 3000") # after a checkpoint
|
|
17
|
+
|
|
18
|
+
Pushes run in a background process and never block training; a push that is still running
|
|
19
|
+
when the next checkpoint lands is left to finish and the new one is skipped (the next
|
|
20
|
+
checkpoint will push). A dataset that does not exist yet is created on the first push
|
|
21
|
+
(private, CC0 metadata on a private store is irrelevant). One dataset per run or per ladder
|
|
22
|
+
directory, named after it, so two runs never overwrite each other.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import tarfile
|
|
32
|
+
import tempfile
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
ARCHIVE = "relay.tar"
|
|
37
|
+
_inflight: dict[str, subprocess.Popen] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def kaggle_cmd() -> list[str] | None:
|
|
41
|
+
"""The kaggle command, or None when it is not installed."""
|
|
42
|
+
exe = shutil.which("kaggle")
|
|
43
|
+
if exe:
|
|
44
|
+
return [exe]
|
|
45
|
+
try: # the module without the console script on PATH
|
|
46
|
+
import kaggle # noqa: F401
|
|
47
|
+
return [sys.executable, "-m", "kaggle.cli"]
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def available() -> bool:
|
|
53
|
+
return bool(os.environ.get("KAGGLE_USERNAME")) and bool(os.environ.get("KAGGLE_KEY")) and kaggle_cmd() is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def dataset_id(slug: str) -> str:
|
|
57
|
+
return f"{os.environ['KAGGLE_USERNAME']}/{slug}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _stage(src: Path, slug: str, message: str) -> Path:
|
|
61
|
+
"""A staging directory with the metadata and ONE tar of `src`."""
|
|
62
|
+
stage = Path(tempfile.mkdtemp(prefix="relay-"))
|
|
63
|
+
with tarfile.open(stage / ARCHIVE, "w") as tar:
|
|
64
|
+
tar.add(src, arcname=src.name)
|
|
65
|
+
(stage / "dataset-metadata.json").write_text(json.dumps({
|
|
66
|
+
"title": f"quantum-cortex relay: {slug}"[:50], "id": dataset_id(slug),
|
|
67
|
+
"licenses": [{"name": "CC0-1.0"}],
|
|
68
|
+
"description": f"checkpoint relay for {src.name}; last push: {message}",
|
|
69
|
+
}, indent=2), encoding="utf-8")
|
|
70
|
+
return stage
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def exists(slug: str) -> bool:
|
|
74
|
+
cmd = kaggle_cmd()
|
|
75
|
+
if cmd is None:
|
|
76
|
+
return False
|
|
77
|
+
r = subprocess.run(cmd + ["datasets", "status", dataset_id(slug)], capture_output=True, text=True)
|
|
78
|
+
return r.returncode == 0 and "ready" in (r.stdout + r.stderr).lower()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def push_command(stage: Path, slug: str, message: str, create: bool) -> list[str]:
|
|
82
|
+
cmd = kaggle_cmd() or ["kaggle"]
|
|
83
|
+
if create:
|
|
84
|
+
return cmd + ["datasets", "create", "-p", str(stage), "-q"]
|
|
85
|
+
return cmd + ["datasets", "version", "-p", str(stage), "-m", message[:80], "-q"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def push(src, slug: str, message: str = "") -> bool:
|
|
89
|
+
"""Push `src` now (blocking). Returns True when the command returned 0."""
|
|
90
|
+
if not available():
|
|
91
|
+
return False
|
|
92
|
+
src = Path(src)
|
|
93
|
+
if not src.is_dir():
|
|
94
|
+
return False
|
|
95
|
+
stage = _stage(src, slug, message)
|
|
96
|
+
r = subprocess.run(push_command(stage, slug, message, create=not exists(slug)), capture_output=True, text=True)
|
|
97
|
+
shutil.rmtree(stage, ignore_errors=True)
|
|
98
|
+
if r.returncode != 0:
|
|
99
|
+
print(f"[relay] push failed ({slug}): {(r.stdout + r.stderr).strip()[-300:]}")
|
|
100
|
+
else:
|
|
101
|
+
print(f"[relay] pushed {slug} ({message}): {(r.stdout + r.stderr).strip()[-120:] or 'ok'}")
|
|
102
|
+
return r.returncode == 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def push_async(src, slug: str, message: str = "") -> bool:
|
|
106
|
+
"""Push in a background process; skipped (False) when the previous push of this slug is
|
|
107
|
+
still running, when the relay is not available, or when `src` is not a directory."""
|
|
108
|
+
if not available():
|
|
109
|
+
return False
|
|
110
|
+
src = Path(src)
|
|
111
|
+
if not src.is_dir():
|
|
112
|
+
return False
|
|
113
|
+
prev = _inflight.get(slug)
|
|
114
|
+
if prev is not None and prev.poll() is None:
|
|
115
|
+
return False
|
|
116
|
+
stage = _stage(src, slug, message)
|
|
117
|
+
cmd = push_command(stage, slug, message, create=not exists(slug))
|
|
118
|
+
_inflight[slug] = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def wait(timeout: float = 600.0) -> None:
|
|
123
|
+
"""Wait for the in flight pushes (called at the end of a run so the last checkpoint lands)."""
|
|
124
|
+
t0 = time.time()
|
|
125
|
+
for p in list(_inflight.values()):
|
|
126
|
+
remaining = max(0.0, timeout - (time.time() - t0))
|
|
127
|
+
try:
|
|
128
|
+
p.wait(timeout=remaining)
|
|
129
|
+
except subprocess.TimeoutExpired:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def pull(slug: str, dest) -> bool:
|
|
134
|
+
"""Download the latest version of the relay dataset and unpack its tar so that `dest`
|
|
135
|
+
holds what was pushed. Returns True when something landed; False (and nothing touched)
|
|
136
|
+
when the relay is unavailable, the dataset does not exist, or the download failed."""
|
|
137
|
+
if not available() or not exists(slug):
|
|
138
|
+
return False
|
|
139
|
+
dest = Path(dest)
|
|
140
|
+
tmp = Path(tempfile.mkdtemp(prefix="relay-pull-"))
|
|
141
|
+
cmd = kaggle_cmd() or ["kaggle"]
|
|
142
|
+
r = subprocess.run(cmd + ["datasets", "download", "-d", dataset_id(slug), "-p", str(tmp), "--unzip", "--force", "-q"],
|
|
143
|
+
capture_output=True, text=True)
|
|
144
|
+
tar_path = next(iter(tmp.rglob(ARCHIVE)), None)
|
|
145
|
+
if r.returncode != 0 or tar_path is None:
|
|
146
|
+
landed = sorted(str(q.relative_to(tmp)) for q in tmp.rglob("*") if q.is_file())[:8]
|
|
147
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
148
|
+
print(f"[relay] pull found nothing for {slug}: download rc={r.returncode}, "
|
|
149
|
+
f"archive {'absent' if tar_path is None else 'present'}, files landed {landed}, "
|
|
150
|
+
f"output {(r.stdout + r.stderr).strip()[-200:]!r}")
|
|
151
|
+
return False
|
|
152
|
+
with tarfile.open(tar_path) as tar:
|
|
153
|
+
members = tar.getmembers()
|
|
154
|
+
root = members[0].name.split("/")[0] if members else None
|
|
155
|
+
tar.extractall(tmp / "x", filter="data")
|
|
156
|
+
unpacked = tmp / "x" / root if root else None
|
|
157
|
+
if unpacked is None or not unpacked.exists():
|
|
158
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
159
|
+
return False
|
|
160
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
if dest.exists():
|
|
162
|
+
shutil.rmtree(dest)
|
|
163
|
+
shutil.move(str(unpacked), str(dest))
|
|
164
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
165
|
+
print(f"[relay] pulled {slug} -> {dest}")
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def slug_for(name: str) -> str:
|
|
170
|
+
"""A dataset slug from a run or directory name: lowercase, hyphens, under Kaggle's limits."""
|
|
171
|
+
s = "".join(ch if ch.isalnum() else "-" for ch in name.lower()).strip("-")
|
|
172
|
+
while "--" in s:
|
|
173
|
+
s = s.replace("--", "-")
|
|
174
|
+
return ("quantum-cortex-relay-" + s)[:50].rstrip("-")
|
cortex_eval/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""cortex_eval — evaluation harnesses for the C2 ablation protocol (ADR-006).
|
|
2
|
+
|
|
3
|
+
Slice B: MQAR (associative recall, by difficulty tiers) and the serial-position
|
|
4
|
+
curve (primacy/recency). Evaluation-only; validated on the control before any
|
|
5
|
+
ablation run. The founder's principle: never measure at a single point that may
|
|
6
|
+
be flat — sweep a scale and read the curve.
|
|
7
|
+
"""
|
|
8
|
+
from cortex_eval.mqar import (
|
|
9
|
+
MQARTier, MQARResult, make_batch, standard_curriculum, accuracy, SEP,
|
|
10
|
+
)
|
|
11
|
+
from cortex_eval.serial_position import (
|
|
12
|
+
SerialPositionTask, SerialPositionResult, aggregate_by_position,
|
|
13
|
+
make_batch as make_serial_batch, PROBE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MQARTier", "MQARResult", "make_batch", "standard_curriculum", "accuracy", "SEP",
|
|
18
|
+
"SerialPositionTask", "SerialPositionResult", "aggregate_by_position",
|
|
19
|
+
"make_serial_batch", "PROBE",
|
|
20
|
+
]
|