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_c2/__init__.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""cortex_c2 — the multi-path C2 memory router (RES-18, ADR-006 D8), Slice A.
|
|
2
|
+
|
|
3
|
+
C2 is not one memory nor a choice between Hopfield and delta: it is a ROUTER of
|
|
4
|
+
memory paths on a guaranteed control floor. On any span, a memory is used only
|
|
5
|
+
where it beats the control; if all are below, the control is used — so C2 can
|
|
6
|
+
never degrade the model. (The ablation proved the floor is needed: the control
|
|
7
|
+
won 3 of 12 MQAR tiers.)
|
|
8
|
+
|
|
9
|
+
Slice A ships the FOUNDATION, retro-compatibility included from day one:
|
|
10
|
+
- a VERSIONED, stable Router interface (the contract every future version honours);
|
|
11
|
+
- RouterV1 = the upper-envelope oracle (picks, per span, the best of the known
|
|
12
|
+
path scores — the ceiling the learned router will chase: +21% vs control);
|
|
13
|
+
- version-tagged serialisation + a single-branch version dispatch (adding v2 is
|
|
14
|
+
one more branch, never a change to the existing one);
|
|
15
|
+
- a contract test (the guard) that any RouterVN must pass.
|
|
16
|
+
|
|
17
|
+
What is deliberately deferred (not by caution but by logic): ACTIVE migration
|
|
18
|
+
(transforming a v1 state into a v2 state) — impossible to write correctly before
|
|
19
|
+
v2 exists, but MADE POSSIBLE by the contract fixed here. The router-per-block
|
|
20
|
+
placement and soft/hard regime + breaker fallback are later slices (B, C).
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Sequence
|
|
27
|
+
|
|
28
|
+
# Path identifiers — the control floor is always path 0.
|
|
29
|
+
PATH_CONTROL = 0
|
|
30
|
+
PATH_HOPFIELD = 1
|
|
31
|
+
PATH_DELTA = 2
|
|
32
|
+
PATH_NAMES = {PATH_CONTROL: "control", PATH_HOPFIELD: "hopfield", PATH_DELTA: "delta"}
|
|
33
|
+
|
|
34
|
+
CURRENT_ROUTER_VERSION = 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class RouteDecision:
|
|
39
|
+
"""The stable output contract of any Router version.
|
|
40
|
+
|
|
41
|
+
path : the chosen path id (PATH_CONTROL always available as the floor)
|
|
42
|
+
confidence: [0,1] — how sure the router is (drives soft/hard regime in Slice C)
|
|
43
|
+
weights : optional soft mixture over paths (None = hard choice of `path`)
|
|
44
|
+
"""
|
|
45
|
+
path: int
|
|
46
|
+
confidence: float = 1.0
|
|
47
|
+
weights: Sequence[float] | None = None
|
|
48
|
+
|
|
49
|
+
def is_hard(self) -> bool:
|
|
50
|
+
return self.weights is None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Router(ABC):
|
|
54
|
+
"""The versioned, STABLE contract. Every future RouterVN implements exactly
|
|
55
|
+
this signature — versions EXTEND, they never break the interface. This is the
|
|
56
|
+
retro-compatibility promise, fixed at v1 so it precedes (not follows) the code.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
version: int = 0 # each concrete version sets this
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def route(self, *, span_ctx, path_scores: Sequence[float] | None = None,
|
|
63
|
+
state: dict | None = None) -> RouteDecision:
|
|
64
|
+
"""Decide which path to use for a span.
|
|
65
|
+
|
|
66
|
+
span_ctx : opaque context handle for the span (features the router may use)
|
|
67
|
+
path_scores : optional known per-path scores (the oracle uses these; a
|
|
68
|
+
learned router predicts them from span_ctx instead)
|
|
69
|
+
state : optional persistent router state (version-tagged; see below)
|
|
70
|
+
Returns a RouteDecision honouring the RouteDecision contract.
|
|
71
|
+
"""
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
# -- version-tagged serialisation (the retro-compat machinery, one branch) --
|
|
75
|
+
def dump_state(self, state: dict | None = None) -> dict:
|
|
76
|
+
"""Serialise router state WITH its version tag, so any future loader can
|
|
77
|
+
dispatch on it. v1 state is trivial; the envelope carries the tag."""
|
|
78
|
+
return {"router_version": self.version, "state": state or {}}
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def load_state(blob: dict) -> dict:
|
|
82
|
+
"""Version dispatch — currently one branch (v1). Adding v2 is one more
|
|
83
|
+
`elif`, never a change to this branch. Retro-compat by construction.
|
|
84
|
+
Active migration (v1→v2 transform) is deferred until a v2 exists — the
|
|
85
|
+
contract here makes it POSSIBLE, it does not implement it."""
|
|
86
|
+
v = blob.get("router_version")
|
|
87
|
+
if v == 1:
|
|
88
|
+
return blob.get("state", {})
|
|
89
|
+
# future: elif v == 2: return _migrate_or_read_v2(blob)
|
|
90
|
+
raise ValueError(f"unknown router_version {v!r} — no reader registered")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RouterV1(Router):
|
|
94
|
+
"""v1 = the upper-envelope ORACLE.
|
|
95
|
+
|
|
96
|
+
Given the known per-path scores for a span, pick the best path *that beats the
|
|
97
|
+
control floor*; if none beats it, choose the control. This is not a learned
|
|
98
|
+
router — it computes the CEILING (the ablation showed +21% vs control, +6%
|
|
99
|
+
over the best single path). Slice B replaces the oracle with a learned gate
|
|
100
|
+
that PREDICTS path_scores from span_ctx; it will implement this same interface.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
version = 1
|
|
104
|
+
|
|
105
|
+
def __init__(self, floor_margin: float = 0.0):
|
|
106
|
+
# a memory must beat the control by at least floor_margin to be chosen
|
|
107
|
+
self.floor_margin = floor_margin
|
|
108
|
+
|
|
109
|
+
def route(self, *, span_ctx=None, path_scores: Sequence[float] | None = None,
|
|
110
|
+
state: dict | None = None) -> RouteDecision:
|
|
111
|
+
if not path_scores:
|
|
112
|
+
# no information → fall to the guaranteed floor (never degrade)
|
|
113
|
+
return RouteDecision(path=PATH_CONTROL, confidence=1.0)
|
|
114
|
+
scores = list(path_scores)
|
|
115
|
+
control = scores[PATH_CONTROL]
|
|
116
|
+
# candidate memories that clear the floor by the margin
|
|
117
|
+
best_path, best_score = PATH_CONTROL, control
|
|
118
|
+
for p in range(1, len(scores)):
|
|
119
|
+
if scores[p] > control + self.floor_margin and scores[p] > best_score:
|
|
120
|
+
best_path, best_score = p, scores[p]
|
|
121
|
+
# confidence = normalised gap to the runner-up (how decisive the choice is)
|
|
122
|
+
ordered = sorted(scores, reverse=True)
|
|
123
|
+
gap = (ordered[0] - ordered[1]) if len(ordered) > 1 else 1.0
|
|
124
|
+
conf = float(min(1.0, max(0.0, gap / (abs(ordered[0]) + 1e-6))))
|
|
125
|
+
return RouteDecision(path=best_path, confidence=conf)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def make_router(version: int = CURRENT_ROUTER_VERSION, **kw) -> Router:
|
|
129
|
+
"""Factory — version dispatch for construction. Retro-compat in action:
|
|
130
|
+
v2 is one more branch, v1 is untouched (the contract fixed at v1 holds)."""
|
|
131
|
+
if version == 1:
|
|
132
|
+
return RouterV1(**kw)
|
|
133
|
+
if version == 2:
|
|
134
|
+
from cortex_c2.router_v2 import RouterV2 # torch-dependent, imported lazily
|
|
135
|
+
return RouterV2(**kw)
|
|
136
|
+
raise ValueError(f"no Router implementation for version {version}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def upper_envelope_auc(per_tier_by_path: dict[str, list[float]]) -> dict:
|
|
140
|
+
"""Compute the RouterV1-oracle result over a set of tiers: per tier, the
|
|
141
|
+
router picks max(control, hopfield, delta); the AUC of that choice is the
|
|
142
|
+
ceiling. Returns the envelope AUC and the routed path per tier."""
|
|
143
|
+
control = per_tier_by_path["control"]
|
|
144
|
+
hop = per_tier_by_path["hopfield"]
|
|
145
|
+
delta = per_tier_by_path["delta"]
|
|
146
|
+
n = len(control)
|
|
147
|
+
router = RouterV1()
|
|
148
|
+
routed_paths, routed_scores = [], []
|
|
149
|
+
for i in range(n):
|
|
150
|
+
d = router.route(path_scores=[control[i], hop[i], delta[i]])
|
|
151
|
+
routed_paths.append(PATH_NAMES[d.path])
|
|
152
|
+
routed_scores.append([control[i], hop[i], delta[i]][d.path])
|
|
153
|
+
env_auc = sum(routed_scores) / n
|
|
154
|
+
ctrl_auc = sum(control) / n
|
|
155
|
+
best_single = max(sum(hop) / n, sum(delta) / n, ctrl_auc)
|
|
156
|
+
return {
|
|
157
|
+
"envelope_auc": round(env_auc, 4),
|
|
158
|
+
"vs_control": round(env_auc - ctrl_auc, 4),
|
|
159
|
+
"vs_best_single_path": round(env_auc - best_single, 4),
|
|
160
|
+
"routed_paths": routed_paths,
|
|
161
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""cortex_c2.consolidation — the consolidating, self-healing router controller.
|
|
2
|
+
|
|
3
|
+
Slice C of RES-18 (ADR-006 D8). This is where the founder's three-message design
|
|
4
|
+
becomes code, as ONE stateful object wrapping a Router:
|
|
5
|
+
|
|
6
|
+
1. Control floor (message 1): a memory path is used only where it beats the
|
|
7
|
+
control; else the control. Enforced by the router; re-checked here.
|
|
8
|
+
2. Regime = consolidation (message 2): a per-span-type decision that has been
|
|
9
|
+
LEARNED — the same path wins, confidently, over N passes — is HARDENED
|
|
10
|
+
(routed hard, one path, no soft mixture, minimal cost). Unsure decisions
|
|
11
|
+
stay SOFT (the router's mixture) and keep generating the learning signal.
|
|
12
|
+
Hardening is conservative (a high, declared confidence threshold + stability
|
|
13
|
+
count) — better stay soft too long than harden an error.
|
|
14
|
+
3. Breaker fallback (message 3): when an active path degrades or diverges, do
|
|
15
|
+
not kill — RE-ROUTE to the best remaining path above the control, else the
|
|
16
|
+
control, and RE-SOFTEN the decision (re-open deliberation). A declared
|
|
17
|
+
HYSTERESIS (cooldown) prevents flapping between paths.
|
|
18
|
+
|
|
19
|
+
This is a textbook RES-17 type×state transition: a decision transitions
|
|
20
|
+
soft → hardened → (on breaker) softened-again. It unifies C1 (routing), RES-4
|
|
21
|
+
(confidence), RES-16 (consolidation + reminiscence), the endocrine explore/exploit
|
|
22
|
+
dial, basal-ganglia habit + hyperdirect veto, and the circuit breaker D7 — one
|
|
23
|
+
mechanism, seven windows.
|
|
24
|
+
|
|
25
|
+
Pure-Python controller (no torch here); it drives any Router.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
|
|
31
|
+
from cortex_c2 import Router, RouteDecision, PATH_CONTROL, PATH_NAMES
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DecisionState:
|
|
36
|
+
"""Per-span-type consolidation state — the RES-17 (type, state) cell."""
|
|
37
|
+
hardened_path: int | None = None # None = soft; an int = hardened to that path
|
|
38
|
+
stable_count: int = 0 # consecutive passes the same path won confidently
|
|
39
|
+
last_path: int | None = None
|
|
40
|
+
cooldown: int = 0 # hysteresis: passes remaining before a path may be re-chosen
|
|
41
|
+
banned_path: int | None = None # a path in cooldown after a breaker fallback
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ConsolidationConfig:
|
|
46
|
+
harden_confidence: float = 0.75 # min confidence to count a pass toward hardening
|
|
47
|
+
harden_stability: int = 5 # consecutive confident passes required to harden (conservative)
|
|
48
|
+
hysteresis_cooldown: int = 8 # passes a path stays banned after a fallback (anti-flapping)
|
|
49
|
+
clear_recovery_margin: float = 0.05 # a banned path must beat control by this to be re-eligible early
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ConsolidatingController:
|
|
53
|
+
"""Wraps a Router and adds consolidation + breaker fallback, per span-type key.
|
|
54
|
+
|
|
55
|
+
`route(key, span_ctx, path_scores)` returns a RouteDecision:
|
|
56
|
+
- if the key is HARDENED and not in cooldown → a hard decision on the learned path;
|
|
57
|
+
- else the router's (soft) decision, with the control floor enforced, and
|
|
58
|
+
hardening bookkeeping updated.
|
|
59
|
+
`report_breaker(key, path)` is called when a path degraded/diverged on this key:
|
|
60
|
+
it re-routes (bans the path for a cooldown), re-softens, and never leaves a void.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, router: Router, cfg: ConsolidationConfig | None = None):
|
|
64
|
+
self.router = router
|
|
65
|
+
self.cfg = cfg or ConsolidationConfig()
|
|
66
|
+
self.states: dict = {}
|
|
67
|
+
|
|
68
|
+
def _state(self, key) -> DecisionState:
|
|
69
|
+
return self.states.setdefault(key, DecisionState())
|
|
70
|
+
|
|
71
|
+
def route(self, *, key, span_ctx=None, path_scores=None) -> RouteDecision:
|
|
72
|
+
st = self._state(key)
|
|
73
|
+
|
|
74
|
+
# HARDENED path: route hard, minimal cost — unless it is the banned path
|
|
75
|
+
if (st.hardened_path is not None and st.hardened_path != st.banned_path
|
|
76
|
+
and st.cooldown == 0):
|
|
77
|
+
self._tick_cooldown(st)
|
|
78
|
+
return RouteDecision(path=st.hardened_path, confidence=1.0) # hard, no mixture
|
|
79
|
+
|
|
80
|
+
# otherwise ask the router (soft), then apply the floor + bookkeeping
|
|
81
|
+
d = self.router.route(span_ctx=span_ctx, path_scores=path_scores)
|
|
82
|
+
|
|
83
|
+
# if the chosen path is banned (still in cooldown), pull to control floor
|
|
84
|
+
if st.banned_path is not None and st.cooldown > 0 and d.path == st.banned_path:
|
|
85
|
+
d = RouteDecision(path=PATH_CONTROL, confidence=d.confidence, weights=d.weights)
|
|
86
|
+
|
|
87
|
+
# consolidation bookkeeping: count confident agreement toward hardening
|
|
88
|
+
if d.confidence >= self.cfg.harden_confidence and d.path == st.last_path:
|
|
89
|
+
st.stable_count += 1
|
|
90
|
+
else:
|
|
91
|
+
st.stable_count = 1
|
|
92
|
+
st.last_path = d.path
|
|
93
|
+
|
|
94
|
+
# harden once stable enough (conservative) and not the banned path
|
|
95
|
+
if (st.stable_count >= self.cfg.harden_stability
|
|
96
|
+
and d.path != PATH_CONTROL
|
|
97
|
+
and not (st.banned_path == d.path and st.cooldown > 0)):
|
|
98
|
+
st.hardened_path = d.path
|
|
99
|
+
|
|
100
|
+
self._tick_cooldown(st) # decrement AFTER serving this pass
|
|
101
|
+
return d
|
|
102
|
+
|
|
103
|
+
def _tick_cooldown(self, st: DecisionState) -> None:
|
|
104
|
+
if st.cooldown > 0:
|
|
105
|
+
st.cooldown -= 1
|
|
106
|
+
if st.cooldown == 0:
|
|
107
|
+
st.banned_path = None
|
|
108
|
+
|
|
109
|
+
def report_breaker(self, *, key, path, path_scores=None) -> RouteDecision:
|
|
110
|
+
"""A path degraded/diverged on this key → re-route with hysteresis.
|
|
111
|
+
|
|
112
|
+
Bans `path` for a cooldown (anti-flapping), re-softens (drops any
|
|
113
|
+
hardening), and returns the best remaining path above the control, else
|
|
114
|
+
the control. Never a void — completes the hyperdirect veto (redirect).
|
|
115
|
+
"""
|
|
116
|
+
st = self._state(key)
|
|
117
|
+
st.banned_path = path
|
|
118
|
+
st.cooldown = self.cfg.hysteresis_cooldown
|
|
119
|
+
st.hardened_path = None # re-soften: re-open deliberation (RES-16 reminiscence)
|
|
120
|
+
st.stable_count = 0
|
|
121
|
+
st.last_path = None
|
|
122
|
+
|
|
123
|
+
# choose the fallback: best remaining path above control, else control
|
|
124
|
+
if path_scores:
|
|
125
|
+
control = path_scores[PATH_CONTROL]
|
|
126
|
+
best_path, best_score = PATH_CONTROL, control
|
|
127
|
+
for p in range(len(path_scores)):
|
|
128
|
+
if p == path:
|
|
129
|
+
continue # the banned path is out
|
|
130
|
+
if p != PATH_CONTROL and path_scores[p] > control and path_scores[p] > best_score:
|
|
131
|
+
best_path, best_score = p, path_scores[p]
|
|
132
|
+
return RouteDecision(path=best_path, confidence=0.5)
|
|
133
|
+
# no scores → fall to the guaranteed floor
|
|
134
|
+
return RouteDecision(path=PATH_CONTROL, confidence=1.0)
|
|
135
|
+
|
|
136
|
+
# -- introspection (RES-17: the system becomes legible) ------------------
|
|
137
|
+
def snapshot(self) -> dict:
|
|
138
|
+
"""Inspectable view — how many keys are hardened, softened, in cooldown."""
|
|
139
|
+
hardened = sum(1 for s in self.states.values() if s.hardened_path is not None)
|
|
140
|
+
cooling = sum(1 for s in self.states.values() if s.cooldown > 0)
|
|
141
|
+
return {
|
|
142
|
+
"keys": len(self.states),
|
|
143
|
+
"hardened": hardened,
|
|
144
|
+
"soft": len(self.states) - hardened,
|
|
145
|
+
"in_cooldown": cooling,
|
|
146
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""cortex_c2.router_ablation — Slice D: does the learned router reach the ceiling?
|
|
2
|
+
|
|
3
|
+
The closing question of N2 (RES-18, ADR-006 D8). Slice A gave the ORACLE ceiling
|
|
4
|
+
(+21% vs control, +6% over the best single path) — but the oracle *reads* the
|
|
5
|
+
truth. Slice B/C gave a router that *learns* to route from context alone and
|
|
6
|
+
consolidates. Slice D measures whether that learned router, in practice,
|
|
7
|
+
approaches the oracle ceiling — the test that validates (or not) that the whole
|
|
8
|
+
multi-path system actually works, not just in oracle.
|
|
9
|
+
|
|
10
|
+
Method (measure-first): build a synthetic routing task where each span-type has a
|
|
11
|
+
context feature and a known best path (mirroring the ablation's per-tier domains:
|
|
12
|
+
Hopfield-favoured, delta-favoured, control-favoured). Train RouterV2 on observed
|
|
13
|
+
per-path scores; drive it through the ConsolidatingController; and compare four
|
|
14
|
+
readings on held-out spans:
|
|
15
|
+
- control-only (always the floor)
|
|
16
|
+
- best single memory (whichever single path scores best on average)
|
|
17
|
+
- the ORACLE (RouterV1: reads truth — the ceiling)
|
|
18
|
+
- the LEARNED router (RouterV2 + controller: predicts, never reads truth)
|
|
19
|
+
The learned router's gap to the oracle is the Slice-D signal. Reaching most of
|
|
20
|
+
the oracle's lift over the best single path means RES-18 pays off in practice.
|
|
21
|
+
|
|
22
|
+
Pure enough to run on CPU in minutes; a fuller version trains on real MQAR spans.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from cortex_c2 import PATH_CONTROL, PATH_HOPFIELD, PATH_DELTA, PATH_NAMES, RouterV1
|
|
35
|
+
from cortex_c2.consolidation import ConsolidatingController, ConsolidationConfig
|
|
36
|
+
|
|
37
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
38
|
+
N_PATHS = 3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- the synthetic routing task (each span-type has a domain) ----------------
|
|
42
|
+
def _span_scores(ctx0: float) -> list[float]:
|
|
43
|
+
"""Known per-path scores as a function of the context feature — mirrors the
|
|
44
|
+
ablation's finding that each path has a domain."""
|
|
45
|
+
if ctx0 < -0.33: # Hopfield's domain (dense recall)
|
|
46
|
+
return [0.10, 0.30, 0.16]
|
|
47
|
+
elif ctx0 > 0.33: # delta's domain (intermediate)
|
|
48
|
+
return [0.10, 0.16, 0.30]
|
|
49
|
+
else: # control's domain (memory hurts here)
|
|
50
|
+
return [0.28, 0.12, 0.12]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _make_spans(n: int, seed: int):
|
|
54
|
+
rng = np.random.default_rng(seed)
|
|
55
|
+
ctx = rng.standard_normal((n, 8)).astype(np.float32)
|
|
56
|
+
scores = np.array([_span_scores(float(ctx[i, 0])) for i in range(n)], dtype=np.float32)
|
|
57
|
+
return ctx, scores
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _auc_of_choice(scores: np.ndarray, chosen_path: np.ndarray) -> float:
|
|
61
|
+
"""Mean achieved score when each span uses its chosen path."""
|
|
62
|
+
return float(np.mean([scores[i, chosen_path[i]] for i in range(len(scores))]))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_router_ablation(train_n: int = 4000, test_n: int = 1000,
|
|
66
|
+
steps: int = 600, seed: int = 1337) -> dict:
|
|
67
|
+
import torch
|
|
68
|
+
from cortex_c2.router_v2 import RouterV2
|
|
69
|
+
|
|
70
|
+
# data
|
|
71
|
+
ctx_tr, sc_tr = _make_spans(train_n, seed)
|
|
72
|
+
ctx_te, sc_te = _make_spans(test_n, seed + 1)
|
|
73
|
+
|
|
74
|
+
# train the learned router (predict per-path scores from context)
|
|
75
|
+
torch.manual_seed(seed)
|
|
76
|
+
router = RouterV2(ctx_dim=8, floor_margin=0.0)
|
|
77
|
+
opt = torch.optim.Adam(router.parameters(), lr=1e-2)
|
|
78
|
+
Xtr = torch.from_numpy(ctx_tr); Ytr = torch.from_numpy(sc_tr)
|
|
79
|
+
bs = 128
|
|
80
|
+
for step in range(steps):
|
|
81
|
+
idx = torch.randint(0, train_n, (bs,))
|
|
82
|
+
loss = router.score_loss(Xtr[idx], Ytr[idx])
|
|
83
|
+
opt.zero_grad(); loss.backward(); opt.step()
|
|
84
|
+
|
|
85
|
+
# --- four readings on held-out spans ---
|
|
86
|
+
n = test_n
|
|
87
|
+
# 1) control-only
|
|
88
|
+
ctrl_choice = np.full(n, PATH_CONTROL)
|
|
89
|
+
auc_control = _auc_of_choice(sc_te, ctrl_choice)
|
|
90
|
+
# 2) best single memory (pick the path with best average score across spans)
|
|
91
|
+
avg = sc_te.mean(axis=0)
|
|
92
|
+
best_single_path = int(np.argmax(avg))
|
|
93
|
+
single_choice = np.full(n, best_single_path)
|
|
94
|
+
auc_best_single = _auc_of_choice(sc_te, single_choice)
|
|
95
|
+
# 3) oracle (RouterV1 reads truth)
|
|
96
|
+
oracle = RouterV1()
|
|
97
|
+
orc_choice = np.array([oracle.route(path_scores=sc_te[i].tolist()).path for i in range(n)])
|
|
98
|
+
auc_oracle = _auc_of_choice(sc_te, orc_choice)
|
|
99
|
+
# 4) learned router (RouterV2 + controller: predicts, never reads truth)
|
|
100
|
+
ctl = ConsolidatingController(router, ConsolidationConfig(harden_confidence=0.6,
|
|
101
|
+
harden_stability=3))
|
|
102
|
+
learned_choice = np.zeros(n, dtype=int)
|
|
103
|
+
Xte = torch.from_numpy(ctx_te)
|
|
104
|
+
for i in range(n):
|
|
105
|
+
# key = the span-type bucket (so consolidation has something to harden)
|
|
106
|
+
c0 = float(ctx_te[i, 0])
|
|
107
|
+
key = "hop" if c0 < -0.33 else "delta" if c0 > 0.33 else "ctrl"
|
|
108
|
+
d = ctl.route(key=key, span_ctx=Xte[i])
|
|
109
|
+
learned_choice[i] = d.path
|
|
110
|
+
auc_learned = _auc_of_choice(sc_te, learned_choice)
|
|
111
|
+
|
|
112
|
+
# gaps
|
|
113
|
+
lift_oracle = auc_oracle - auc_best_single
|
|
114
|
+
lift_learned = auc_learned - auc_best_single
|
|
115
|
+
frac_of_ceiling = (lift_learned / lift_oracle) if lift_oracle > 1e-9 else float("nan")
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"benchmark": "router_ablation",
|
|
119
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
120
|
+
"regime": {"train_n": train_n, "test_n": test_n, "steps": steps, "seed": seed},
|
|
121
|
+
"auc": {
|
|
122
|
+
"control_only": round(auc_control, 4),
|
|
123
|
+
"best_single_memory": round(auc_best_single, 4),
|
|
124
|
+
"oracle_ceiling": round(auc_oracle, 4),
|
|
125
|
+
"learned_router": round(auc_learned, 4),
|
|
126
|
+
},
|
|
127
|
+
"best_single_path": PATH_NAMES[best_single_path],
|
|
128
|
+
"lift_over_best_single": {
|
|
129
|
+
"oracle": round(lift_oracle, 4),
|
|
130
|
+
"learned": round(lift_learned, 4),
|
|
131
|
+
"learned_fraction_of_oracle_ceiling": round(frac_of_ceiling, 3),
|
|
132
|
+
},
|
|
133
|
+
"controller": ctl.snapshot(),
|
|
134
|
+
"verdict": (
|
|
135
|
+
f"learned router captures {frac_of_ceiling*100:.0f}% of the oracle's lift "
|
|
136
|
+
f"over the best single path"
|
|
137
|
+
if lift_oracle > 1e-9 else
|
|
138
|
+
"no oracle lift on this task (paths do not separate)"
|
|
139
|
+
),
|
|
140
|
+
"note": "RES-18 Slice D — the learned router predicts (never reads truth); "
|
|
141
|
+
"reaching most of the oracle ceiling means the multi-path design pays off. "
|
|
142
|
+
"Synthetic routing task; a fuller run trains on real MQAR spans.",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def write_artefact(result: dict, out_dir: str | Path) -> Path:
|
|
147
|
+
out = Path(out_dir); out.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
stamp = result["timestamp_utc"][:19].replace(":", "").replace("-", "")
|
|
149
|
+
path = out / f"router-ablation-{stamp}.json"
|
|
150
|
+
path.write_text(json.dumps(result, indent=2) + "\n")
|
|
151
|
+
(out / "LATEST-router.json").write_text(json.dumps(result, indent=2) + "\n")
|
|
152
|
+
return path
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _cli():
|
|
156
|
+
ap = argparse.ArgumentParser(description="RES-18 router ablation (Slice D)")
|
|
157
|
+
ap.add_argument("--steps", type=int, default=600)
|
|
158
|
+
ap.add_argument("--out", default="metrics/mqar")
|
|
159
|
+
ap.add_argument("--quick", action="store_true")
|
|
160
|
+
args = ap.parse_args()
|
|
161
|
+
r = run_router_ablation(steps=200 if args.quick else args.steps,
|
|
162
|
+
train_n=1000 if args.quick else 4000)
|
|
163
|
+
path = write_artefact(r, args.out)
|
|
164
|
+
print("\n=== RES-18 router ablation (Slice D) ===")
|
|
165
|
+
print(json.dumps(r["auc"], indent=2))
|
|
166
|
+
print(r["verdict"])
|
|
167
|
+
print(f"controller: {json.dumps(r['controller'])}")
|
|
168
|
+
print(f"artefact: {path}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
_cli()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ============================================================================ #
|
|
176
|
+
# (b) D19 Level 1b — the router on REAL MQAR spans, not the synthetic task #
|
|
177
|
+
# ============================================================================ #
|
|
178
|
+
def _real_span_ctx(kv: int, seq: int) -> np.ndarray:
|
|
179
|
+
"""Context feature for a real MQAR span-type: normalised (kv, seq) + simple
|
|
180
|
+
interactions. The router must learn which path wins from THIS, never from
|
|
181
|
+
the truth. (8 dims to stay compatible with RouterV2(ctx_dim=8).)"""
|
|
182
|
+
k = math.log2(kv) / 5.0 # kv ∈ {4..32} → ~0.4..1.0
|
|
183
|
+
s = math.log2(seq) / 9.0 # seq ∈ {128..512} → ~0.78..1.0
|
|
184
|
+
return np.array([k, s, k * s, k * k, s * s, k - s, 1.0 - k, 1.0 - s], dtype=np.float32)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def run_router_ablation_on_real_mqar(curves_by_path: dict, steps: int = 800,
|
|
188
|
+
seed: int = 1337, holdout_frac: float = 0.34) -> dict:
|
|
189
|
+
"""Train RouterV2 on real per-tier MQAR accuracies (from an ablation artefact)
|
|
190
|
+
and test on held-out tiers. curves_by_path = {"none": [...], "hopfield": [...],
|
|
191
|
+
"delta": [...]} where each item is {"kv_pairs","seq_len","accuracy"} (same tier
|
|
192
|
+
order). Held-out tiers are chosen by seed so the router never sees their scores.
|
|
193
|
+
"""
|
|
194
|
+
import torch
|
|
195
|
+
from cortex_c2.router_v2 import RouterV2
|
|
196
|
+
|
|
197
|
+
tiers = [(t["kv_pairs"], t["seq_len"]) for t in curves_by_path["none"]]
|
|
198
|
+
n = len(tiers)
|
|
199
|
+
scores = np.array([[curves_by_path["none"][i]["accuracy"],
|
|
200
|
+
curves_by_path["hopfield"][i]["accuracy"],
|
|
201
|
+
curves_by_path["delta"][i]["accuracy"]] for i in range(n)],
|
|
202
|
+
dtype=np.float32)
|
|
203
|
+
ctx = np.array([_real_span_ctx(kv, sq) for kv, sq in tiers], dtype=np.float32)
|
|
204
|
+
|
|
205
|
+
rng = np.random.default_rng(seed)
|
|
206
|
+
perm = rng.permutation(n)
|
|
207
|
+
n_test = max(2, int(round(n * holdout_frac)))
|
|
208
|
+
test_idx, train_idx = perm[:n_test], perm[n_test:]
|
|
209
|
+
|
|
210
|
+
torch.manual_seed(seed)
|
|
211
|
+
router = RouterV2(ctx_dim=8, floor_margin=0.0)
|
|
212
|
+
opt = torch.optim.Adam(router.parameters(), lr=1e-2)
|
|
213
|
+
Xtr = torch.from_numpy(ctx[train_idx]); Ytr = torch.from_numpy(scores[train_idx])
|
|
214
|
+
for _ in range(steps):
|
|
215
|
+
loss = router.score_loss(Xtr, Ytr)
|
|
216
|
+
opt.zero_grad(); loss.backward(); opt.step()
|
|
217
|
+
|
|
218
|
+
# readings on held-out tiers
|
|
219
|
+
te_scores = scores[test_idx]
|
|
220
|
+
auc_control = float(te_scores[:, PATH_CONTROL].mean())
|
|
221
|
+
avg = te_scores.mean(axis=0); best_single_path = int(np.argmax(avg))
|
|
222
|
+
auc_best_single = float(te_scores[:, best_single_path].mean())
|
|
223
|
+
oracle = RouterV1()
|
|
224
|
+
orc = [oracle.route(path_scores=te_scores[i].tolist()).path for i in range(len(test_idx))]
|
|
225
|
+
auc_oracle = float(np.mean([te_scores[i, orc[i]] for i in range(len(test_idx))]))
|
|
226
|
+
Xte = torch.from_numpy(ctx[test_idx])
|
|
227
|
+
learned = [router.route(span_ctx=Xte[i]).path for i in range(len(test_idx))]
|
|
228
|
+
auc_learned = float(np.mean([te_scores[i, learned[i]] for i in range(len(test_idx))]))
|
|
229
|
+
|
|
230
|
+
lift_o = auc_oracle - auc_best_single
|
|
231
|
+
lift_l = auc_learned - auc_best_single
|
|
232
|
+
frac = (lift_l / lift_o) if lift_o > 1e-9 else float("nan")
|
|
233
|
+
return {
|
|
234
|
+
"benchmark": "router_ablation_real_mqar",
|
|
235
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
236
|
+
"regime": {"n_tiers": n, "held_out": [tiers[i] for i in test_idx],
|
|
237
|
+
"train": [tiers[i] for i in train_idx], "steps": steps, "seed": seed},
|
|
238
|
+
"auc_heldout": {
|
|
239
|
+
"control_only": round(auc_control, 4),
|
|
240
|
+
"best_single_memory": round(auc_best_single, 4),
|
|
241
|
+
"oracle_ceiling": round(auc_oracle, 4),
|
|
242
|
+
"learned_router": round(auc_learned, 4),
|
|
243
|
+
},
|
|
244
|
+
"best_single_path": PATH_NAMES[best_single_path],
|
|
245
|
+
"learned_routes_heldout": [PATH_NAMES[p] for p in learned],
|
|
246
|
+
"oracle_routes_heldout": [PATH_NAMES[p] for p in orc],
|
|
247
|
+
"lift_over_best_single": {"oracle": round(lift_o, 4), "learned": round(lift_l, 4),
|
|
248
|
+
"learned_fraction_of_oracle_ceiling": round(frac, 3) if frac == frac else None},
|
|
249
|
+
"verdict": (f"on REAL MQAR held-out tiers the learned router captures "
|
|
250
|
+
f"{frac*100:.0f}% of the oracle lift" if frac == frac else
|
|
251
|
+
"no oracle lift on held-out tiers (paths do not separate there)"),
|
|
252
|
+
"note": "D19 Level 1b — router trained/tested on real per-tier MQAR accuracies "
|
|
253
|
+
"(held-out tiers never seen). Small-N caveat: 12 tiers only; repeat across seeds/holdouts.",
|
|
254
|
+
}
|
cortex_c2/router_v2.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""cortex_c2.router_v2 — the learned soft router (RES-18, ADR-006 D8), Slice B.
|
|
2
|
+
|
|
3
|
+
Slice A's RouterV1 is an ORACLE: it *reads* the true per-path scores. That is the
|
|
4
|
+
ceiling, not a usable router — at inference there is no label saying "Hopfield
|
|
5
|
+
scores 0.06 here". RouterV2 is the real thing: a small head that **predicts** a
|
|
6
|
+
score per path *from the span context alone*, then routes by weighting. It learns
|
|
7
|
+
by a supervised signal — its predictions must correlate with the performances
|
|
8
|
+
actually observed (the ablation gives those). The central challenge of the whole
|
|
9
|
+
router: route without knowing the answer.
|
|
10
|
+
|
|
11
|
+
RouterV2 honours the *same* `Router` interface as v1 (retro-compat in action:
|
|
12
|
+
v2 is added, v1 stays; the version dispatch gains one branch). It emits a **soft**
|
|
13
|
+
RouteDecision (a weighted mixture over paths) with a confidence; Slice C turns
|
|
14
|
+
high-confidence decisions into hard routing and wires the breaker fallback.
|
|
15
|
+
|
|
16
|
+
The control floor (RES-18) is preserved: the predicted-best path is used only if
|
|
17
|
+
it clears the control's predicted score by a margin; else the mixture is pulled
|
|
18
|
+
toward the control. C2 can never be made to degrade the model by the router.
|
|
19
|
+
|
|
20
|
+
Depends on torch (kept out of cortex_c2/__init__.py so the core stays light).
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Sequence
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
import torch.nn as nn
|
|
28
|
+
import torch.nn.functional as F
|
|
29
|
+
|
|
30
|
+
from cortex_c2 import (Router, RouteDecision, PATH_CONTROL, PATH_HOPFIELD,
|
|
31
|
+
PATH_DELTA, PATH_NAMES)
|
|
32
|
+
|
|
33
|
+
N_PATHS = 3 # control, hopfield, delta
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RouterV2(nn.Module, Router):
|
|
37
|
+
"""A learned gate that predicts per-path scores from a context vector.
|
|
38
|
+
|
|
39
|
+
- `ctx_dim`: dimensionality of the span-context feature the router sees.
|
|
40
|
+
- forward(ctx) -> predicted score per path (b, N_PATHS).
|
|
41
|
+
- route(span_ctx=<tensor>) -> a soft RouteDecision (weights = softmax over the
|
|
42
|
+
predicted scores, with the control floor enforced), plus a confidence.
|
|
43
|
+
- training: `score_loss(ctx, observed)` regresses predictions onto the
|
|
44
|
+
observed per-path performances (the supervised signal from the ablation).
|
|
45
|
+
|
|
46
|
+
Version dispatch: `version = 2`; `Router.load_state` gains its v2 branch in
|
|
47
|
+
Slice C when v2 carries persistent state. For now v2's "state" is its weights,
|
|
48
|
+
saved by the normal torch checkpoint — the contract's tag still applies.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
version = 2
|
|
52
|
+
|
|
53
|
+
def __init__(self, ctx_dim: int = 32, hidden: int = 64,
|
|
54
|
+
floor_margin: float = 0.0, temperature: float = 1.0):
|
|
55
|
+
nn.Module.__init__(self)
|
|
56
|
+
self.ctx_dim = ctx_dim
|
|
57
|
+
self.floor_margin = floor_margin
|
|
58
|
+
self.temperature = temperature
|
|
59
|
+
self.net = nn.Sequential(
|
|
60
|
+
nn.Linear(ctx_dim, hidden),
|
|
61
|
+
nn.GELU(),
|
|
62
|
+
nn.Linear(hidden, N_PATHS),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# -- prediction ----------------------------------------------------------
|
|
66
|
+
def forward(self, ctx: torch.Tensor) -> torch.Tensor:
|
|
67
|
+
"""ctx: (b, ctx_dim) -> predicted per-path score (b, N_PATHS)."""
|
|
68
|
+
return self.net(ctx)
|
|
69
|
+
|
|
70
|
+
# -- the Router contract -------------------------------------------------
|
|
71
|
+
def route(self, *, span_ctx=None, path_scores: Sequence[float] | None = None,
|
|
72
|
+
state: dict | None = None) -> RouteDecision:
|
|
73
|
+
"""Route from the *predicted* scores. span_ctx is the context feature
|
|
74
|
+
(a tensor of shape (ctx_dim,) or (1, ctx_dim)); path_scores is ignored
|
|
75
|
+
(v2 predicts, it does not read the truth — that was v1's oracle)."""
|
|
76
|
+
if span_ctx is None:
|
|
77
|
+
return RouteDecision(path=PATH_CONTROL, confidence=1.0) # floor
|
|
78
|
+
ctx = span_ctx if span_ctx.dim() == 2 else span_ctx.unsqueeze(0)
|
|
79
|
+
with torch.no_grad():
|
|
80
|
+
pred = self.forward(ctx).squeeze(0) # (N_PATHS,)
|
|
81
|
+
# control floor: a memory competes only if it clears control + margin
|
|
82
|
+
adj = pred.clone()
|
|
83
|
+
control = pred[PATH_CONTROL]
|
|
84
|
+
for p in (PATH_HOPFIELD, PATH_DELTA):
|
|
85
|
+
if pred[p] <= control + self.floor_margin:
|
|
86
|
+
adj[p] = float("-inf") # remove from the mixture
|
|
87
|
+
weights = F.softmax(adj / self.temperature, dim=-1)
|
|
88
|
+
best = int(torch.argmax(weights).item())
|
|
89
|
+
# confidence = how peaked the mixture is (max weight, rescaled)
|
|
90
|
+
conf = float((weights.max() - 1.0 / N_PATHS) / (1.0 - 1.0 / N_PATHS))
|
|
91
|
+
conf = max(0.0, min(1.0, conf))
|
|
92
|
+
return RouteDecision(path=best, confidence=conf,
|
|
93
|
+
weights=weights.tolist())
|
|
94
|
+
|
|
95
|
+
# -- training signal -----------------------------------------------------
|
|
96
|
+
def score_loss(self, ctx: torch.Tensor, observed: torch.Tensor) -> torch.Tensor:
|
|
97
|
+
"""MSE between predicted and observed per-path scores.
|
|
98
|
+
|
|
99
|
+
ctx : (b, ctx_dim)
|
|
100
|
+
observed : (b, N_PATHS) — the true per-path performances for those spans
|
|
101
|
+
(from the ablation / online measurement). This is how the
|
|
102
|
+
router learns to predict which path wins, from context alone.
|
|
103
|
+
"""
|
|
104
|
+
pred = self.forward(ctx)
|
|
105
|
+
return F.mse_loss(pred, observed)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def make_router_v2(ctx_dim: int = 32, **kw) -> RouterV2:
|
|
109
|
+
return RouterV2(ctx_dim=ctx_dim, **kw)
|