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_c2b/lm_bridge.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""cortex_c2b.lm_bridge -- the journal in the decode loop (ADR-008; REPRISE step 3).
|
|
2
|
+
|
|
3
|
+
Until 2026-09-12 the journal was measured as an ORGAN: a hash-seeded cue encoder
|
|
4
|
+
and a reference skill probe stood in for the model. This module wires the organ
|
|
5
|
+
into the language model itself, under the nine invariants validated by the
|
|
6
|
+
founder before any code was written (ADR-008):
|
|
7
|
+
|
|
8
|
+
1. N1 intact at step zero: the reader's output projection is zero-initialised,
|
|
9
|
+
so a model with the journal on produces N1's logits at initialisation,
|
|
10
|
+
window or no window; a parent checkpoint loads its weights unchanged.
|
|
11
|
+
2. One read path, the organ's: cues go to `JournalPath.retrieve`; nothing is
|
|
12
|
+
duplicated on the model side, nothing scans.
|
|
13
|
+
3. The journal enters as CONTENT, through a zero-init cross attention over
|
|
14
|
+
the retrieved bytes embedded with the model's own byte embeddings -- not
|
|
15
|
+
as prompt tokens. Journal OFF = the reader absent = N1 exactly.
|
|
16
|
+
4. The router decides the read, on the floor: the reader's contribution is
|
|
17
|
+
multiplied by the path 4 gate; with no score the gate is zero.
|
|
18
|
+
5. The cue encoder is learned (a projection of the mean pooled residual
|
|
19
|
+
stream to a unit vector), trained by contrast: the cue of a query must be
|
|
20
|
+
nearest to the cue of its own statement among the batch. One encoder for
|
|
21
|
+
writing and reading; the DG/CA3/CA1 write gate is unchanged.
|
|
22
|
+
6. Organ use is taught, not assumed: `cortex_c2b.organ_use` builds the
|
|
23
|
+
curriculum; the target of every example is computed from what the journal
|
|
24
|
+
actually returned (cite what was shown, else abstain).
|
|
25
|
+
7. Two processes, persistence declared: session A writes, session B reopens
|
|
26
|
+
the sealed journal and the checkpoint from the disk alone.
|
|
27
|
+
8. A failure is attributable: retrieval hit (the encoder), attention mass on
|
|
28
|
+
the retrieved bytes (the reader), attribute exactness given a valid
|
|
29
|
+
citation (the answer) are reported next to the verdict.
|
|
30
|
+
9. Everything is a flag, a ledger record, and a CPU smoke test.
|
|
31
|
+
|
|
32
|
+
The contract of an episodic answer (ADR-008, point d, decided 2026-09-12):
|
|
33
|
+
`<EPI>` marks an episodic query; the answer is `<CITE>` + a one byte LOCAL
|
|
34
|
+
LABEL of the retrieved episode (the harness maps it back to the pointer and
|
|
35
|
+
verifies it) + `<ANS>` + the attribute bytes + newline, or `<UNKNOWN>` +
|
|
36
|
+
newline. Four of the eight reserved oracle tokens (NOW-1) are taken; the
|
|
37
|
+
vocabulary size does not change, so the N1 checkpoint loads as is.
|
|
38
|
+
|
|
39
|
+
Pure torch over the numpy organ. Written from scratch (our filon).
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
|
|
45
|
+
import numpy as np
|
|
46
|
+
import torch
|
|
47
|
+
import torch.nn as nn
|
|
48
|
+
import torch.nn.functional as F
|
|
49
|
+
|
|
50
|
+
from cortex_c2b import Journal, CUE_DIM
|
|
51
|
+
from cortex_c2b.write_path import WritePath, WriteReport
|
|
52
|
+
from cortex_c2b.read_path import JournalPath
|
|
53
|
+
|
|
54
|
+
# ---- the contract tokens: 4 of the 8 reserved oracle tokens (ids 256..263) ----
|
|
55
|
+
TOK_EPI, TOK_CITE, TOK_ANS, TOK_UNKNOWN = 256, 257, 258, 259
|
|
56
|
+
CONTRACT_TOKENS = {"EPI": TOK_EPI, "CITE": TOK_CITE, "ANS": TOK_ANS, "UNKNOWN": TOK_UNKNOWN}
|
|
57
|
+
LABELS = b"ABCDEFGH" # local labels of the retrieved episodes, at most 8 per read
|
|
58
|
+
NEWLINE = 10
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------- #
|
|
62
|
+
# The cue encoder -- learned, one for writing and reading (invariant 5) #
|
|
63
|
+
# ---------------------------------------------------------------------------- #
|
|
64
|
+
class CueEncoder(nn.Module):
|
|
65
|
+
"""Mean pooled residual stream -> linear -> unit vector of `cue_dim`."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, n_embd: int, cue_dim: int = CUE_DIM):
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.proj = nn.Linear(n_embd, cue_dim, bias=False)
|
|
70
|
+
|
|
71
|
+
def forward(self, h: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
|
72
|
+
if mask is None:
|
|
73
|
+
pooled = h.mean(dim=1)
|
|
74
|
+
else:
|
|
75
|
+
m = mask.to(h.dtype).unsqueeze(-1)
|
|
76
|
+
pooled = (h * m).sum(dim=1) / m.sum(dim=1).clamp(min=1.0)
|
|
77
|
+
return F.normalize(self.proj(pooled), dim=-1, eps=1e-6)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def contrastive_loss(query_cues: torch.Tensor, statement_cues: torch.Tensor,
|
|
81
|
+
temperature: float = 0.1) -> torch.Tensor:
|
|
82
|
+
"""InfoNCE in both directions: row i of the queries must pick row i of the
|
|
83
|
+
statements among the batch (and back). The signal that teaches the encoder
|
|
84
|
+
to put a question and its own episode at the same address."""
|
|
85
|
+
logits = query_cues @ statement_cues.t() / temperature
|
|
86
|
+
target = torch.arange(logits.size(0), device=logits.device)
|
|
87
|
+
return 0.5 * (F.cross_entropy(logits, target) + F.cross_entropy(logits.t(), target))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------- #
|
|
91
|
+
# The reader -- zero-init cross attention over the read window (invariant 3) #
|
|
92
|
+
# ---------------------------------------------------------------------------- #
|
|
93
|
+
class JournalReader(nn.Module):
|
|
94
|
+
"""Queries from the residual stream, keys and values from the embedded bytes
|
|
95
|
+
of the read window (plus a learned null slot: "nothing to read"). The output
|
|
96
|
+
projection starts at zero, so the model begins as N1 and learns to read.
|
|
97
|
+
`forward` also returns the attention mass on the real bytes (excluding the
|
|
98
|
+
null slot): the reader's diagnostic (invariant 8)."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, n_embd: int, n_head: int, window_len: int):
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.h = n_head
|
|
103
|
+
self.ln_x = nn.LayerNorm(n_embd)
|
|
104
|
+
self.ln_w = nn.LayerNorm(n_embd)
|
|
105
|
+
self.q = nn.Linear(n_embd, n_embd, bias=False)
|
|
106
|
+
self.kv = nn.Linear(n_embd, 2 * n_embd, bias=False)
|
|
107
|
+
self.out = nn.Linear(n_embd, n_embd, bias=False)
|
|
108
|
+
self.null = nn.Parameter(torch.zeros(1, 1, n_embd))
|
|
109
|
+
self.wpe = nn.Embedding(window_len, n_embd) # order within the window
|
|
110
|
+
nn.init.zeros_(self.out.weight) # the no-op law
|
|
111
|
+
self.scale = (n_embd // n_head) ** -0.5
|
|
112
|
+
|
|
113
|
+
def forward(self, x: torch.Tensor, window_emb: torch.Tensor, window_mask: torch.Tensor):
|
|
114
|
+
b, t, c = x.shape
|
|
115
|
+
r = window_emb.size(1)
|
|
116
|
+
pos = torch.arange(r, device=x.device)
|
|
117
|
+
w = torch.cat([self.null.expand(b, 1, c), window_emb + self.wpe(pos)[None]], dim=1) # (b, r+1, c)
|
|
118
|
+
m = torch.cat([torch.ones(b, 1, dtype=torch.bool, device=x.device), window_mask.bool()], dim=1)
|
|
119
|
+
hq = self.q(self.ln_x(x)).view(b, t, self.h, c // self.h).transpose(1, 2) # (b,h,t,d)
|
|
120
|
+
k, v = self.kv(self.ln_w(w)).split(c, dim=2)
|
|
121
|
+
k = k.view(b, r + 1, self.h, c // self.h).transpose(1, 2)
|
|
122
|
+
v = v.view(b, r + 1, self.h, c // self.h).transpose(1, 2)
|
|
123
|
+
att = (hq @ k.transpose(-2, -1)) * self.scale
|
|
124
|
+
att = att.masked_fill(~m[:, None, None, :], float("-inf"))
|
|
125
|
+
p = torch.softmax(att, dim=-1)
|
|
126
|
+
read = (p @ v).transpose(1, 2).contiguous().view(b, t, c)
|
|
127
|
+
mass = p[..., 1:].sum(dim=-1).mean() # attention on real bytes, null slot excluded
|
|
128
|
+
self.last_attended = read # RES-21: what was read, before the zero init projection
|
|
129
|
+
self.last_attn = p.detach() # (b, h, t, r + 1): the pointer policy reads it at decode
|
|
130
|
+
return self.out(read), mass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class MatchHead(nn.Module):
|
|
134
|
+
"""RES-21 (ADR-008, amendment of 2026-09-14): a small head that answers ONE question at
|
|
135
|
+
the decision position, "is the queried entity's episode in the window?", from what
|
|
136
|
+
the model had before reading (the residual stream at the journal block) and what it
|
|
137
|
+
read (the reader's attended values, before the zero init projection). Trained
|
|
138
|
+
directly on that label, which the harness knows exactly for every curriculum example
|
|
139
|
+
(the own pointer among the window's labels, never the ground truth alone), and
|
|
140
|
+
coupled to the decision token's logits: + coupling * m on <CITE>, - coupling * m on
|
|
141
|
+
<UNKNOWN>. The last layer starts at zero, so the model begins as its parent (m = 0,
|
|
142
|
+
no bias) and learns the comparison from there."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, n_embd: int):
|
|
145
|
+
super().__init__()
|
|
146
|
+
self.ln_pre = nn.LayerNorm(n_embd)
|
|
147
|
+
self.ln_read = nn.LayerNorm(n_embd)
|
|
148
|
+
self.up = nn.Linear(2 * n_embd, n_embd)
|
|
149
|
+
self.out = nn.Linear(n_embd, 1)
|
|
150
|
+
nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias) # the no-op law
|
|
151
|
+
|
|
152
|
+
def forward(self, pre: torch.Tensor, attended: torch.Tensor) -> torch.Tensor:
|
|
153
|
+
z = torch.cat([self.ln_pre(pre), self.ln_read(attended)], dim=-1)
|
|
154
|
+
return self.out(torch.nn.functional.gelu(self.up(z))).squeeze(-1) # (b,) a logit: present > 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------- #
|
|
158
|
+
# The router's gate on the read (invariant 4) #
|
|
159
|
+
# ---------------------------------------------------------------------------- #
|
|
160
|
+
PATH_JOURNAL = 4 # the journal is path 4 of the RES-18 router (ADR-007)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def journal_gate(route: str, decision=None) -> float:
|
|
164
|
+
"""The multiplier on the reader's contribution. `pinned_on` / `pinned_off`
|
|
165
|
+
are the two arms of the protocol (declared, recorded); `router` takes the
|
|
166
|
+
path 4 weight of a RouteDecision -- a hard decision for another path, or no
|
|
167
|
+
decision at all, gives zero: the floor guarantee at the model scale."""
|
|
168
|
+
if route == "pinned_on":
|
|
169
|
+
return 1.0
|
|
170
|
+
if route == "pinned_off":
|
|
171
|
+
return 0.0
|
|
172
|
+
if route != "router":
|
|
173
|
+
raise ValueError(f"unknown journal_route {route!r}")
|
|
174
|
+
if decision is None:
|
|
175
|
+
return 0.0
|
|
176
|
+
if decision.weights is not None:
|
|
177
|
+
return float(decision.weights[PATH_JOURNAL]) if len(decision.weights) > PATH_JOURNAL else 0.0
|
|
178
|
+
return float(decision.confidence) if decision.path == PATH_JOURNAL else 0.0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------- #
|
|
182
|
+
# The read window -- labelled, budgeted, in a seeded random order #
|
|
183
|
+
# ---------------------------------------------------------------------------- #
|
|
184
|
+
def build_read_window(items: list[tuple[str, bytes]], budget: int, rng: np.random.Generator,
|
|
185
|
+
marks: list[float] | None = None) -> tuple[bytes, dict[int, str]]:
|
|
186
|
+
"""`items` = (pointer, payload) as retrieved. Returns the window bytes
|
|
187
|
+
(`A:<payload>\\n` per item, order shuffled with `rng` so a position is never a
|
|
188
|
+
shortcut, each payload cut to its share of `budget`) and the label byte ->
|
|
189
|
+
pointer map the harness uses to verify a citation.
|
|
190
|
+
`marks` (RES-23, the familiarity mark, v9): one number per item, the organ's
|
|
191
|
+
own similarity for that line; the prefix becomes `A(0.97):` (two decimals,
|
|
192
|
+
clamped at zero) and each payload gives six more bytes to it inside the same
|
|
193
|
+
budget. None keeps the window byte for byte as every earlier run wrote it."""
|
|
194
|
+
items = list(items)[: len(LABELS)]
|
|
195
|
+
if not items:
|
|
196
|
+
return b"", {}
|
|
197
|
+
if marks is not None and len(marks) != len(items):
|
|
198
|
+
raise ValueError("one mark per item")
|
|
199
|
+
order = rng.permutation(len(items))
|
|
200
|
+
per = max(1, budget // len(items) - (9 if marks is not None else 3)) # 'X:' or 'X(0.97):', and the newline
|
|
201
|
+
out, labels = [], {}
|
|
202
|
+
for slot, i in enumerate(order):
|
|
203
|
+
pointer, payload = items[i]
|
|
204
|
+
label = LABELS[slot]
|
|
205
|
+
labels[label] = pointer
|
|
206
|
+
prefix = bytes([label]) + (f"({max(0.0, min(1.0, float(marks[i]))):.2f})".encode("ascii") if marks is not None else b"") + b":"
|
|
207
|
+
out.append(prefix + payload[:per] + b"\n")
|
|
208
|
+
return b"".join(out), labels
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def encode_query(text: str) -> list[int]:
|
|
212
|
+
return [TOK_EPI] + list(text.encode("utf-8"))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def encode_target(label: int | None, attr: str | None) -> list[int]:
|
|
216
|
+
"""The contract: cite a label and answer, or abstain."""
|
|
217
|
+
if label is None:
|
|
218
|
+
return [TOK_UNKNOWN, NEWLINE]
|
|
219
|
+
return [TOK_CITE, label, TOK_ANS] + list(attr.encode("utf-8")) + [NEWLINE]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def parse_contract(tokens: list[int]) -> tuple[str, int | None, str | None]:
|
|
223
|
+
"""('unknown'|'cite'|'malformed', label, attribute). A malformed answer is a
|
|
224
|
+
claim without the contract's shape -- counted against the model, never for it."""
|
|
225
|
+
toks = list(tokens)
|
|
226
|
+
if toks and toks[0] == TOK_UNKNOWN:
|
|
227
|
+
return "unknown", None, None
|
|
228
|
+
if len(toks) >= 3 and toks[0] == TOK_CITE and toks[2] == TOK_ANS and toks[1] in LABELS:
|
|
229
|
+
body = toks[3:]
|
|
230
|
+
if NEWLINE in body:
|
|
231
|
+
body = body[: body.index(NEWLINE)]
|
|
232
|
+
try:
|
|
233
|
+
return "cite", toks[1], bytes(b for b in body if b < 256).decode("utf-8")
|
|
234
|
+
except UnicodeDecodeError:
|
|
235
|
+
return "malformed", toks[1], None
|
|
236
|
+
return "malformed", None, None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------- #
|
|
240
|
+
# The bridge -- the torch model above, the numpy organ below (invariant 2) #
|
|
241
|
+
# ---------------------------------------------------------------------------- #
|
|
242
|
+
@dataclass
|
|
243
|
+
class ReadResult:
|
|
244
|
+
window: bytes
|
|
245
|
+
labels: dict[int, str] # label byte -> pointer
|
|
246
|
+
pointers: list[str] # what the organ returned, in retrieval order
|
|
247
|
+
scores: list[float]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class JournalBridge:
|
|
251
|
+
"""Holds the organ (journal, write path, router path) and talks to the model
|
|
252
|
+
through two calls it must expose: `journal_hidden(idx, mask)` (the residual
|
|
253
|
+
stream at the journal block, before the reader) and `cue_encoder`.
|
|
254
|
+
|
|
255
|
+
The model never sees a pointer or a payload except through the read window;
|
|
256
|
+
the organ never sees a tensor except the cue."""
|
|
257
|
+
|
|
258
|
+
def __init__(self, model, journal: Journal, write_path: WritePath | None = None,
|
|
259
|
+
journal_path: JournalPath | None = None, k: int = 4, budget_bytes: int = 256,
|
|
260
|
+
seed: int = 0, shuffle_seed: int | None = None, device=None, mark: bool = False):
|
|
261
|
+
"""`seed` is the scope's separation seed (DG) and the index seed -- it must
|
|
262
|
+
be the same at every open of a journal (Decision 8: a journal written under
|
|
263
|
+
another separation is refused). `shuffle_seed` only drives the order of
|
|
264
|
+
the read window and may differ between sessions."""
|
|
265
|
+
self.model = model
|
|
266
|
+
self.j = journal
|
|
267
|
+
if model.cue_encoder is None or model.cue_encoder.proj.out_features != journal.cue_dim:
|
|
268
|
+
raise ValueError("the model's cue encoder must produce the journal's cue dimension "
|
|
269
|
+
f"({journal.cue_dim}); set journal_cue_dim accordingly")
|
|
270
|
+
self.wp = write_path or WritePath(journal, seed=seed)
|
|
271
|
+
self.jp = journal_path or JournalPath(journal, seed=seed)
|
|
272
|
+
self.k, self.budget = k, budget_bytes
|
|
273
|
+
self.mark = mark # RES-23: the organ's similarity in every window line
|
|
274
|
+
self.rng = np.random.default_rng(seed if shuffle_seed is None else shuffle_seed)
|
|
275
|
+
self.device = device or next(model.parameters()).device
|
|
276
|
+
self.pointer_of: dict[str, str] = {} # entity -> pointer, for the curriculum's targets
|
|
277
|
+
|
|
278
|
+
# ---- tensors ----------------------------------------------------------------
|
|
279
|
+
def tokens(self, sequences: list[list[int]]) -> tuple[torch.Tensor, torch.Tensor]:
|
|
280
|
+
n = max(len(s) for s in sequences)
|
|
281
|
+
idx = torch.zeros(len(sequences), n, dtype=torch.long)
|
|
282
|
+
mask = torch.zeros(len(sequences), n, dtype=torch.bool)
|
|
283
|
+
for i, s in enumerate(sequences):
|
|
284
|
+
idx[i, : len(s)] = torch.tensor(s, dtype=torch.long); mask[i, : len(s)] = True
|
|
285
|
+
return idx.to(self.device), mask.to(self.device)
|
|
286
|
+
|
|
287
|
+
def cue_tensor(self, sequences: list[list[int]]) -> torch.Tensor:
|
|
288
|
+
"""Cues with gradient (for the contrastive loss)."""
|
|
289
|
+
idx, mask = self.tokens(sequences)
|
|
290
|
+
h = self.model.journal_hidden(idx)
|
|
291
|
+
return self.model.cue_encoder(h, mask)
|
|
292
|
+
|
|
293
|
+
def cues(self, sequences: list[list[int]]) -> np.ndarray:
|
|
294
|
+
"""Cues for the organ (no gradient)."""
|
|
295
|
+
with torch.no_grad():
|
|
296
|
+
return self.cue_tensor(sequences).float().cpu().numpy()
|
|
297
|
+
|
|
298
|
+
# ---- the organ ---------------------------------------------------------------
|
|
299
|
+
def write(self, text: str, schema: str, now: float, entity: str | None = None) -> WriteReport:
|
|
300
|
+
cue = self.cues([list(text.encode("utf-8"))])[0]
|
|
301
|
+
rep = self.wp.write(cue, text.encode("utf-8"), schema, now=now)
|
|
302
|
+
if rep.admitted:
|
|
303
|
+
self.jp.on_write(rep.entry)
|
|
304
|
+
if entity is not None:
|
|
305
|
+
self.pointer_of[entity] = rep.entry.pointer
|
|
306
|
+
return rep
|
|
307
|
+
|
|
308
|
+
def read(self, cue: np.ndarray) -> ReadResult:
|
|
309
|
+
hits = self.jp.retrieve(cue, k=self.k)
|
|
310
|
+
items = [(e.pointer, payload) for e, payload, _ in hits]
|
|
311
|
+
scores = [float(s) for _, _, s in hits]
|
|
312
|
+
window, labels = build_read_window(items, self.budget, self.rng, marks=scores if self.mark else None)
|
|
313
|
+
return ReadResult(window=window, labels=labels, pointers=[p for p, _ in items], scores=scores)
|
|
314
|
+
|
|
315
|
+
def marks_for(self, cue: np.ndarray, pointers: list[str]) -> list[float]:
|
|
316
|
+
"""The organ's similarity between `cue` and each episode, whatever route put
|
|
317
|
+
the line in a window (a plain read, a paired negative, a forced bootstrap,
|
|
318
|
+
a shape filler): the same computation as the index's scoring."""
|
|
319
|
+
return [self.jp.cue_similarity(cue, p) for p in pointers]
|
|
320
|
+
|
|
321
|
+
def window_for(self, cue: np.ndarray, items: list[tuple[str, bytes]], rng: np.random.Generator
|
|
322
|
+
) -> tuple[bytes, dict[int, str]]:
|
|
323
|
+
"""A window over arbitrary items, marked with the organ's similarities when
|
|
324
|
+
the bridge marks, unmarked otherwise; the curriculum's windows go through here."""
|
|
325
|
+
marks = self.marks_for(cue, [p for p, _ in items]) if self.mark else None
|
|
326
|
+
return build_read_window(items, self.budget, rng, marks=marks)
|
|
327
|
+
|
|
328
|
+
def window_tensors(self, windows: list[bytes], window_len: int) -> tuple[torch.Tensor, torch.Tensor]:
|
|
329
|
+
"""Byte windows -> (tokens, mask), each cut to `window_len`; an empty
|
|
330
|
+
window is one masked position (the reader keeps its null slot)."""
|
|
331
|
+
r = max(1, min(window_len, max((len(w) for w in windows), default=1)))
|
|
332
|
+
tok = torch.zeros(len(windows), r, dtype=torch.long)
|
|
333
|
+
mask = torch.zeros(len(windows), r, dtype=torch.bool)
|
|
334
|
+
for i, w in enumerate(windows):
|
|
335
|
+
w = w[:r]
|
|
336
|
+
if w:
|
|
337
|
+
tok[i, : len(w)] = torch.tensor(list(w), dtype=torch.long); mask[i, : len(w)] = True
|
|
338
|
+
return tok.to(self.device), mask.to(self.device)
|
cortex_c2b/organ_use.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""cortex_c2b.organ_use -- teaching the model to use its own organ (NOW-8; ADR-008 invariant 6).
|
|
2
|
+
|
|
3
|
+
The rule that combines abstention and citation, decided by the founder on
|
|
4
|
+
2026-09-12: **the target of every example is computed from what the journal
|
|
5
|
+
actually returned, never from the ground truth alone.** If the episode that
|
|
6
|
+
holds the answer is among the retrieved ones, the target cites its local label
|
|
7
|
+
and gives the attribute; otherwise the target is `<UNKNOWN>`, even though the
|
|
8
|
+
fact exists in the journal. The model is never rewarded for citing what it was
|
|
9
|
+
not shown, so no invented citation enters the signal; while the cue encoder is
|
|
10
|
+
poor the model learns silence first (one token), and as the contrastive loss
|
|
11
|
+
improves retrieval, citations replace abstentions by themselves.
|
|
12
|
+
|
|
13
|
+
Three mitigations, all declared and hashed in the run's configuration:
|
|
14
|
+
- an asymmetric weight on the decision token of an abstention target
|
|
15
|
+
(answering instead of abstaining costs more than the reverse);
|
|
16
|
+
- negative examples (entities never planted) from the first step;
|
|
17
|
+
- a bootstrap for the cold start: on a missed retrieval, with a declared
|
|
18
|
+
probability that decays to zero, the right episode is FORCED into the
|
|
19
|
+
window and the example is marked `forced` -- counted in the diagnostics,
|
|
20
|
+
never as recall.
|
|
21
|
+
|
|
22
|
+
Decontamination (invariant 5): the training facts come from the same generator
|
|
23
|
+
family as the frozen protocol, with disjoint seeds, and every entity name is
|
|
24
|
+
checked against the protocol's facts and its negative control; a collision is
|
|
25
|
+
removed and reported. Written from scratch (our filon).
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
from cortex_c2b.hm_protocol import Fact, generate_facts, N_FACTS, N_NEGCTRL
|
|
34
|
+
from cortex_c2b.lm_bridge import (JournalBridge, encode_query, encode_target, build_read_window,
|
|
35
|
+
TOK_UNKNOWN)
|
|
36
|
+
|
|
37
|
+
EVAL_SEEDS = (0, 10_000) # the frozen protocol's facts and its negative control
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------- #
|
|
41
|
+
# Facts: disjoint seeds, checked entities #
|
|
42
|
+
# ---------------------------------------------------------------------------- #
|
|
43
|
+
def decontaminate(train: list[Fact], eval_facts: list[Fact]) -> tuple[list[Fact], list[str]]:
|
|
44
|
+
"""Remove every training fact whose entity appears in the evaluation facts."""
|
|
45
|
+
banned = {f.entity for f in eval_facts}
|
|
46
|
+
kept = [f for f in train if f.entity not in banned]
|
|
47
|
+
removed = [f.entity for f in train if f.entity in banned]
|
|
48
|
+
return kept, removed
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def training_facts(n: int, seed: int = 100_000, n_negatives: int = 50) -> tuple[list[Fact], list[Fact], dict]:
|
|
52
|
+
"""Planted facts and never-planted negatives for training, both disjoint
|
|
53
|
+
from the frozen protocol's sets (seeds 0 and 10_000). Returns
|
|
54
|
+
(facts, negatives, report)."""
|
|
55
|
+
if seed in EVAL_SEEDS or seed + 1 in EVAL_SEEDS:
|
|
56
|
+
raise ValueError("training seeds must differ from the frozen protocol's seeds (0 and 10_000)")
|
|
57
|
+
eval_facts = [f for s in EVAL_SEEDS for f in generate_facts(N_FACTS if s == 0 else N_NEGCTRL, s)[0]]
|
|
58
|
+
facts, gen_hash = generate_facts(n, seed)
|
|
59
|
+
negs, _ = generate_facts(n_negatives, seed + 1)
|
|
60
|
+
facts, removed_f = decontaminate(facts, eval_facts)
|
|
61
|
+
negs, removed_n = decontaminate(negs, eval_facts + facts)
|
|
62
|
+
report = {"generator_hash": gen_hash, "seed": seed, "n_facts": len(facts), "n_negatives": len(negs),
|
|
63
|
+
"removed_collisions": removed_f + removed_n}
|
|
64
|
+
return facts, negs, report
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------- #
|
|
68
|
+
# The pool: facts planted through the normal gated path under the CURRENT encoder
|
|
69
|
+
# ---------------------------------------------------------------------------- #
|
|
70
|
+
def plant_pool(bridge: JournalBridge, facts: list[Fact], now: float = 0.0) -> dict:
|
|
71
|
+
"""Write every fact as an episode (statement bytes, schema, cue from the
|
|
72
|
+
model). Returns admission statistics; `bridge.pointer_of` maps entities to
|
|
73
|
+
their pointers, which is how a target knows whether the right episode was
|
|
74
|
+
among the retrieved ones."""
|
|
75
|
+
admitted = 0
|
|
76
|
+
for i, f in enumerate(facts):
|
|
77
|
+
rep = bridge.write(f.statement, f.schema, now=now + i, entity=f.entity)
|
|
78
|
+
admitted += int(rep.admitted)
|
|
79
|
+
return {"planted": len(facts), "admitted": admitted}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------- #
|
|
83
|
+
# Examples #
|
|
84
|
+
# ---------------------------------------------------------------------------- #
|
|
85
|
+
@dataclass
|
|
86
|
+
class Example:
|
|
87
|
+
kind: str # answer | abstain | negative | forced
|
|
88
|
+
query: list[int] # <EPI> + question bytes
|
|
89
|
+
target: list[int] # the contract
|
|
90
|
+
window: bytes
|
|
91
|
+
labels: dict[int, str] = field(default_factory=dict)
|
|
92
|
+
retrieved_ok: bool = False # the right episode was among the retrieved (the encoder's success)
|
|
93
|
+
entity: str = ""
|
|
94
|
+
attr: str = ""
|
|
95
|
+
statement: list[int] = field(default_factory=list) # the episode's bytes, for the contrastive pair
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def tokens(self) -> list[int]:
|
|
99
|
+
return self.query + self.target
|
|
100
|
+
|
|
101
|
+
def weights(self, asym: float) -> list[float]:
|
|
102
|
+
"""Loss on the target only; the decision token of an abstention target
|
|
103
|
+
carries the asymmetric weight."""
|
|
104
|
+
w = [0.0] * len(self.query) + [1.0] * len(self.target)
|
|
105
|
+
if self.target and self.target[0] == TOK_UNKNOWN:
|
|
106
|
+
w[len(self.query)] = float(asym)
|
|
107
|
+
return w
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _label_of(labels: dict[int, str], pointer: str) -> int | None:
|
|
111
|
+
for label, p in labels.items():
|
|
112
|
+
if p == pointer:
|
|
113
|
+
return label
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def make_example(bridge: JournalBridge, fact: Fact, negative: bool, forced_frac: float,
|
|
118
|
+
rng: np.random.Generator, paraphrase: bool = False) -> Example:
|
|
119
|
+
"""One example, its target computed from the actual read (the rule above)."""
|
|
120
|
+
question = fact.paraphrase if paraphrase else fact.query
|
|
121
|
+
query = encode_query(question)
|
|
122
|
+
cue = bridge.cues([query])[0]
|
|
123
|
+
read = bridge.read(cue)
|
|
124
|
+
statement = list(fact.statement.encode("utf-8"))
|
|
125
|
+
if negative:
|
|
126
|
+
return Example("negative", query, encode_target(None, None), read.window, read.labels,
|
|
127
|
+
False, fact.entity, fact.attr, statement)
|
|
128
|
+
pointer = bridge.pointer_of.get(fact.entity)
|
|
129
|
+
label = _label_of(read.labels, pointer) if pointer else None
|
|
130
|
+
if label is not None:
|
|
131
|
+
return Example("answer", query, encode_target(label, fact.attr), read.window, read.labels,
|
|
132
|
+
True, fact.entity, fact.attr, statement)
|
|
133
|
+
if pointer and forced_frac > 0 and rng.random() < forced_frac:
|
|
134
|
+
# the bootstrap: the right episode is forced into the window (replacing the last slot)
|
|
135
|
+
items = [(p, bridge.j.payloads.get(p)) for p in read.pointers][: max(0, bridge.k - 1)]
|
|
136
|
+
items.append((pointer, bridge.j.payloads.get(pointer)))
|
|
137
|
+
window, labels = bridge.window_for(cue, items, rng)
|
|
138
|
+
return Example("forced", query, encode_target(_label_of(labels, pointer), fact.attr), window, labels,
|
|
139
|
+
False, fact.entity, fact.attr, statement)
|
|
140
|
+
return Example("abstain", query, encode_target(None, None), read.window, read.labels,
|
|
141
|
+
False, fact.entity, fact.attr, statement)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def make_paired_negative(bridge: JournalBridge, fact: Fact, rng: np.random.Generator,
|
|
145
|
+
paraphrase: bool = False, keep_shape: bool = False) -> Example:
|
|
146
|
+
"""The minimal pair (v5 lever, run 6f10e8cbfebf): the SAME planted fact, its own episode
|
|
147
|
+
WITHHELD from the window; target abstain. Four runs showed the decision to cite or abstain
|
|
148
|
+
following the share of negatives seen in training (a class prior) far more than the
|
|
149
|
+
window's content; with pairs the prior earns nothing, and the only way down the loss is
|
|
150
|
+
to check whether the queried entity is in the read.
|
|
151
|
+
|
|
152
|
+
`keep_shape=False` is the pair AS RUN in v5 (`db028e6a4262`, INVALID): the own pointer
|
|
153
|
+
removed from the k retrieved, the window built from what is left, so it has k - 1 lines
|
|
154
|
+
whenever the episode had been retrieved (over 90 % of the time late in the run). That is
|
|
155
|
+
a shape cue, cheaper than a comparison, and absent from the frozen protocol's windows,
|
|
156
|
+
which always have k lines. The pair was not minimal; kept as written so the v5 run stays
|
|
157
|
+
reproducible under its hash. `keep_shape=True` is the v6 lever: k + 1 retrieved, the own
|
|
158
|
+
dropped, k lines kept, so the pair's two halves have the same shape and differ only by the
|
|
159
|
+
episode's presence."""
|
|
160
|
+
question = fact.paraphrase if paraphrase else fact.query
|
|
161
|
+
query = encode_query(question)
|
|
162
|
+
cue = bridge.cues([query])[0]
|
|
163
|
+
own = bridge.pointer_of.get(fact.entity)
|
|
164
|
+
items = withheld_items(bridge, cue, own, rng, keep_shape=keep_shape)
|
|
165
|
+
window, labels = bridge.window_for(cue, items, rng)
|
|
166
|
+
return Example("negative", query, encode_target(None, None), window, labels, False,
|
|
167
|
+
fact.entity, fact.attr, list(fact.statement.encode("utf-8")))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def withheld_items(bridge: JournalBridge, cue: np.ndarray, own: str | None, rng: np.random.Generator,
|
|
171
|
+
keep_shape: bool) -> list[tuple[str, bytes]]:
|
|
172
|
+
"""The window items of a paired negative. `keep_shape=False`: the read's items minus the own
|
|
173
|
+
pointer, one line fewer than the read whenever the own was retrieved (v5 as run). `keep_shape=True`:
|
|
174
|
+
the same number of lines as the read would have shown: the k + 1 th candidate fills the slot when the
|
|
175
|
+
index returns one, else another planted episode drawn at random (the index is a locality sensitive
|
|
176
|
+
hash and may return fewer than k candidates, so the fill is by construction, not by luck)."""
|
|
177
|
+
if not keep_shape:
|
|
178
|
+
read = bridge.read(cue)
|
|
179
|
+
return [(p, bridge.j.payloads.get(p)) for p in read.pointers if p != own]
|
|
180
|
+
hits = bridge.jp.retrieve(cue, k=bridge.k + 1)
|
|
181
|
+
top = [(e.pointer, payload) for e, payload, _ in hits]
|
|
182
|
+
shown = min(len(top), bridge.k) # what a plain read shows
|
|
183
|
+
items = [(p, pl) for p, pl in top if p != own][: bridge.k]
|
|
184
|
+
if len(items) < shown: # the own was shown and no k + 1 th candidate exists
|
|
185
|
+
seen = {p for p, _ in top}
|
|
186
|
+
others = sorted(p for p in bridge.pointer_of.values() if p != own and p not in seen)
|
|
187
|
+
if others:
|
|
188
|
+
p = others[int(rng.integers(len(others)))]
|
|
189
|
+
items.append((p, bridge.j.payloads.get(p)))
|
|
190
|
+
return items
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def make_batch(bridge: JournalBridge, facts: list[Fact], negatives: list[Fact], n: int,
|
|
194
|
+
rng: np.random.Generator, negatives_frac: float = 0.25, forced_frac: float = 0.5,
|
|
195
|
+
paraphrase_frac: float = 0.5, paired: bool = False, keep_shape: bool = False) -> list[Example]:
|
|
196
|
+
"""`paired`: negatives are minimal pairs of the planted facts (the episode withheld)
|
|
197
|
+
instead of separate never planted entities; `keep_shape`: the pair keeps k lines (v6)."""
|
|
198
|
+
out = []
|
|
199
|
+
for _ in range(n):
|
|
200
|
+
if rng.random() < negatives_frac and (paired or negatives):
|
|
201
|
+
if paired:
|
|
202
|
+
f = facts[int(rng.integers(len(facts)))]
|
|
203
|
+
out.append(make_paired_negative(bridge, f, rng, paraphrase=rng.random() < paraphrase_frac,
|
|
204
|
+
keep_shape=keep_shape))
|
|
205
|
+
else:
|
|
206
|
+
f = negatives[int(rng.integers(len(negatives)))]
|
|
207
|
+
out.append(make_example(bridge, f, True, 0.0, rng, paraphrase=rng.random() < paraphrase_frac))
|
|
208
|
+
else:
|
|
209
|
+
f = facts[int(rng.integers(len(facts)))]
|
|
210
|
+
out.append(make_example(bridge, f, False, forced_frac, rng, paraphrase=rng.random() < paraphrase_frac))
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def own_in_window(e: Example) -> bool:
|
|
215
|
+
"""RES-21's label: the queried entity's own episode is among the window's labels. Computed
|
|
216
|
+
from what the journal returned (the pointer is the content hash of the statement's bytes),
|
|
217
|
+
never from the ground truth alone: a positive whose episode was not retrieved is a 0."""
|
|
218
|
+
from cortex_c2b import content_hash
|
|
219
|
+
return content_hash(bytes(e.statement)) in set(e.labels.values())
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def match_tensors(examples: list[Example], device):
|
|
223
|
+
"""(targets (b,) float 0/1, decision positions (b,) long): the position at which the
|
|
224
|
+
decision token is predicted is the last token of the query."""
|
|
225
|
+
import torch
|
|
226
|
+
t = torch.tensor([1.0 if own_in_window(e) else 0.0 for e in examples], device=device)
|
|
227
|
+
d = torch.tensor([len(e.query) - 1 for e in examples], dtype=torch.long, device=device)
|
|
228
|
+
return t, d
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def batch_stats(examples: list[Example]) -> dict:
|
|
232
|
+
n = max(1, len(examples))
|
|
233
|
+
kinds = {k: sum(e.kind == k for e in examples) for k in ("answer", "abstain", "negative", "forced")}
|
|
234
|
+
planted = [e for e in examples if e.kind != "negative"]
|
|
235
|
+
return {**{f"n_{k}": v for k, v in kinds.items()},
|
|
236
|
+
"retrieval_hit": (sum(e.retrieved_ok for e in planted) / len(planted)) if planted else 0.0,
|
|
237
|
+
"abstain_target_rate": sum(e.target[0] == TOK_UNKNOWN for e in examples) / n}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def collate(bridge: JournalBridge, examples: list[Example], asym: float, window_len: int):
|
|
241
|
+
"""Tensors for one training step: (idx, targets, weights, window, window_mask,
|
|
242
|
+
query_sequences, statement_sequences). Targets are the next token; the
|
|
243
|
+
weight of a position is the weight of the token it predicts."""
|
|
244
|
+
import torch
|
|
245
|
+
seqs = [e.tokens for e in examples]
|
|
246
|
+
idx, _ = bridge.tokens([s[:-1] for s in seqs])
|
|
247
|
+
tgt, _ = bridge.tokens([s[1:] for s in seqs])
|
|
248
|
+
n = idx.size(1)
|
|
249
|
+
w = torch.zeros(len(examples), n)
|
|
250
|
+
for i, e in enumerate(examples):
|
|
251
|
+
ww = e.weights(asym)[1:] # aligned with the predicted token
|
|
252
|
+
w[i, : len(ww)] = torch.tensor(ww)
|
|
253
|
+
window, wmask = bridge.window_tensors([e.window for e in examples], window_len)
|
|
254
|
+
return idx, tgt, w.to(idx.device), window, wmask, [e.query for e in examples], [e.statement for e in examples]
|