exp007_math: math anchors, 4 ask-methods — format lock-in 4/4, no correctness tax, held-out f(x) 0.688->0.938; sep peak 0.496 @L23
Browse files- exp007_math/README.md +200 -0
- exp007_math/adapters/q7_relay_pw_math_s0.pt +3 -0
- exp007_math/ar_differentiation_bed.py +487 -0
- exp007_math/build_results.py +88 -0
- exp007_math/exp013_augmentation_bed.py +506 -0
- exp007_math/geolip_vitals.py +219 -0
- exp007_math/qwen_exp001_relay.py +271 -0
- exp007_math/qwen_exp002_refine.py +237 -0
- exp007_math/qwen_exp003_instruct.py +451 -0
- exp007_math/qwen_exp006_story.py +470 -0
- exp007_math/qwen_exp007_math.py +395 -0
- exp007_math/repro.py +23 -0
- exp007_math/results/ledger.jsonl +2 -0
- exp007_math/results/results.json +116 -0
exp007_math/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# exp007_math — one adapter stack, four ways of asking for a computation: format locks in with no correctness tax, and held-out function evaluation improves (0.688 → 0.938)
|
| 2 |
+
|
| 3 |
+
**Question.** exp006 left a sharp split on this trunk: the `relay_pw` anchor
|
| 4 |
+
stack locked in every *formal* register (continuation shape, instruct shape,
|
| 5 |
+
tool-call JSON all → 1.0) but a *semantic* constraint — "use these three
|
| 6 |
+
keywords" — did not move at all (0.083 frozen, 0.083 trained). So what does
|
| 7 |
+
the anchor paradigm actually carry: only the **shape** of answers, or
|
| 8 |
+
**computation** itself? This bed asks one arithmetic question four different
|
| 9 |
+
ways and judges FORMAT separately from CORRECTNESS, with a frozen-trunk
|
| 10 |
+
baseline to separate "the trunk can already add" from what the anchor adds,
|
| 11 |
+
and a problem-hash holdout so that held-out correctness is unseen-instance
|
| 12 |
+
computation, not recall.
|
| 13 |
+
|
| 14 |
+
**Answer.** Both. Format locks in everywhere — all four ask-methods reach
|
| 15 |
+
format 1.0, repairing the one register the frozen trunk asks wrong (`nl`
|
| 16 |
+
answers correctly but not in the asked shape, format 0.312) — and
|
| 17 |
+
**correctness never degrades** on any register. Where correctness had
|
| 18 |
+
headroom it *improves*: code-register held-out `f(x)` evaluation goes
|
| 19 |
+
**0.688 → 0.938**, on held-out questions whose exact form and target never
|
| 20 |
+
appear in training — function evaluation improving, not target recall (see
|
| 21 |
+
claim 2 for the exact unseen-instance accounting). Cross-referencing
|
| 22 |
+
exp006 gives the interpretation (not a ledger assertion): the LM loss
|
| 23 |
+
teaches exactly what **target-token predictability** demands. Answer tokens
|
| 24 |
+
are unpredictable without using the question, so correctness moves; exp006's
|
| 25 |
+
keywords were never needed to predict a plausible story, so compliance never
|
| 26 |
+
moved. And the four ask-methods are held apart in sign-code space more
|
| 27 |
+
strongly than any register set ledgered in this line so far: separation
|
| 28 |
+
**0.4711 @ L16 / 0.4957 @ L23**.
|
| 29 |
+
|
| 30 |
+
## The design
|
| 31 |
+
|
| 32 |
+
**Registers (the four ask-methods).** Every problem is rendered exactly one
|
| 33 |
+
way, deterministically assigned:
|
| 34 |
+
|
| 35 |
+
| register | system instruction asks for | example user turn | example target |
|
| 36 |
+
|---|---|---|---|
|
| 37 |
+
| `nl` | equation + result | "What is 37 + 45?" | `37 + 45 = 82` |
|
| 38 |
+
| `json` | `emit_answer` tool call | "Compute 37 + 45." | `{"expression": "37 + 45", "result": 82}` |
|
| 39 |
+
| `code` | value of a defined Python function | `def f(x): return 3 * x + 2` … "What is f(4)?" | `f(4) = 14` |
|
| 40 |
+
| `word` | one short sentence | "Sam has 37 apples. Sam gets 45 more. …" | `Sam has 82 apples.` |
|
| 41 |
+
|
| 42 |
+
**Problem space.** Fully synthetic — no downloads, ground truth computable:
|
| 43 |
+
`a + b` (1–99), `a − b` (result ≥ 0), `a * b` (2–12), and `f(x) = a·x + b`
|
| 44 |
+
with a 2–9, b 0–20, x 2–9. 4,000 unique problems; `f(x)` problems always
|
| 45 |
+
land in the `code` register (the other kinds round-robin across registers,
|
| 46 |
+
with non-`f(x)` problems in `code` rendered as a one-line function).
|
| 47 |
+
**Holdout is by hash of the rendered question (10%)** — a held-out
|
| 48 |
+
problem's exact question and target never appear in training. For
|
| 49 |
+
`f(x) = a·x + b` problems the `(a, b, x)` combination is additionally
|
| 50 |
+
unseen anywhere in the corpus in any register; a non-`f(x)` problem's
|
| 51 |
+
underlying `a op b` fact *can* appear in training under a different
|
| 52 |
+
register (audited and accounted in claim 2).
|
| 53 |
+
|
| 54 |
+
**Arms.** `frozen` (the untouched trunk, judged with the same prompts) and
|
| 55 |
+
`relay_pw_math`: one exp002-certified `relay_pw` (RelayPatchwork) adapter
|
| 56 |
+
per block of a fully frozen Qwen2.5-0.5B-Instruct trunk — the identical
|
| 57 |
+
adapter layout as exp006 — trained on **all four registers interleaved**
|
| 58 |
+
uniformly per step. 3000 steps, block 512, batch 4, pure Adam lr 1e-3,
|
| 59 |
+
weight decay 0, seed 0. Greedy decoding at eval, max 96 new tokens.
|
| 60 |
+
|
| 61 |
+
**Judges.** The judge splits format from correctness, per register,
|
| 62 |
+
n=16/register, both arms. A generation must close (emit `<|im_end|>` or
|
| 63 |
+
stop) or it fails both. Then:
|
| 64 |
+
|
| 65 |
+
| register | FORMAT passes if | CORRECT passes if |
|
| 66 |
+
|---|---|---|
|
| 67 |
+
| `nl`, `code` | body contains `=` | last integer in body == ground truth |
|
| 68 |
+
| `json` | tool call parses with both keys (`expression`, `result`) | `int(result)` == ground truth |
|
| 69 |
+
| `word` | body ≤ 24 words | last integer in body == ground truth |
|
| 70 |
+
|
| 71 |
+
For `nl`/`code`/`word` the body must also contain at least one integer,
|
| 72 |
+
else both format and correctness fail. Plus per-register held-out
|
| 73 |
+
perplexity, and the register probe carried from
|
| 74 |
+
exp003/exp006: sign-code separation = mean inter-register Hamming distance
|
| 75 |
+
− mean intra-register Hamming distance of the signed winner codes at blocks
|
| 76 |
+
0 / 8 / 16 / 23 (the frozen arm has no adapters, hence no probe).
|
| 77 |
+
|
| 78 |
+
## Results (all from `results/ledger.jsonl`, seed 0)
|
| 79 |
+
|
| 80 |
+
**Frozen vs trained, per register** — format, correctness, held-out ppl:
|
| 81 |
+
|
| 82 |
+
| register | format (frozen → trained) | correct (frozen → trained) | held-out ppl (frozen → trained) |
|
| 83 |
+
|---|---|---|---|
|
| 84 |
+
| `nl` | 0.312 → **1.0** | 1.0 → 1.0 | 2.763 → 1.342 |
|
| 85 |
+
| `json` | 1.0 → 1.0 | 1.0 → 1.0 | 4.393 �� 1.061 |
|
| 86 |
+
| `code` | 1.0 → 1.0 | 0.688 → **0.938** | 2.591 → 1.168 |
|
| 87 |
+
| `word` | 1.0 → 1.0 | 0.938 → **1.0** | 3.145 → 1.525 |
|
| 88 |
+
|
| 89 |
+
The frozen trunk is already a competent zero-shot calculator at these
|
| 90 |
+
operand sizes (`nl`/`json` correctness 1.0) — its failures are one register
|
| 91 |
+
asked in the wrong shape (`nl`: "45" or "The equation for 94 - 27 is 67."
|
| 92 |
+
instead of "94 - 27 = 67") and genuine computation misses on `f(x)`
|
| 93 |
+
evaluation (0.688) and word problems (0.938). Training repairs the format
|
| 94 |
+
failure completely and moves both correctness gaps up, degrading nothing.
|
| 95 |
+
|
| 96 |
+
**Register separation** (trained arm; signed winner codes across the four
|
| 97 |
+
ask-methods):
|
| 98 |
+
|
| 99 |
+
| tap | inter | intra | sep |
|
| 100 |
+
|---|---|---|---|
|
| 101 |
+
| L0 | 0.2563 | 0.0418 | 0.2145 |
|
| 102 |
+
| L8 | 0.3293 | 0.0485 | 0.2808 |
|
| 103 |
+
| L16 | 0.8737 | 0.4026 | **0.4711** |
|
| 104 |
+
| L23 | 0.9304 | 0.4347 | **0.4957** |
|
| 105 |
+
|
| 106 |
+
Separation grows monotonically with depth and the peak (0.4957 @ L23) is
|
| 107 |
+
the deepest ledgered in this line so far: exp006's four story registers
|
| 108 |
+
reached 0.3476 / 0.3451 at L16 / L23 and peaked at 0.4619 @ L8 (where this
|
| 109 |
+
run is *lower*, 0.2808), and exp003's instruct tasks stayed ≤ 0.304 at
|
| 110 |
+
L16 / L23 (all cited from their ledgers). Four surface forms of the *same
|
| 111 |
+
underlying computation* are held further apart in sign-code space, at its
|
| 112 |
+
peak, than either previous register set at theirs — with the depth
|
| 113 |
+
*profile* differing across beds.
|
| 114 |
+
|
| 115 |
+
## What this adds to the line
|
| 116 |
+
|
| 117 |
+
**Claim 1 — format lock-in, with no correctness tax.** All four registers
|
| 118 |
+
reach format 1.0 (from a frozen `nl` format of 0.312), and correctness never
|
| 119 |
+
degrades on any register while it happens: `nl` and `json` hold 1.0, `word`
|
| 120 |
+
moves 0.938 → 1.0. Locking the ask-shape did not cost the anchor any of the
|
| 121 |
+
trunk's arithmetic — the failure mode where format training taxes semantics
|
| 122 |
+
did not appear at this scale.
|
| 123 |
+
|
| 124 |
+
**Claim 2 — computation moves through the adapters.** Code-register
|
| 125 |
+
held-out correctness improves 0.688 → 0.938 on 16 judged problems, every
|
| 126 |
+
one rendered as an `f(x)` question whose exact form and target string never
|
| 127 |
+
appear in training. Exact composition (audited from the deterministic
|
| 128 |
+
generator, seed 0): 5 of the 16 are true `f(x) = a·x + b` problems whose
|
| 129 |
+
`(a, b, x)` combinations appear **nowhere** in the corpus in any register —
|
| 130 |
+
for those, improvement is unseen-instance function evaluation with no
|
| 131 |
+
recall channel at all. The other 11 wrap an `a op b` problem as a one-line
|
| 132 |
+
function; the question and target are still unseen, but for 4 of the 11
|
| 133 |
+
the underlying arithmetic fact appears in training under a *different*
|
| 134 |
+
register, so cross-register fact transfer is a possible channel for those
|
| 135 |
+
four. The improvement therefore cannot be target-string recall, and for the
|
| 136 |
+
`a·x + b` subset it cannot be recall of any kind. The ledgered frozen
|
| 137 |
+
baseline is what makes the sentence honest: the trunk already computes most
|
| 138 |
+
of these, and the anchor's contribution is the measured delta on the part
|
| 139 |
+
it couldn't.
|
| 140 |
+
|
| 141 |
+
**Claim 3 — the predictability principle (cross-experiment interpretation,
|
| 142 |
+
not a ledger assertion).** Placed next to exp006, this run completes a
|
| 143 |
+
clean contrast on the same trunk, adapter, recipe, and step count: math
|
| 144 |
+
correctness improved where exp006's keyword compliance (0.083 → 0.083) did
|
| 145 |
+
not. The parsimonious reading: **the LM loss teaches exactly what
|
| 146 |
+
target-token predictability demands.** Here the answer tokens ("= 82") are
|
| 147 |
+
unpredictable unless the model actually uses the question, so lowering the
|
| 148 |
+
loss forces computation. In exp006 the target stories were predictable
|
| 149 |
+
as stories without honoring the keyword instruction, so nothing pushed
|
| 150 |
+
compliance. This is an interpretation cross-referenced against exp006's
|
| 151 |
+
ledger, not a number asserted from this one; the exp008-class test it
|
| 152 |
+
suggests is a constraint whose target tokens are unpredictable without
|
| 153 |
+
obeying it.
|
| 154 |
+
|
| 155 |
+
**Claim 4 — register separation, deepest peak yet.** Sign-code separation
|
| 156 |
+
across the four ask-methods reaches 0.4711 @ L16 and 0.4957 @ L23 — above
|
| 157 |
+
exp006's story registers at those taps (0.3476 / 0.3451) and above exp006's
|
| 158 |
+
own peak (0.4619 @ L8), and above exp003's instruct tasks (≤ 0.304 at
|
| 159 |
+
L16 / L23). The depth profile is bed-dependent — exp006 peaked shallow, this
|
| 160 |
+
run at L8 sits at 0.2808 — but the peak itself is the deepest ledgered in
|
| 161 |
+
the line. The addressing surface differentiates *how a thing is asked* even
|
| 162 |
+
when *what is computed* is identical — and here does so most strongly deep
|
| 163 |
+
in the trunk.
|
| 164 |
+
|
| 165 |
+
**Caveats.** 1 seed; n=16/register; operands small (2-digit; a*b up to
|
| 166 |
+
12x12; f(x)=ax+b a 2-9 b 0-20 x 2-9); zero-shot correctness already high
|
| 167 |
+
(nl/json 1.0) so headroom was format + code; of the 16 judged code
|
| 168 |
+
problems only 5 are true a·x+b instances and 4 of the remaining 11 share
|
| 169 |
+
their underlying arithmetic fact with another register's training text
|
| 170 |
+
(claim 2 accounting); register-separation depth profiles differ across
|
| 171 |
+
beds, so "deepest yet" is a claim about the peak, not every tap.
|
| 172 |
+
|
| 173 |
+
## Files
|
| 174 |
+
|
| 175 |
+
- `qwen_exp007_math.py` — the bed: deterministic problem generator,
|
| 176 |
+
four-register renderer, format/correctness judges, held-out ppl, the
|
| 177 |
+
campaign (`run_exp007`), CPU smoke.
|
| 178 |
+
- `geolip_vitals.py` / `ar_differentiation_bed.py` /
|
| 179 |
+
`exp013_augmentation_bed.py` / `qwen_exp001_relay.py` /
|
| 180 |
+
`qwen_exp002_refine.py` / `qwen_exp003_instruct.py` /
|
| 181 |
+
`qwen_exp006_story.py` — the paste-order harness (trunk wrapper,
|
| 182 |
+
`relay_pw`, tool-call parser, arm loader and register probe; identical to
|
| 183 |
+
the copies shipped with exp006).
|
| 184 |
+
- `repro.py` — smoke / `--run` (full campaign).
|
| 185 |
+
- `build_results.py` — re-asserts every README claim from the ledger.
|
| 186 |
+
- `results/ledger.jsonl` — both rows: frozen baseline + trained arm.
|
| 187 |
+
- `adapters/q7_relay_pw_math_s0.pt` — the trained adapter stack.
|
| 188 |
+
|
| 189 |
+
## Repro
|
| 190 |
+
|
| 191 |
+
```bash
|
| 192 |
+
pip install torch transformers
|
| 193 |
+
python repro.py # CPU smoke: problem gen + format/correctness
|
| 194 |
+
# judges exercised in both directions
|
| 195 |
+
python repro.py --run # full campaign (GPU-only verdicts)
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
Data and caches land in `./data` (override with `GEOLIP_DATA`). The corpus
|
| 199 |
+
is fully synthetic — no dataset downloads; ground truth is computed, so the
|
| 200 |
+
judges need no reference model.
|
exp007_math/adapters/q7_relay_pw_math_s0.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc8650497885ea506f2d29ef644f8a8e32557030cf403cdf5f604b5d4df23783
|
| 3 |
+
size 22168167
|
exp007_math/ar_differentiation_bed.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ar_differentiation_bed.py — exp012: autoregressive differentiation of the aleph.
|
| 2 |
+
|
| 3 |
+
Differentiation is cultivated by PREDICTIVE pressure along the sequence — the
|
| 4 |
+
address parameterizing the next-byte distribution (Law 2: chain-rule advantage pays
|
| 5 |
+
ONLY where the composed address directly parameterizes the predictive distribution).
|
| 6 |
+
This bed puts the aleph in the autoregressive gradient path and measures what
|
| 7 |
+
differentiates. The head arms enforce the employment law at its maximum: the
|
| 8 |
+
ENTIRE next-byte distribution is parameterized by the address.
|
| 9 |
+
|
| 10 |
+
Byte-level causal LM on wikitext-2-raw (HF parquet, CDN-fast), block 256. ARMS:
|
| 11 |
+
sdpa — standard causal transformer control (matched trunk).
|
| 12 |
+
hub — attention replaced by CAUSAL HUB: linear attention whose feature map
|
| 13 |
+
is the 2K-oriented aleph address, prefix-sum memories (no selection
|
| 14 |
+
event; O(n*K*d)). Differentiation cultivated INSIDE attention.
|
| 15 |
+
addr_head — sdpa trunk, but the OUTPUT HEAD reads ONLY the signed aleph
|
| 16 |
+
coefficient vector w_k = sinh(u_k)/sum_j cosh(u_j) of the final
|
| 17 |
+
hidden state (K -> 256 logits). The address MUST carry every bit of
|
| 18 |
+
next-byte information — the hardest Law-2 bottleneck.
|
| 19 |
+
|
| 20 |
+
JUDGED BY: val bits-per-byte per arm (task) + CULTIVATION VITALS on every aleph
|
| 21 |
+
codebook (readouts, never losses): axis aliveness/hppl, drift-from-init +
|
| 22 |
+
binding fraction @0.29154, winner-|cos| saturation (sign-code emergence), shadow
|
| 23 |
+
path diversity (fixed high-bits hash). Never by recon.
|
| 24 |
+
|
| 25 |
+
Riders: pure Adam wd=0; no BN/Dropout/GAP on geometric paths; orthogonal init;
|
| 26 |
+
Colab-cell-safe (paste-ahead imports, no bare argparse, no __file__ reliance);
|
| 27 |
+
GPU-only for verdict runs.
|
| 28 |
+
|
| 29 |
+
Terminal: python ar_differentiation_bed.py # shapes/parse smoke
|
| 30 |
+
python ar_differentiation_bed.py --train # verdict run
|
| 31 |
+
Colab: paste geolip_vitals.py cell, then this file (smoke auto-runs),
|
| 32 |
+
then train(steps=2000, data_root="/content/data") in the next cell.
|
| 33 |
+
"""
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
import math
|
| 36 |
+
import torch
|
| 37 |
+
import torch.nn as nn
|
| 38 |
+
import torch.nn.functional as F
|
| 39 |
+
|
| 40 |
+
if "anchor_drift" not in globals():
|
| 41 |
+
try:
|
| 42 |
+
from geolip_vitals import anchor_drift, axis_aliveness, path_diversity
|
| 43 |
+
except ImportError:
|
| 44 |
+
_here = globals().get("__file__")
|
| 45 |
+
if _here is not None:
|
| 46 |
+
import sys, pathlib
|
| 47 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 48 |
+
from geolip_vitals import anchor_drift, axis_aliveness, path_diversity
|
| 49 |
+
else:
|
| 50 |
+
raise ImportError(
|
| 51 |
+
"geolip_vitals not found — paste/run its cell first, or "
|
| 52 |
+
"hf_hub_download exp012_ar/geolip_vitals.py from "
|
| 53 |
+
"AbstractPhil/geolip-aleph-differentiation.")
|
| 54 |
+
|
| 55 |
+
VOCAB = 256 # bytes
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# ------------------------------------------------------------------ aleph address
|
| 59 |
+
def _super_fibonacci_s3(n: int) -> torch.Tensor:
|
| 60 |
+
"""Near-uniform unit quaternions (Alexa CVPR'22) —
|
| 61 |
+
starts the codebook INSIDE the RP^3 attractor basin. D=4 only."""
|
| 62 |
+
PHI, PSI = math.sqrt(2.0), 1.533751168755204288118041
|
| 63 |
+
i = torch.arange(n, dtype=torch.float64)
|
| 64 |
+
s = (i + 0.5) / n
|
| 65 |
+
r, R = torch.sqrt(s), torch.sqrt(1.0 - s)
|
| 66 |
+
a, b = 2 * math.pi * i / PHI, 2 * math.pi * i / PSI
|
| 67 |
+
q = torch.stack([r * torch.sin(a), r * torch.cos(a),
|
| 68 |
+
R * torch.sin(b), R * torch.cos(b)], dim=-1)
|
| 69 |
+
return F.normalize(q, dim=-1).float()
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class AlephAddress(nn.Module):
|
| 73 |
+
"""Closed-form aleph over 2K oriented half-axes (aleph-void article).
|
| 74 |
+
signed(x): (..., K) w_k = sinh(u_k)/sum_j cosh(u_j) — the Law-2 head feature.
|
| 75 |
+
oriented(x): ((..., K), (..., K)) positive halves of the 2K softmax — HUB map."""
|
| 76 |
+
|
| 77 |
+
def __init__(self, K: int, D: int, tau: float = 0.1, init: str = "random"):
|
| 78 |
+
super().__init__()
|
| 79 |
+
self.K, self.D, self.tau = K, D, tau
|
| 80 |
+
if init == "fibonacci":
|
| 81 |
+
assert D == 4, "fibonacci init lives on S^3 (D=4)"
|
| 82 |
+
A = _super_fibonacci_s3(K)
|
| 83 |
+
else:
|
| 84 |
+
A = F.normalize(torch.randn(K, D), dim=-1)
|
| 85 |
+
self.codebook = nn.Parameter(A)
|
| 86 |
+
self.register_buffer("home", self.codebook.detach().clone())
|
| 87 |
+
|
| 88 |
+
def _u(self, x):
|
| 89 |
+
A = F.normalize(self.codebook, dim=-1)
|
| 90 |
+
return (F.normalize(x, dim=-1) @ A.transpose(-1, -2)) / self.tau
|
| 91 |
+
|
| 92 |
+
def oriented(self, x):
|
| 93 |
+
u = self._u(x)
|
| 94 |
+
m = u.abs().amax(dim=-1, keepdim=True)
|
| 95 |
+
ep, en = torch.exp(u - m), torch.exp(-u - m)
|
| 96 |
+
Z = (ep + en).sum(dim=-1, keepdim=True)
|
| 97 |
+
return ep / Z, en / Z
|
| 98 |
+
|
| 99 |
+
def signed(self, x):
|
| 100 |
+
u = self._u(x)
|
| 101 |
+
m = u.abs().amax(dim=-1, keepdim=True)
|
| 102 |
+
ep, en = torch.exp(u - m), torch.exp(-u - m)
|
| 103 |
+
return (ep - en) / (ep + en).sum(dim=-1, keepdim=True)
|
| 104 |
+
|
| 105 |
+
def signed_at(self, x, taus):
|
| 106 |
+
"""Multi-tau stroboscope (rule of 3): signed coefficients at several
|
| 107 |
+
temperatures, concatenated — softer taus keep the vector dense while a
|
| 108 |
+
hard tau supplies the sign-code sharpness. v2 refinement (b)."""
|
| 109 |
+
A = F.normalize(self.codebook, dim=-1)
|
| 110 |
+
cos = F.normalize(x, dim=-1) @ A.transpose(-1, -2)
|
| 111 |
+
outs = []
|
| 112 |
+
for t in taus:
|
| 113 |
+
u = cos / t
|
| 114 |
+
m = u.abs().amax(dim=-1, keepdim=True)
|
| 115 |
+
ep, en = torch.exp(u - m), torch.exp(-u - m)
|
| 116 |
+
outs.append((ep - en) / (ep + en).sum(dim=-1, keepdim=True))
|
| 117 |
+
return torch.cat(outs, dim=-1)
|
| 118 |
+
|
| 119 |
+
def m_hat(self, x):
|
| 120 |
+
"""Closed-form soft read (decoders read M_hat, never M). v2 control (c)."""
|
| 121 |
+
u = self._u(x)
|
| 122 |
+
m = u.abs().amax(dim=-1, keepdim=True)
|
| 123 |
+
ep, en = torch.exp(u - m), torch.exp(-u - m)
|
| 124 |
+
A = F.normalize(self.codebook, dim=-1)
|
| 125 |
+
return ((ep - en) @ A) / (ep + en).sum(dim=-1, keepdim=True)
|
| 126 |
+
|
| 127 |
+
def m_hard_ste(self, x):
|
| 128 |
+
"""Hard mode (aleph-void article): M_hard = sign(cos_win) * A[win], straight-through to
|
| 129 |
+
the soft read — forward fully discrete SIGN CODE, backward soft gradient.
|
| 130 |
+
Legal per theme A (reconstructive sign code, not a one-hot roster pick)."""
|
| 131 |
+
u = self._u(x)
|
| 132 |
+
soft = self.m_hat(x)
|
| 133 |
+
win = u.abs().argmax(dim=-1)
|
| 134 |
+
A = F.normalize(self.codebook, dim=-1)
|
| 135 |
+
sign = torch.sign(torch.gather(u, -1, win.unsqueeze(-1))).squeeze(-1)
|
| 136 |
+
hard = sign.unsqueeze(-1) * A[win]
|
| 137 |
+
return hard + soft - soft.detach()
|
| 138 |
+
|
| 139 |
+
@torch.no_grad()
|
| 140 |
+
def vitals(self, x_sample) -> dict:
|
| 141 |
+
u = self._u(x_sample.reshape(-1, x_sample.shape[-1]))
|
| 142 |
+
p, n = self.oriented(x_sample.reshape(-1, x_sample.shape[-1]))
|
| 143 |
+
two_k = torch.cat([p, n], dim=-1)
|
| 144 |
+
win = two_k.argmax(dim=-1)
|
| 145 |
+
cos_win = (u.abs().amax(dim=-1) * self.tau) # winner |cos| — sign-code sat.
|
| 146 |
+
d = anchor_drift(self.codebook, self.home)
|
| 147 |
+
return {"drift": round(d["mean"], 4),
|
| 148 |
+
"binding_frac": round(d["binding_fraction"], 4),
|
| 149 |
+
"aliveness": axis_aliveness(two_k),
|
| 150 |
+
"win_cos_mean": round(cos_win.mean().item(), 4),
|
| 151 |
+
"paths": path_diversity(win)}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# ------------------------------------------------------------------------- blocks
|
| 155 |
+
class CausalSDPA(nn.Module):
|
| 156 |
+
def __init__(self, d: int, heads: int = 4):
|
| 157 |
+
super().__init__()
|
| 158 |
+
self.h = heads
|
| 159 |
+
self.qkv = nn.Linear(d, 3 * d, bias=False)
|
| 160 |
+
self.o = nn.Linear(d, d, bias=False)
|
| 161 |
+
nn.init.orthogonal_(self.qkv.weight); nn.init.orthogonal_(self.o.weight)
|
| 162 |
+
|
| 163 |
+
def forward(self, x):
|
| 164 |
+
B, n, d = x.shape
|
| 165 |
+
q, k, v = self.qkv(x).chunk(3, dim=-1)
|
| 166 |
+
q, k, v = (t.view(B, n, self.h, d // self.h).transpose(1, 2) for t in (q, k, v))
|
| 167 |
+
y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
|
| 168 |
+
return self.o(y.transpose(1, 2).reshape(B, n, d))
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class CausalHUB(nn.Module):
|
| 172 |
+
"""Causal aleph linear attention: prefix-sum memories over the two K-wide
|
| 173 |
+
halves of the oriented address; 2K never materialized; no selection event."""
|
| 174 |
+
|
| 175 |
+
def __init__(self, d: int, K: int = 32, D: int = 4, tau: float = 0.1):
|
| 176 |
+
super().__init__()
|
| 177 |
+
self.addr = AlephAddress(K, D, tau)
|
| 178 |
+
self.q = nn.Linear(d, D, bias=False)
|
| 179 |
+
self.k = nn.Linear(d, D, bias=False)
|
| 180 |
+
self.v = nn.Linear(d, d, bias=False)
|
| 181 |
+
self.o = nn.Linear(d, d, bias=False)
|
| 182 |
+
for m in (self.q, self.k, self.v, self.o):
|
| 183 |
+
nn.init.orthogonal_(m.weight)
|
| 184 |
+
|
| 185 |
+
def forward(self, x):
|
| 186 |
+
qp, qn = self.addr.oriented(self.q(x)) # (B, n, K)
|
| 187 |
+
kp, kn = self.addr.oriented(self.k(x))
|
| 188 |
+
v = self.v(x) # (B, n, d)
|
| 189 |
+
Sp = torch.cumsum(torch.einsum("bnk,bnd->bnkd", kp, v), dim=1)
|
| 190 |
+
Sn = torch.cumsum(torch.einsum("bnk,bnd->bnkd", kn, v), dim=1)
|
| 191 |
+
zp = torch.cumsum(kp, dim=1)
|
| 192 |
+
zn = torch.cumsum(kn, dim=1)
|
| 193 |
+
num = torch.einsum("bnk,bnkd->bnd", qp, Sp) + torch.einsum("bnk,bnkd->bnd", qn, Sn)
|
| 194 |
+
den = (qp * zp).sum(-1, keepdim=True) + (qn * zn).sum(-1, keepdim=True)
|
| 195 |
+
return self.o(num / den.clamp_min(1e-12))
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class MslRelay(nn.Module):
|
| 199 |
+
"""Depth-composition unit (chain-rule probe): multi-slot M_hat read entering
|
| 200 |
+
the trunk as a NEAR-ZERO gated residual (gate init -3.0, sigma~0.047 — theme D:
|
| 201 |
+
geometry enters as a nudge and grows only if it earns gradient)."""
|
| 202 |
+
|
| 203 |
+
def __init__(self, d: int, n_slots: int = 16, K: int = 64):
|
| 204 |
+
super().__init__()
|
| 205 |
+
self.n_slots = n_slots
|
| 206 |
+
self.proj = nn.Linear(d, n_slots * 4, bias=False)
|
| 207 |
+
self.out = nn.Linear(n_slots * 4, d, bias=False)
|
| 208 |
+
nn.init.orthogonal_(self.proj.weight)
|
| 209 |
+
nn.init.orthogonal_(self.out.weight)
|
| 210 |
+
self.addr = AlephAddress(K, 4)
|
| 211 |
+
self.gate = nn.Parameter(torch.tensor(-3.0))
|
| 212 |
+
|
| 213 |
+
def forward(self, x):
|
| 214 |
+
B, n, _ = x.shape
|
| 215 |
+
slots = self.proj(x).view(B, n, self.n_slots, 4)
|
| 216 |
+
m = self.addr.m_hat(slots).reshape(B, n, -1)
|
| 217 |
+
return x + self.gate.sigmoid() * self.out(m)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class Block(nn.Module):
|
| 221 |
+
def __init__(self, d: int, attn: nn.Module):
|
| 222 |
+
super().__init__()
|
| 223 |
+
self.n1, self.n2 = nn.LayerNorm(d), nn.LayerNorm(d)
|
| 224 |
+
self.attn = attn
|
| 225 |
+
self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
|
| 226 |
+
|
| 227 |
+
def forward(self, x):
|
| 228 |
+
x = x + self.attn(self.n1(x))
|
| 229 |
+
return x + self.mlp(self.n2(x))
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class ByteLM(nn.Module):
|
| 233 |
+
def __init__(self, arm: str, d: int = 192, layers: int = 4, block: int = 256,
|
| 234 |
+
K: int = 32, D: int = 4):
|
| 235 |
+
super().__init__()
|
| 236 |
+
# "<arm>_tri" suffix = trigram byte embedding (AlephLM byte_emb x3 lineage):
|
| 237 |
+
# token embedding is the sum of embeddings of bytes t, t-1, t-2.
|
| 238 |
+
self.trigram = arm.endswith("_tri")
|
| 239 |
+
if self.trigram:
|
| 240 |
+
arm = arm[:-4]
|
| 241 |
+
# "_fib" = super-Fibonacci S^3 codebook init (basin test: starts INSIDE
|
| 242 |
+
# the RP^3 attractor; primary observable is init->final geodesic drift).
|
| 243 |
+
self.fib = arm.endswith("_fib")
|
| 244 |
+
if self.fib:
|
| 245 |
+
arm = arm[:-4]
|
| 246 |
+
# "relay*" = stacked addresses in depth: MslRelay after every block.
|
| 247 |
+
# relay -> sdpa trunk + standard head; relay_msl64 -> + addressed head.
|
| 248 |
+
self.use_relay = arm.startswith("relay")
|
| 249 |
+
if arm == "relay":
|
| 250 |
+
arm = "sdpa"
|
| 251 |
+
elif arm == "relay_msl64":
|
| 252 |
+
arm = "addr_msl64"
|
| 253 |
+
self.arm, self.block = arm, block
|
| 254 |
+
self.emb = nn.Embedding(VOCAB, d)
|
| 255 |
+
if self.trigram:
|
| 256 |
+
self.emb1 = nn.Embedding(VOCAB, d)
|
| 257 |
+
self.emb2 = nn.Embedding(VOCAB, d)
|
| 258 |
+
self.pos = nn.Parameter(torch.zeros(1, block, d) + 0.01 * torch.randn(1, block, d))
|
| 259 |
+
mk_attn = (lambda: CausalHUB(d, K, D)) if arm == "hub" else (lambda: CausalSDPA(d))
|
| 260 |
+
self.blocks = nn.ModuleList([Block(d, mk_attn()) for _ in range(layers)])
|
| 261 |
+
if self.use_relay:
|
| 262 |
+
self.relays = nn.ModuleList([MslRelay(d) for _ in range(layers)])
|
| 263 |
+
self.nf = nn.LayerNorm(d)
|
| 264 |
+
if arm == "addr_head":
|
| 265 |
+
self.head_addr = AlephAddress(K, d) # v1: codebook in model dim — COLLAPSED
|
| 266 |
+
self.head = nn.Linear(K, VOCAB, bias=True)
|
| 267 |
+
elif arm in ("addr_d4", "addr_3tau", "addr_mhat"):
|
| 268 |
+
# v2 refinements: LOW-D HOME — learned projection to the native D=4 home
|
| 269 |
+
# before addressing (mirrors the healthy HUB arms), K=64.
|
| 270 |
+
self.head_proj = nn.Linear(d, 4, bias=False)
|
| 271 |
+
nn.init.orthogonal_(self.head_proj.weight)
|
| 272 |
+
self.head_addr = AlephAddress(64, 4)
|
| 273 |
+
if arm == "addr_d4":
|
| 274 |
+
self.head = nn.Linear(64, VOCAB, bias=True) # w alone, D=4 home
|
| 275 |
+
elif arm == "addr_3tau":
|
| 276 |
+
self.taus = (0.05, 0.1, 0.3) # rule-of-3 strobe
|
| 277 |
+
self.head = nn.Linear(64 * 3, VOCAB, bias=True)
|
| 278 |
+
else: # addr_mhat
|
| 279 |
+
self.head = nn.Linear(4, VOCAB, bias=True) # tightest: M_hat
|
| 280 |
+
elif arm.startswith("addr_msl"):
|
| 281 |
+
# v3: MULTI-SLOT heads — the 16s funnel widening: P parallel D=4 slots
|
| 282 |
+
# over a SHARED codebook. addr_msl consumes the reconstructive M_hat per
|
| 283 |
+
# slot (Px4 dims); addr_msl_w consumes signed w per slot (Px64) — tests
|
| 284 |
+
# whether slot-parallel consumption alone rescues the coefficient path.
|
| 285 |
+
# addr_msl<P> = slot-count dose-response. addr_mslh<P> = HARD sign-code
|
| 286 |
+
# consumption (straight-through M_hard per slot).
|
| 287 |
+
self.hard = arm.startswith("addr_mslh")
|
| 288 |
+
if arm in ("addr_msl", "addr_msl_w"):
|
| 289 |
+
self.n_slots = 16
|
| 290 |
+
else:
|
| 291 |
+
self.n_slots = int(arm[len("addr_mslh" if self.hard else "addr_msl"):])
|
| 292 |
+
self.head_proj = nn.Linear(d, self.n_slots * 4, bias=False)
|
| 293 |
+
nn.init.orthogonal_(self.head_proj.weight)
|
| 294 |
+
self.head_addr = AlephAddress(
|
| 295 |
+
64, 4, init="fibonacci" if self.fib else "random")
|
| 296 |
+
width = self.n_slots * (64 if arm == "addr_msl_w" else 4)
|
| 297 |
+
self.head = nn.Linear(width, VOCAB, bias=True)
|
| 298 |
+
elif arm == "addr_3tau_mhat":
|
| 299 |
+
# v3: combine the two v2 winners — 3-tau stroboscope + reconstructive read.
|
| 300 |
+
self.head_proj = nn.Linear(d, 4, bias=False)
|
| 301 |
+
nn.init.orthogonal_(self.head_proj.weight)
|
| 302 |
+
self.head_addr = AlephAddress(64, 4)
|
| 303 |
+
self.taus = (0.05, 0.1, 0.3)
|
| 304 |
+
self.head = nn.Linear(64 * 3 + 4, VOCAB, bias=True)
|
| 305 |
+
else:
|
| 306 |
+
self.head = nn.Linear(d, VOCAB, bias=True)
|
| 307 |
+
self._last_h = None
|
| 308 |
+
|
| 309 |
+
def forward(self, idx):
|
| 310 |
+
x = self.emb(idx)
|
| 311 |
+
if self.trigram: # past-only shifts — causality preserved
|
| 312 |
+
x = x + self.emb1(F.pad(idx, (1, 0), value=0)[:, :-1]) \
|
| 313 |
+
+ self.emb2(F.pad(idx, (2, 0), value=0)[:, :-2])
|
| 314 |
+
x = x + self.pos[:, : idx.shape[1]]
|
| 315 |
+
if self.use_relay:
|
| 316 |
+
for b, r in zip(self.blocks, self.relays):
|
| 317 |
+
x = r(b(x))
|
| 318 |
+
else:
|
| 319 |
+
for b in self.blocks:
|
| 320 |
+
x = b(x)
|
| 321 |
+
h = self.nf(x)
|
| 322 |
+
self._last_h = h.detach()
|
| 323 |
+
if self.arm == "addr_head":
|
| 324 |
+
return self.head(self.head_addr.signed(h))
|
| 325 |
+
if self.arm == "addr_d4":
|
| 326 |
+
return self.head(self.head_addr.signed(self.head_proj(h)))
|
| 327 |
+
if self.arm == "addr_3tau":
|
| 328 |
+
return self.head(self.head_addr.signed_at(self.head_proj(h), self.taus))
|
| 329 |
+
if self.arm == "addr_mhat":
|
| 330 |
+
return self.head(self.head_addr.m_hat(self.head_proj(h)))
|
| 331 |
+
if self.arm.startswith("addr_msl"):
|
| 332 |
+
B, n, _ = h.shape
|
| 333 |
+
slots = self.head_proj(h).view(B, n, self.n_slots, 4)
|
| 334 |
+
if self.arm == "addr_msl_w":
|
| 335 |
+
feats = self.head_addr.signed(slots).reshape(B, n, -1)
|
| 336 |
+
elif getattr(self, "hard", False):
|
| 337 |
+
feats = self.head_addr.m_hard_ste(slots).reshape(B, n, -1)
|
| 338 |
+
else:
|
| 339 |
+
feats = self.head_addr.m_hat(slots).reshape(B, n, -1)
|
| 340 |
+
return self.head(feats)
|
| 341 |
+
if self.arm == "addr_3tau_mhat":
|
| 342 |
+
p = self.head_proj(h)
|
| 343 |
+
feats = torch.cat([self.head_addr.signed_at(p, self.taus),
|
| 344 |
+
self.head_addr.m_hat(p)], dim=-1)
|
| 345 |
+
return self.head(feats)
|
| 346 |
+
return self.head(h)
|
| 347 |
+
|
| 348 |
+
@torch.no_grad()
|
| 349 |
+
def vitals(self) -> dict:
|
| 350 |
+
out = {}
|
| 351 |
+
if self.arm == "hub":
|
| 352 |
+
for i, b in enumerate(self.blocks):
|
| 353 |
+
if self._last_h is not None:
|
| 354 |
+
out[f"L{i}"] = b.attn.addr.vitals(b.attn.q(self._last_h[:2]))
|
| 355 |
+
elif self.arm == "addr_head" and self._last_h is not None:
|
| 356 |
+
out["head"] = self.head_addr.vitals(self._last_h[:2])
|
| 357 |
+
elif self.arm in ("addr_d4", "addr_3tau", "addr_mhat",
|
| 358 |
+
"addr_3tau_mhat") and self._last_h is not None:
|
| 359 |
+
out["head"] = self.head_addr.vitals(self.head_proj(self._last_h[:2]))
|
| 360 |
+
elif self.arm.startswith("addr_msl") and self._last_h is not None:
|
| 361 |
+
slots = self.head_proj(self._last_h[:2])
|
| 362 |
+
out["head"] = self.head_addr.vitals(
|
| 363 |
+
slots.reshape(*slots.shape[:-1], self.n_slots, 4))
|
| 364 |
+
if self.use_relay and self._last_h is not None:
|
| 365 |
+
for i, r in enumerate(self.relays):
|
| 366 |
+
s = r.proj(self._last_h[:2])
|
| 367 |
+
v = r.addr.vitals(s.reshape(*s.shape[:-1], r.n_slots, 4))
|
| 368 |
+
out[f"relay{i}"] = {"gate": round(r.gate.sigmoid().item(), 4),
|
| 369 |
+
"drift": v["drift"],
|
| 370 |
+
"binding_frac": v["binding_frac"],
|
| 371 |
+
"ppl": round(v["aliveness"]["usage_ppl"], 1)}
|
| 372 |
+
return out
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
# --------------------------------------------------------------------------- data
|
| 376 |
+
def _wikitext_bytes(data_root: str):
|
| 377 |
+
"""wikitext-2-raw as flat uint8 tensors via the HF parquet CDN."""
|
| 378 |
+
from huggingface_hub import hf_hub_download
|
| 379 |
+
import pyarrow.parquet as pq
|
| 380 |
+
|
| 381 |
+
def load(split):
|
| 382 |
+
p = hf_hub_download("Salesforce/wikitext",
|
| 383 |
+
f"wikitext-2-raw-v1/{split}-00000-of-00001.parquet",
|
| 384 |
+
repo_type="dataset", local_dir=data_root)
|
| 385 |
+
text = "".join(pq.read_table(p).column("text").to_pylist())
|
| 386 |
+
return torch.frombuffer(bytearray(text.encode("utf-8")), dtype=torch.uint8).clone()
|
| 387 |
+
|
| 388 |
+
return load("train"), load("validation")
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def _batch(data: torch.Tensor, batch: int, block: int, device, g: torch.Generator):
|
| 392 |
+
ix = torch.randint(0, data.numel() - block - 1, (batch,), generator=g)
|
| 393 |
+
x = torch.stack([data[i:i + block] for i in ix]).long().to(device)
|
| 394 |
+
y = torch.stack([data[i + 1:i + block + 1] for i in ix]).long().to(device)
|
| 395 |
+
return x, y
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
# -------------------------------------------------------------------- train/smoke
|
| 399 |
+
def train(arms=("sdpa", "hub", "addr_head"), steps: int = 2000, batch: int = 32,
|
| 400 |
+
block: int = 256, device: str = "cuda", data_root: str = "./data",
|
| 401 |
+
seed: int = 0, eval_every: int = 500, save: bool = True):
|
| 402 |
+
"""Verdict run — GPU only. Pure Adam wd=0. Reports val bits-per-byte + vitals.
|
| 403 |
+
save=True writes {data_root}/ar_ckpts/{arm}_s{seed}_t{steps}.pt per arm —
|
| 404 |
+
the cultivated codebooks are SPECIMENS for the projective reading instruments."""
|
| 405 |
+
import os
|
| 406 |
+
if device == "cuda" and not torch.cuda.is_available():
|
| 407 |
+
raise RuntimeError("Verdict runs are GPU-only (never CPU-train for accuracy).")
|
| 408 |
+
ckpt_dir = os.path.join(data_root, "ar_ckpts")
|
| 409 |
+
os.makedirs(ckpt_dir, exist_ok=True)
|
| 410 |
+
tr, va = _wikitext_bytes(data_root)
|
| 411 |
+
print(f"data ready: train {tr.numel():,} bytes, val {va.numel():,} bytes", flush=True)
|
| 412 |
+
results = {}
|
| 413 |
+
for arm in arms:
|
| 414 |
+
torch.manual_seed(seed)
|
| 415 |
+
g = torch.Generator().manual_seed(seed)
|
| 416 |
+
model = ByteLM(arm, block=block).to(device)
|
| 417 |
+
n_params = sum(p.numel() for p in model.parameters())
|
| 418 |
+
opt = torch.optim.Adam(model.parameters(), lr=3e-4, weight_decay=0.0)
|
| 419 |
+
for step in range(1, steps + 1):
|
| 420 |
+
x, y = _batch(tr, batch, block, device, g)
|
| 421 |
+
logits = model(x)
|
| 422 |
+
loss = F.cross_entropy(logits.reshape(-1, VOCAB), y.reshape(-1))
|
| 423 |
+
opt.zero_grad(set_to_none=True)
|
| 424 |
+
loss.backward()
|
| 425 |
+
opt.step()
|
| 426 |
+
if step % eval_every == 0 or step == steps:
|
| 427 |
+
model.eval()
|
| 428 |
+
with torch.no_grad():
|
| 429 |
+
losses = []
|
| 430 |
+
for _ in range(20):
|
| 431 |
+
xv, yv = _batch(va, batch, block, device, g)
|
| 432 |
+
lv = F.cross_entropy(model(xv).reshape(-1, VOCAB),
|
| 433 |
+
yv.reshape(-1))
|
| 434 |
+
losses.append(lv.item())
|
| 435 |
+
bpb = sum(losses) / len(losses) / math.log(2)
|
| 436 |
+
print(f"[{arm}] step {step} val_bpb={bpb:.4f} vitals={model.vitals()}",
|
| 437 |
+
flush=True)
|
| 438 |
+
model.train()
|
| 439 |
+
results[arm] = {"val_bpb": bpb, "params": n_params, "vitals": model.vitals()}
|
| 440 |
+
if save:
|
| 441 |
+
path = os.path.join(ckpt_dir, f"{arm}_s{seed}_t{steps}.pt")
|
| 442 |
+
torch.save({"arm": arm, "seed": seed, "steps": steps, "val_bpb": bpb,
|
| 443 |
+
"state_dict": {k: v.cpu() for k, v in
|
| 444 |
+
model.state_dict().items()}}, path)
|
| 445 |
+
print(f"saved specimen: {path}", flush=True)
|
| 446 |
+
print(results, flush=True)
|
| 447 |
+
return results
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def smoke():
|
| 451 |
+
"""Shapes/parse only — no accuracy claims."""
|
| 452 |
+
x = torch.randint(0, VOCAB, (2, 64))
|
| 453 |
+
for arm in ("sdpa", "hub", "addr_head"):
|
| 454 |
+
m = ByteLM(arm, d=96, layers=2, block=64, K=16)
|
| 455 |
+
logits = m(x)
|
| 456 |
+
assert logits.shape == (2, 64, VOCAB)
|
| 457 |
+
logits.sum().backward()
|
| 458 |
+
# causality check: future byte must not affect past logits
|
| 459 |
+
with torch.no_grad():
|
| 460 |
+
a = m(x)[0, 10]
|
| 461 |
+
x2 = x.clone(); x2[0, 40] = (x2[0, 40] + 7) % 256
|
| 462 |
+
b = m(x2)[0, 10]
|
| 463 |
+
assert torch.allclose(a, b, atol=1e-4), f"{arm} leaks future context"
|
| 464 |
+
print(f"{arm}: OK params={sum(p.numel() for p in m.parameters()):,} "
|
| 465 |
+
f"vitals={m.vitals()}", flush=True)
|
| 466 |
+
print("OK — AR bed smoke passed (verdict run: train() on GPU)", flush=True)
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def _in_notebook() -> bool:
|
| 470 |
+
try:
|
| 471 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 472 |
+
return True
|
| 473 |
+
except NameError:
|
| 474 |
+
return False
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
if __name__ == "__main__":
|
| 478 |
+
if _in_notebook():
|
| 479 |
+
smoke()
|
| 480 |
+
print("Notebook mode: call train(steps=2000) in the next cell (GPU).")
|
| 481 |
+
else:
|
| 482 |
+
import argparse
|
| 483 |
+
ap = argparse.ArgumentParser()
|
| 484 |
+
ap.add_argument("--train", action="store_true")
|
| 485 |
+
ap.add_argument("--steps", type=int, default=2000)
|
| 486 |
+
a, _ = ap.parse_known_args()
|
| 487 |
+
train(steps=a.steps) if a.train else smoke()
|
exp007_math/build_results.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""build_results.py — exp007_math: read results/ledger.jsonl and RE-ASSERT
|
| 2 |
+
every claim in the README. The ledger carries two rows: the FROZEN trunk
|
| 3 |
+
baseline (separates "the trunk can already add" from what the anchor adds)
|
| 4 |
+
and the trained relay_pw_math arm (one relay_pw stack, all four ask-method
|
| 5 |
+
registers interleaved). Run from inside this folder: python build_results.py
|
| 6 |
+
"""
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
rows = [json.loads(l) for l in
|
| 12 |
+
open(os.path.join(HERE, "results", "ledger.jsonl"), encoding="utf-8")]
|
| 13 |
+
assert all(r["exp"] == "q7" for r in rows) and len(rows) == 2
|
| 14 |
+
|
| 15 |
+
frozen = next(r for r in rows if r["arm"] == "frozen")
|
| 16 |
+
trained = next(r for r in rows if r["arm"] == "relay_pw_math")
|
| 17 |
+
assert frozen["seed"] == 0 and frozen["steps"] == 0
|
| 18 |
+
assert trained["seed"] == 0 and trained["steps"] == 3000
|
| 19 |
+
REGS = ("nl", "json", "code", "word")
|
| 20 |
+
|
| 21 |
+
# same synthetic corpus under both arms (train-side cache stats identical)
|
| 22 |
+
assert frozen["cache_stats"] == trained["cache_stats"] == \
|
| 23 |
+
{"nl": 696, "json": 702, "code": 1496, "word": 718}
|
| 24 |
+
|
| 25 |
+
fv, tv = frozen["validity"], trained["validity"]
|
| 26 |
+
|
| 27 |
+
# claim 1a: format lock-in — the frozen trunk fails FORMAT only on nl
|
| 28 |
+
# (0.312: it answers correctly but not in the asked shape; json/code/word
|
| 29 |
+
# already 1.0); after training all four registers reach format 1.0.
|
| 30 |
+
assert fv["nl"]["format"] == 0.312
|
| 31 |
+
assert fv["json"]["format"] == 1.0
|
| 32 |
+
assert fv["code"]["format"] == 1.0
|
| 33 |
+
assert fv["word"]["format"] == 1.0
|
| 34 |
+
assert all(tv[r]["format"] == 1.0 for r in REGS)
|
| 35 |
+
|
| 36 |
+
# claim 1b: no correctness tax — correctness never degrades on any register:
|
| 37 |
+
# nl and json hold 1.0 -> 1.0, word 0.938 -> 1.0, code improves (claim 2).
|
| 38 |
+
assert fv["nl"]["correct"] == 1.0 and tv["nl"]["correct"] == 1.0
|
| 39 |
+
assert fv["json"]["correct"] == 1.0 and tv["json"]["correct"] == 1.0
|
| 40 |
+
assert fv["word"]["correct"] == 0.938 and tv["word"]["correct"] == 1.0
|
| 41 |
+
assert all(tv[r]["correct"] >= fv[r]["correct"] for r in REGS)
|
| 42 |
+
|
| 43 |
+
# claim 2: computation through the adapters — code-register held-out
|
| 44 |
+
# correctness 0.688 -> 0.938 on 16 f(x)-rendered questions never seen in
|
| 45 |
+
# training (audited: 5/16 are true a*x+b instances unseen anywhere in the
|
| 46 |
+
# corpus; 4/16 wrapped-arithmetic items share their a-op-b fact with
|
| 47 |
+
# another register's training text — see README claim 2). Not target
|
| 48 |
+
# recall. Frozen baseline ledgered.
|
| 49 |
+
assert fv["code"]["correct"] == 0.688
|
| 50 |
+
assert tv["code"]["correct"] == 0.938
|
| 51 |
+
|
| 52 |
+
# claim 3 (the predictability principle) is a CROSS-EXPERIMENT
|
| 53 |
+
# interpretation — exp006's keyword compliance (0.083 -> 0.083, exp006
|
| 54 |
+
# ledger) vs correctness moving here — and is NOT asserted from this
|
| 55 |
+
# ledger. Its local anchor IS asserted: held-out LM loss improved on
|
| 56 |
+
# every register (the loss had something to teach, and correctness moved
|
| 57 |
+
# exactly where target tokens are unpredictable without the question).
|
| 58 |
+
assert frozen["ppl"] == {"nl": 2.763, "json": 4.393,
|
| 59 |
+
"code": 2.591, "word": 3.145}
|
| 60 |
+
assert trained["ppl"] == {"nl": 1.342, "json": 1.061,
|
| 61 |
+
"code": 1.168, "word": 1.525}
|
| 62 |
+
assert all(trained["ppl"][r] < frozen["ppl"][r] for r in REGS)
|
| 63 |
+
|
| 64 |
+
# claim 4: register separation, deepest PEAK yet in the line — sign-code
|
| 65 |
+
# sep across the 4 ask-methods 0.4711 @ L16 and 0.4957 @ L23 (exp006 story
|
| 66 |
+
# registers 0.3476 / 0.3451 at those taps, own peak 0.4619 @ L8; exp003
|
| 67 |
+
# instruct arms <= 0.304 at L16/L23 — cited from their ledgers, not
|
| 68 |
+
# asserted here). Separation grows monotonically with depth in THIS run;
|
| 69 |
+
# depth profiles differ across beds. Frozen arm has no adapters, no probe.
|
| 70 |
+
sep = trained["register_sep"]
|
| 71 |
+
assert sep["L16"]["sep"] == 0.4711
|
| 72 |
+
assert sep["L23"]["sep"] == 0.4957
|
| 73 |
+
assert sep["L0"]["sep"] == 0.2145 and sep["L8"]["sep"] == 0.2808
|
| 74 |
+
assert sep["L0"]["sep"] < sep["L8"]["sep"] < sep["L16"]["sep"] \
|
| 75 |
+
< sep["L23"]["sep"]
|
| 76 |
+
assert frozen["register_sep"] is None
|
| 77 |
+
|
| 78 |
+
out = {"frozen": {"ppl": frozen["ppl"], "validity": fv},
|
| 79 |
+
"relay_pw_math": {"steps": trained["steps"],
|
| 80 |
+
"ppl": trained["ppl"],
|
| 81 |
+
"validity": tv,
|
| 82 |
+
"register_sep": sep},
|
| 83 |
+
"cache_stats": trained["cache_stats"],
|
| 84 |
+
"n_rows": len(rows)}
|
| 85 |
+
json.dump(out, open(os.path.join(HERE, "results", "results.json"), "w",
|
| 86 |
+
encoding="utf-8"), indent=1)
|
| 87 |
+
print(f"{len(rows)} rows -> results/results.json")
|
| 88 |
+
print("all README claims asserted OK")
|
exp007_math/exp013_augmentation_bed.py
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""exp013_augmentation_bed.py — augmenting pretrained models with the aleph.
|
| 2 |
+
Three tracks, one file (sequel to exp012's ar_differentiation_bed):
|
| 3 |
+
|
| 4 |
+
A. AUTOREGRESSION FROM CLIP-L: next-token prediction over CLIP-tokenized text,
|
| 5 |
+
reading FROZEN openai/clip-vit-large-patch14 text-tower hidden states.
|
| 6 |
+
FACTOR: extraction layer in {final, penultimate} (the last two layers — the
|
| 7 |
+
penultimate is what diffusion stacks consume). Heads at ~matched params:
|
| 8 |
+
linear | mlp | aleph multi-slot M_hat (P=64, D=4, shared K=64 — the exp012
|
| 9 |
+
certified construction) | sign-code (straight-through).
|
| 10 |
+
B. JOINT-FAILURE PROBES on frozen pooled embeddings (CLIP-L both layers + BERT):
|
| 11 |
+
b1 SPELLING-AR — decode a word's characters from ONLY the head's read of its
|
| 12 |
+
pooled embedding (GATE: linear/mlp must fail <50% exact first);
|
| 13 |
+
b2 ORDER — original-vs-shuffled discrimination (secondary; info may be absent).
|
| 14 |
+
C. GPT-2 (124M) AUGMENTATION: frozen trunk + trainable adapters after every block —
|
| 15 |
+
aleph MslRelay adapters vs param-matched MLP adapters vs frozen baseline;
|
| 16 |
+
gate growth by depth is a first-class readout (exp012 depth-gradient law).
|
| 17 |
+
|
| 18 |
+
Riders: pure Adam wd=0; no contrastive/InfoNCE into address paths; vitals are
|
| 19 |
+
readouts; GPU-only for verdict runs; caches/specimens live OUTSIDE the repo.
|
| 20 |
+
Colab: paste geolip_vitals.py, then ar_differentiation_bed.py, then this file.
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
import json
|
| 24 |
+
import math
|
| 25 |
+
import os
|
| 26 |
+
import re
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
|
| 31 |
+
# ---- paste-ahead imports (notebook-safe) -----------------------------------------
|
| 32 |
+
if "anchor_drift" not in globals():
|
| 33 |
+
try:
|
| 34 |
+
from geolip_vitals import anchor_drift, axis_aliveness, path_diversity
|
| 35 |
+
except ImportError:
|
| 36 |
+
_here = globals().get("__file__")
|
| 37 |
+
if _here is None:
|
| 38 |
+
raise ImportError("paste/run geolip_vitals.py first")
|
| 39 |
+
import sys, pathlib
|
| 40 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 41 |
+
from geolip_vitals import anchor_drift, axis_aliveness, path_diversity
|
| 42 |
+
if "AlephAddress" not in globals():
|
| 43 |
+
try:
|
| 44 |
+
from ar_differentiation_bed import AlephAddress, MslRelay, _wikitext_bytes
|
| 45 |
+
except ImportError:
|
| 46 |
+
_here = globals().get("__file__")
|
| 47 |
+
if _here is None:
|
| 48 |
+
raise ImportError("paste/run ar_differentiation_bed.py first")
|
| 49 |
+
from ar_differentiation_bed import AlephAddress, MslRelay, _wikitext_bytes
|
| 50 |
+
|
| 51 |
+
DATA_ROOT = os.environ.get("GEOLIP_DATA", "./data")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class SquaredReLU(nn.Module):
|
| 55 |
+
def forward(self, x):
|
| 56 |
+
return F.relu(x) ** 2
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# =============================================================== head arm family ==
|
| 60 |
+
class HeadArm(nn.Module):
|
| 61 |
+
"""Conditioning transform d_in -> F_OUT (256), then a shared-shape task head.
|
| 62 |
+
Arms: linear | mlp | aleph (multi-slot M_hat) | sign (straight-through M_hard).
|
| 63 |
+
The aleph arms are the exp012-certified construction: P=64 slots x D=4 over one
|
| 64 |
+
shared K=64 codebook."""
|
| 65 |
+
F_OUT = 256
|
| 66 |
+
|
| 67 |
+
def __init__(self, arm: str, d_in: int, mlp_hidden: int = 192):
|
| 68 |
+
# mlp_hidden=192 param-matches the aleph arm at d_in=768:
|
| 69 |
+
# mlp ~ 192*(768+256)+LN ~ 197K vs aleph proj 768*256 + codebook = 196.9K
|
| 70 |
+
super().__init__()
|
| 71 |
+
self.arm = arm
|
| 72 |
+
if arm == "linear":
|
| 73 |
+
self.net = nn.Linear(d_in, self.F_OUT, bias=False)
|
| 74 |
+
elif arm == "mlp":
|
| 75 |
+
self.net = nn.Sequential(nn.Linear(d_in, mlp_hidden), SquaredReLU(),
|
| 76 |
+
nn.LayerNorm(mlp_hidden),
|
| 77 |
+
nn.Linear(mlp_hidden, self.F_OUT))
|
| 78 |
+
elif arm in ("aleph", "sign"):
|
| 79 |
+
self.proj = nn.Linear(d_in, 64 * 4, bias=False)
|
| 80 |
+
nn.init.orthogonal_(self.proj.weight)
|
| 81 |
+
self.addr = AlephAddress(64, 4)
|
| 82 |
+
else:
|
| 83 |
+
raise ValueError(arm)
|
| 84 |
+
|
| 85 |
+
def forward(self, x):
|
| 86 |
+
if self.arm in ("linear", "mlp"):
|
| 87 |
+
return self.net(x)
|
| 88 |
+
slots = self.proj(x).reshape(*x.shape[:-1], 64, 4)
|
| 89 |
+
read = self.addr.m_hard_ste(slots) if self.arm == "sign" \
|
| 90 |
+
else self.addr.m_hat(slots)
|
| 91 |
+
return read.reshape(*x.shape[:-1], 256)
|
| 92 |
+
|
| 93 |
+
@torch.no_grad()
|
| 94 |
+
def vitals(self, x_sample) -> dict:
|
| 95 |
+
if self.arm in ("linear", "mlp"):
|
| 96 |
+
return {}
|
| 97 |
+
slots = self.proj(x_sample).reshape(*x_sample.shape[:-1], 64, 4)
|
| 98 |
+
p, n = self.addr.oriented(slots)
|
| 99 |
+
two_k = torch.cat([p, n], -1).reshape(-1, 128)
|
| 100 |
+
d = anchor_drift(self.addr.codebook, self.addr.home)
|
| 101 |
+
return {"drift": round(d["mean"], 4),
|
| 102 |
+
"binding_frac": round(d["binding_fraction"], 4),
|
| 103 |
+
"usage_ppl": round(axis_aliveness(two_k)["usage_ppl"], 1),
|
| 104 |
+
"paths": path_diversity(two_k.argmax(-1))["unique_hashed"]}
|
| 105 |
+
|
| 106 |
+
def param_count(self):
|
| 107 |
+
return sum(p.numel() for p in self.parameters())
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# ================================================================== caches ========
|
| 111 |
+
def _wikitext_lines(data_root, min_chars=40, max_lines=None):
|
| 112 |
+
from huggingface_hub import hf_hub_download
|
| 113 |
+
import pyarrow.parquet as pq
|
| 114 |
+
out = {}
|
| 115 |
+
for split, cap in (("train", max_lines), ("validation", None)):
|
| 116 |
+
p = hf_hub_download("Salesforce/wikitext",
|
| 117 |
+
f"wikitext-2-raw-v1/{split}-00000-of-00001.parquet",
|
| 118 |
+
repo_type="dataset", local_dir=data_root)
|
| 119 |
+
lines = [t.strip() for t in pq.read_table(p).column("text").to_pylist()
|
| 120 |
+
if len(t.strip()) >= min_chars]
|
| 121 |
+
out[split] = lines[:cap] if cap else lines
|
| 122 |
+
return out["train"], out["validation"]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@torch.no_grad()
|
| 126 |
+
def cache_clip(data_root, n_train=12000, n_val=1500, device="cuda", batch=64):
|
| 127 |
+
"""CLIP-L text tower over wikitext lines; caches BOTH of the last two layers.
|
| 128 |
+
hidden_states[-1] == the final encoder layer output (pre final-LN),
|
| 129 |
+
last_hidden_state == final-LN(final layer). We cache:
|
| 130 |
+
'final' = last_hidden_state (what the projection head consumes),
|
| 131 |
+
'penult' = hidden_states[-2] (the layer diffusion stacks consume).
|
| 132 |
+
Also caches pooled (EOS-position) vectors for both layers, and token ids."""
|
| 133 |
+
from transformers import CLIPTextModel, CLIPTokenizerFast
|
| 134 |
+
path = os.path.join(data_root, "exp013", "clip_cache.pt")
|
| 135 |
+
if os.path.exists(path):
|
| 136 |
+
return torch.load(path, map_location="cpu", weights_only=True)
|
| 137 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 138 |
+
tok = CLIPTokenizerFast.from_pretrained("openai/clip-vit-large-patch14")
|
| 139 |
+
model = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14").to(device).eval()
|
| 140 |
+
tr_lines, va_lines = _wikitext_lines(data_root, max_lines=n_train)
|
| 141 |
+
va_lines = va_lines[:n_val]
|
| 142 |
+
def encode(lines):
|
| 143 |
+
H_f, H_p, IDS, EOS = [], [], [], []
|
| 144 |
+
for i in range(0, len(lines), batch):
|
| 145 |
+
enc = tok(lines[i:i + batch], padding="max_length", truncation=True,
|
| 146 |
+
max_length=77, return_tensors="pt").to(device)
|
| 147 |
+
out = model(**enc, output_hidden_states=True)
|
| 148 |
+
H_f.append(out.last_hidden_state.half().cpu())
|
| 149 |
+
H_p.append(out.hidden_states[-2].half().cpu())
|
| 150 |
+
IDS.append(enc.input_ids.cpu())
|
| 151 |
+
EOS.append(enc.input_ids.argmax(-1).cpu()) # EOT id is the max token id
|
| 152 |
+
return (torch.cat(H_f), torch.cat(H_p), torch.cat(IDS), torch.cat(EOS))
|
| 153 |
+
tr = encode(tr_lines)
|
| 154 |
+
va = encode(va_lines)
|
| 155 |
+
blob = {"train": {"final": tr[0], "penult": tr[1], "ids": tr[2], "eos": tr[3]},
|
| 156 |
+
"val": {"final": va[0], "penult": va[1], "ids": va[2], "eos": va[3]},
|
| 157 |
+
"vocab": tok.vocab_size}
|
| 158 |
+
torch.save(blob, path)
|
| 159 |
+
print(f"clip cache: train {tr[0].shape}, val {va[0].shape} -> {path}", flush=True)
|
| 160 |
+
return blob
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@torch.no_grad()
|
| 164 |
+
def cache_word_embeddings(data_root, n_words=10000, device="cuda", batch=256):
|
| 165 |
+
"""Pooled embeddings of frequent wikitext words for the spelling probe:
|
| 166 |
+
CLIP-L final + penultimate (EOS-pooled) and BERT (CLS + mean of last layer)."""
|
| 167 |
+
from transformers import (CLIPTextModel, CLIPTokenizerFast,
|
| 168 |
+
BertModel, BertTokenizerFast)
|
| 169 |
+
path = os.path.join(data_root, "exp013", "word_cache.pt")
|
| 170 |
+
if os.path.exists(path):
|
| 171 |
+
return torch.load(path, map_location="cpu", weights_only=True)
|
| 172 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 173 |
+
tr_lines, _ = _wikitext_lines(data_root)
|
| 174 |
+
from collections import Counter
|
| 175 |
+
cnt = Counter(w for l in tr_lines for w in re.findall(r"[a-z]{3,12}", l.lower()))
|
| 176 |
+
words = [w for w, _ in cnt.most_common(n_words)]
|
| 177 |
+
blob = {"words": words}
|
| 178 |
+
ct = CLIPTokenizerFast.from_pretrained("openai/clip-vit-large-patch14")
|
| 179 |
+
cm = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14").to(device).eval()
|
| 180 |
+
fin, pen = [], []
|
| 181 |
+
for i in range(0, len(words), batch):
|
| 182 |
+
enc = ct(words[i:i + batch], padding="max_length", truncation=True,
|
| 183 |
+
max_length=77, return_tensors="pt").to(device)
|
| 184 |
+
out = cm(**enc, output_hidden_states=True)
|
| 185 |
+
eos = enc.input_ids.argmax(-1)
|
| 186 |
+
idx = torch.arange(eos.numel(), device=device)
|
| 187 |
+
fin.append(out.last_hidden_state[idx, eos].half().cpu())
|
| 188 |
+
pen.append(out.hidden_states[-2][idx, eos].half().cpu())
|
| 189 |
+
blob["clip_final"], blob["clip_penult"] = torch.cat(fin), torch.cat(pen)
|
| 190 |
+
del cm
|
| 191 |
+
bt = BertTokenizerFast.from_pretrained("bert-base-uncased")
|
| 192 |
+
bm = BertModel.from_pretrained("bert-base-uncased").to(device).eval()
|
| 193 |
+
cls, mean = [], []
|
| 194 |
+
for i in range(0, len(words), batch):
|
| 195 |
+
enc = bt(words[i:i + batch], padding=True, truncation=True,
|
| 196 |
+
max_length=16, return_tensors="pt").to(device)
|
| 197 |
+
out = bm(**enc).last_hidden_state
|
| 198 |
+
m = enc.attention_mask.unsqueeze(-1)
|
| 199 |
+
cls.append(out[:, 0].half().cpu())
|
| 200 |
+
mean.append(((out * m).sum(1) / m.sum(1)).half().cpu())
|
| 201 |
+
blob["bert_cls"], blob["bert_mean"] = torch.cat(cls), torch.cat(mean)
|
| 202 |
+
torch.save(blob, path)
|
| 203 |
+
print(f"word cache: {len(words)} words -> {path}", flush=True)
|
| 204 |
+
return blob
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ============================================================ track A =============
|
| 208 |
+
def track_a(arms=("linear", "mlp", "aleph", "sign"), layers=("final", "penult"),
|
| 209 |
+
steps=1500, batch=64, seed=0, device="cuda",
|
| 210 |
+
data_root=DATA_ROOT, eval_every=500, save=True):
|
| 211 |
+
"""Next-CLIP-token prediction from frozen CLIP-L hidden states."""
|
| 212 |
+
if not torch.cuda.is_available():
|
| 213 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 214 |
+
blob = cache_clip(data_root, device=device)
|
| 215 |
+
V = blob["vocab"]
|
| 216 |
+
results = {}
|
| 217 |
+
ck_dir = os.path.join(data_root, "exp013", "ckpts")
|
| 218 |
+
os.makedirs(ck_dir, exist_ok=True)
|
| 219 |
+
for layer in layers:
|
| 220 |
+
Htr = blob["train"][layer].float()
|
| 221 |
+
ids_tr = blob["train"]["ids"]
|
| 222 |
+
Hva = blob["val"][layer].float()
|
| 223 |
+
ids_va = blob["val"]["ids"]
|
| 224 |
+
for arm in arms:
|
| 225 |
+
torch.manual_seed(seed)
|
| 226 |
+
g = torch.Generator().manual_seed(seed)
|
| 227 |
+
head = HeadArm(arm, Htr.shape[-1]).to(device)
|
| 228 |
+
out_proj = nn.Linear(HeadArm.F_OUT, V).to(device)
|
| 229 |
+
params = list(head.parameters()) + list(out_proj.parameters())
|
| 230 |
+
opt = torch.optim.Adam(params, lr=3e-4, weight_decay=0.0)
|
| 231 |
+
n_par = sum(p.numel() for p in params)
|
| 232 |
+
for step in range(1, steps + 1):
|
| 233 |
+
ix = torch.randint(0, Htr.shape[0], (batch,), generator=g)
|
| 234 |
+
h = Htr[ix].to(device)
|
| 235 |
+
y = ids_tr[ix].to(device)
|
| 236 |
+
logits = out_proj(head(h[:, :-1]))
|
| 237 |
+
loss = F.cross_entropy(logits.reshape(-1, V), y[:, 1:].reshape(-1))
|
| 238 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 239 |
+
if step % eval_every == 0 or step == steps:
|
| 240 |
+
with torch.no_grad():
|
| 241 |
+
ls = []
|
| 242 |
+
for j in range(0, min(1024, Hva.shape[0]), batch):
|
| 243 |
+
h = Hva[j:j + batch].to(device)
|
| 244 |
+
y = ids_va[j:j + batch].to(device)
|
| 245 |
+
lg = out_proj(head(h[:, :-1]))
|
| 246 |
+
ls.append(F.cross_entropy(
|
| 247 |
+
lg.reshape(-1, V), y[:, 1:].reshape(-1)).item())
|
| 248 |
+
ce = sum(ls) / len(ls)
|
| 249 |
+
vit = head.vitals(Hva[:2, :8].to(device))
|
| 250 |
+
print(f"[A {layer} {arm} s{seed}] step {step} val_ce={ce:.4f} "
|
| 251 |
+
f"params={n_par:,} vitals={vit}", flush=True)
|
| 252 |
+
results[f"{layer}/{arm}/s{seed}"] = {"val_ce": ce, "params": n_par,
|
| 253 |
+
"vitals": vit}
|
| 254 |
+
if save and arm in ("aleph", "sign"):
|
| 255 |
+
torch.save({"track": "A", "layer": layer, "arm": arm, "seed": seed,
|
| 256 |
+
"val_ce": ce, "state_dict": {k: v.cpu() for k, v in
|
| 257 |
+
head.state_dict().items()}},
|
| 258 |
+
os.path.join(ck_dir, f"A_{layer}_{arm}_s{seed}.pt"))
|
| 259 |
+
print(results, flush=True)
|
| 260 |
+
return results
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
# ============================================================ track B =============
|
| 264 |
+
class CharDecoder(nn.Module):
|
| 265 |
+
"""Tiny GRU char decoder conditioned ONLY on the arm's 256-d read."""
|
| 266 |
+
CHARS = "abcdefghijklmnopqrstuvwxyz"
|
| 267 |
+
def __init__(self, cond_dim=256, hidden=256):
|
| 268 |
+
super().__init__()
|
| 269 |
+
self.V = len(self.CHARS) + 2 # +BOS +EOS
|
| 270 |
+
self.emb = nn.Embedding(self.V, 64)
|
| 271 |
+
self.init = nn.Linear(cond_dim, hidden)
|
| 272 |
+
self.gru = nn.GRU(64, hidden, batch_first=True)
|
| 273 |
+
self.out = nn.Linear(hidden, self.V)
|
| 274 |
+
|
| 275 |
+
def encode_word(self, w):
|
| 276 |
+
return [1] + [2 + self.CHARS.index(c) for c in w] + [0] # BOS..EOS(0)
|
| 277 |
+
|
| 278 |
+
def forward(self, cond, tgt): # tgt: (B, L) int, teacher-forced
|
| 279 |
+
h0 = torch.tanh(self.init(cond)).unsqueeze(0)
|
| 280 |
+
x = self.emb(tgt[:, :-1])
|
| 281 |
+
y, _ = self.gru(x, h0)
|
| 282 |
+
return self.out(y) # predict tgt[:,1:]
|
| 283 |
+
|
| 284 |
+
@torch.no_grad()
|
| 285 |
+
def greedy(self, cond, max_len=14):
|
| 286 |
+
B = cond.shape[0]
|
| 287 |
+
h = torch.tanh(self.init(cond)).unsqueeze(0)
|
| 288 |
+
t = torch.ones(B, 1, dtype=torch.long, device=cond.device)
|
| 289 |
+
done = torch.zeros(B, dtype=torch.bool, device=cond.device)
|
| 290 |
+
outs = []
|
| 291 |
+
for _ in range(max_len):
|
| 292 |
+
y, h = self.gru(self.emb(t), h)
|
| 293 |
+
t = self.out(y).argmax(-1)
|
| 294 |
+
outs.append(t)
|
| 295 |
+
done |= (t.squeeze(1) == 0)
|
| 296 |
+
if done.all():
|
| 297 |
+
break
|
| 298 |
+
return torch.cat(outs, 1)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def track_b1(arms=("linear", "mlp"), substrates=("clip_final", "clip_penult",
|
| 302 |
+
"bert_cls", "bert_mean"), steps=3000, batch=128, seed=0,
|
| 303 |
+
device="cuda", data_root=DATA_ROOT, save=True):
|
| 304 |
+
"""Spelling-AR from pooled embeddings. Run baselines first (the GATE:
|
| 305 |
+
qualify the task only if linear/mlp exact-match < 0.50), then aleph/sign."""
|
| 306 |
+
if not torch.cuda.is_available():
|
| 307 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 308 |
+
blob = cache_word_embeddings(data_root, device=device)
|
| 309 |
+
words = blob["words"]
|
| 310 |
+
dec_tpl = CharDecoder()
|
| 311 |
+
enc = [dec_tpl.encode_word(w) for w in words]
|
| 312 |
+
L = max(len(e) for e in enc)
|
| 313 |
+
tgt = torch.zeros(len(enc), L, dtype=torch.long)
|
| 314 |
+
for i, e in enumerate(enc):
|
| 315 |
+
tgt[i, :len(e)] = torch.tensor(e)
|
| 316 |
+
g0 = torch.Generator().manual_seed(1234) # fixed split across arms
|
| 317 |
+
perm = torch.randperm(len(words), generator=g0)
|
| 318 |
+
tr_ix, va_ix = perm[:9000], perm[9000:]
|
| 319 |
+
ck_dir = os.path.join(data_root, "exp013", "ckpts")
|
| 320 |
+
os.makedirs(ck_dir, exist_ok=True)
|
| 321 |
+
results = {}
|
| 322 |
+
for sub in substrates:
|
| 323 |
+
E = blob[sub].float()
|
| 324 |
+
for arm in arms:
|
| 325 |
+
torch.manual_seed(seed)
|
| 326 |
+
g = torch.Generator().manual_seed(seed)
|
| 327 |
+
head = HeadArm(arm, E.shape[-1]).to(device)
|
| 328 |
+
dec = CharDecoder().to(device)
|
| 329 |
+
params = list(head.parameters()) + list(dec.parameters())
|
| 330 |
+
opt = torch.optim.Adam(params, lr=1e-3, weight_decay=0.0)
|
| 331 |
+
for step in range(1, steps + 1):
|
| 332 |
+
ix = tr_ix[torch.randint(0, tr_ix.numel(), (batch,), generator=g)]
|
| 333 |
+
cond = head(E[ix].to(device))
|
| 334 |
+
t = tgt[ix].to(device)
|
| 335 |
+
lg = dec(cond, t)
|
| 336 |
+
mask = (t[:, 1:] != 0) | (torch.cumsum(t[:, 1:] == 0, 1) == 1)
|
| 337 |
+
loss = F.cross_entropy(lg[mask], t[:, 1:][mask])
|
| 338 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 339 |
+
with torch.no_grad():
|
| 340 |
+
cond = head(E[va_ix].to(device))
|
| 341 |
+
pred = dec.greedy(cond)
|
| 342 |
+
t = tgt[va_ix, 1:].to(device)
|
| 343 |
+
n = min(pred.shape[1], t.shape[1])
|
| 344 |
+
pad_ok = torch.ones_like(t[:, :n], dtype=torch.bool)
|
| 345 |
+
seen_eos = torch.cumsum(t[:, :n] == 0, 1) > 0
|
| 346 |
+
match = ((pred[:, :n] == t[:, :n]) | seen_eos).all(-1)
|
| 347 |
+
exact = match.float().mean().item()
|
| 348 |
+
vit = head.vitals(E[va_ix[:16]].to(device))
|
| 349 |
+
print(f"[B1 {sub} {arm} s{seed}] exact={exact:.4f} vitals={vit}", flush=True)
|
| 350 |
+
results[f"{sub}/{arm}/s{seed}"] = {"exact": exact, "vitals": vit}
|
| 351 |
+
if save and arm in ("aleph", "sign"):
|
| 352 |
+
torch.save({"track": "B1", "sub": sub, "arm": arm, "seed": seed,
|
| 353 |
+
"exact": exact, "state_dict": {k: v.cpu() for k, v in
|
| 354 |
+
head.state_dict().items()}},
|
| 355 |
+
os.path.join(ck_dir, f"B1_{sub}_{arm}_s{seed}.pt"))
|
| 356 |
+
print(results, flush=True)
|
| 357 |
+
return results
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
# ============================================================ track C =============
|
| 361 |
+
class MLPAdapter(nn.Module):
|
| 362 |
+
"""Param-matched plain adapter (the ablation twin of MslRelay).
|
| 363 |
+
hidden=64 matches MslRelay at d=768 (2*768*64=98.3K vs 98.6K incl codebook).
|
| 364 |
+
Output layer ZERO-INIT (standard adapter stabilization — the first version
|
| 365 |
+
diverged at lr 1e-3 with random init; the aleph relay needed no such aid,
|
| 366 |
+
which is itself a datapoint, but the control gets its best shot)."""
|
| 367 |
+
def __init__(self, d, hidden=64):
|
| 368 |
+
super().__init__()
|
| 369 |
+
out = nn.Linear(hidden, d)
|
| 370 |
+
nn.init.zeros_(out.weight)
|
| 371 |
+
nn.init.zeros_(out.bias)
|
| 372 |
+
self.net = nn.Sequential(nn.Linear(d, hidden), SquaredReLU(), out)
|
| 373 |
+
self.gate = nn.Parameter(torch.tensor(-3.0))
|
| 374 |
+
|
| 375 |
+
def forward(self, x):
|
| 376 |
+
return x + self.gate.sigmoid() * self.net(x)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
class _BlockWithAdapter(nn.Module):
|
| 380 |
+
def __init__(self, block, adapter):
|
| 381 |
+
super().__init__()
|
| 382 |
+
self.block, self.adapter = block, adapter
|
| 383 |
+
|
| 384 |
+
def forward(self, *a, **k):
|
| 385 |
+
out = self.block(*a, **k)
|
| 386 |
+
if isinstance(out, tuple):
|
| 387 |
+
return (self.adapter(out[0]),) + out[1:]
|
| 388 |
+
return self.adapter(out)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def track_c(arms=("frozen", "aleph", "mlp"), steps=1500, batch=8, block=256,
|
| 392 |
+
seed=0, device="cuda", data_root=DATA_ROOT,
|
| 393 |
+
eval_every=500, save=True):
|
| 394 |
+
"""GPT-2 124M frozen; adapters after every block; train adapters only."""
|
| 395 |
+
from transformers import GPT2LMHeadModel, GPT2TokenizerFast
|
| 396 |
+
if not torch.cuda.is_available():
|
| 397 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 398 |
+
tok = GPT2TokenizerFast.from_pretrained("gpt2")
|
| 399 |
+
tr_lines, va_lines = _wikitext_lines(data_root)
|
| 400 |
+
def to_stream(lines):
|
| 401 |
+
ids = tok("\n\n".join(lines), return_tensors="pt").input_ids[0]
|
| 402 |
+
return ids
|
| 403 |
+
stream_tr = to_stream(tr_lines[:8000])
|
| 404 |
+
stream_va = to_stream(va_lines[:1000])
|
| 405 |
+
ck_dir = os.path.join(data_root, "exp013", "ckpts")
|
| 406 |
+
os.makedirs(ck_dir, exist_ok=True)
|
| 407 |
+
results = {}
|
| 408 |
+
for arm in arms:
|
| 409 |
+
torch.manual_seed(seed)
|
| 410 |
+
g = torch.Generator().manual_seed(seed)
|
| 411 |
+
model = GPT2LMHeadModel.from_pretrained("gpt2").to(device)
|
| 412 |
+
for p in model.parameters():
|
| 413 |
+
p.requires_grad_(False)
|
| 414 |
+
adapters = []
|
| 415 |
+
if arm != "frozen":
|
| 416 |
+
d = model.config.n_embd
|
| 417 |
+
for i, blk in enumerate(model.transformer.h):
|
| 418 |
+
ad = (MslRelay(d) if arm == "aleph" else MLPAdapter(d)).to(device)
|
| 419 |
+
model.transformer.h[i] = _BlockWithAdapter(blk, ad)
|
| 420 |
+
adapters.append(ad)
|
| 421 |
+
params = [p for ad in adapters for p in ad.parameters()]
|
| 422 |
+
n_par = sum(p.numel() for p in params)
|
| 423 |
+
opt = torch.optim.Adam(params, lr=1e-3, weight_decay=0.0)
|
| 424 |
+
else:
|
| 425 |
+
params, n_par = [], 0
|
| 426 |
+
def eval_ppl():
|
| 427 |
+
model.eval()
|
| 428 |
+
with torch.no_grad():
|
| 429 |
+
ls = []
|
| 430 |
+
for j in range(0, stream_va.numel() - block - 1, block * 4):
|
| 431 |
+
x = stream_va[j:j + block].unsqueeze(0).to(device)
|
| 432 |
+
out = model(x, labels=x)
|
| 433 |
+
ls.append(out.loss.item())
|
| 434 |
+
model.train()
|
| 435 |
+
return math.exp(sum(ls) / len(ls))
|
| 436 |
+
if arm == "frozen":
|
| 437 |
+
ppl = eval_ppl()
|
| 438 |
+
print(f"[C frozen] ppl={ppl:.3f}", flush=True)
|
| 439 |
+
results["frozen"] = {"ppl": ppl}
|
| 440 |
+
continue
|
| 441 |
+
for step in range(1, steps + 1):
|
| 442 |
+
ix = torch.randint(0, stream_tr.numel() - block - 1, (batch,), generator=g)
|
| 443 |
+
x = torch.stack([stream_tr[i:i + block] for i in ix]).to(device)
|
| 444 |
+
loss = model(x, labels=x).loss
|
| 445 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 446 |
+
if step % eval_every == 0 or step == steps:
|
| 447 |
+
ppl = eval_ppl()
|
| 448 |
+
gates = [round(ad.gate.sigmoid().item(), 4) for ad in adapters]
|
| 449 |
+
vit = {}
|
| 450 |
+
if arm == "aleph":
|
| 451 |
+
drifts = [round(anchor_drift(ad.addr.codebook, ad.addr.home)
|
| 452 |
+
["mean"], 3) for ad in adapters]
|
| 453 |
+
vit = {"drift_by_depth": drifts}
|
| 454 |
+
print(f"[C {arm} s{seed}] step {step} ppl={ppl:.3f} "
|
| 455 |
+
f"params={n_par:,} gates={gates} {vit}", flush=True)
|
| 456 |
+
results[f"{arm}/s{seed}"] = {"ppl": ppl, "params": n_par, "gates": gates,
|
| 457 |
+
**vit}
|
| 458 |
+
if save and arm == "aleph":
|
| 459 |
+
torch.save({"track": "C", "arm": arm, "seed": seed, "ppl": ppl,
|
| 460 |
+
"state_dict": {f"relay{i}.{k}": v.cpu()
|
| 461 |
+
for i, ad in enumerate(adapters)
|
| 462 |
+
for k, v in ad.state_dict().items()}},
|
| 463 |
+
os.path.join(ck_dir, f"C_{arm}_s{seed}.pt"))
|
| 464 |
+
print(results, flush=True)
|
| 465 |
+
return results
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
# ================================================================ smoke ===========
|
| 469 |
+
def smoke():
|
| 470 |
+
"""Shapes/parse only — no substrates, no training."""
|
| 471 |
+
for arm in ("linear", "mlp", "aleph", "sign"):
|
| 472 |
+
h = HeadArm(arm, 768)
|
| 473 |
+
y = h(torch.randn(2, 10, 768))
|
| 474 |
+
assert y.shape == (2, 10, 256)
|
| 475 |
+
y.sum().backward()
|
| 476 |
+
print(arm, "OK", f"{h.param_count():,}", h.vitals(torch.randn(2, 4, 768)))
|
| 477 |
+
dec = CharDecoder()
|
| 478 |
+
t = torch.tensor([dec.encode_word("hello") + [0] * 3,
|
| 479 |
+
dec.encode_word("worlds") + [0] * 2])
|
| 480 |
+
lg = dec(torch.randn(2, 256), t)
|
| 481 |
+
assert lg.shape[:2] == (2, t.shape[1] - 1)
|
| 482 |
+
print("decoder OK; greedy:", dec.greedy(torch.randn(2, 256)).shape)
|
| 483 |
+
ad = MLPAdapter(768)
|
| 484 |
+
assert ad(torch.randn(2, 4, 768)).shape == (2, 4, 768)
|
| 485 |
+
print("adapter OK — exp013 smoke passed (caches+tracks need GPU+transformers)")
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def _in_notebook():
|
| 489 |
+
try:
|
| 490 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 491 |
+
return True
|
| 492 |
+
except NameError:
|
| 493 |
+
return False
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
if __name__ == "__main__":
|
| 497 |
+
if _in_notebook():
|
| 498 |
+
smoke()
|
| 499 |
+
print("Notebook: cache_clip()/cache_word_embeddings() then "
|
| 500 |
+
"track_b1() gate -> track_a() -> track_c().")
|
| 501 |
+
else:
|
| 502 |
+
import argparse
|
| 503 |
+
ap = argparse.ArgumentParser()
|
| 504 |
+
ap.add_argument("--track", default="smoke")
|
| 505 |
+
a, _ = ap.parse_known_args()
|
| 506 |
+
{"smoke": smoke, "a": track_a, "b1": track_b1, "c": track_c}[a.track]()
|
exp007_math/geolip_vitals.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""geolip_vitals.py — shared diagnostic harness for the GeoLIP aleph experiments.
|
| 2 |
+
ALL functions are READOUTS: no gradients, no losses. CV is a readout, never a
|
| 3 |
+
force. Addressing is judged by drift->0.29154 and CV->0.20, never by recon cosine
|
| 4 |
+
(judgment criteria per the aleph-void article: https://huggingface.co/blog/AbstractPhil/geometric-vocabulary-patchwork-aleph-void).
|
| 5 |
+
|
| 6 |
+
Vitals provided:
|
| 7 |
+
anchor_drift — geodesic drift of anchors from init; binding fraction @0.29154
|
| 8 |
+
pentachoron_cv — CM 4-volume CV over random 5-row subsets (geovocab2 import)
|
| 9 |
+
axis_aliveness — oriented-address usage: axes alive, hppl, collapse flag
|
| 10 |
+
gate_stats — gate means vs the 0.012-0.03 band
|
| 11 |
+
path_diversity — unique-path counting, FIXED high-bits hash (low-16 bug is the
|
| 12 |
+
retracted artifact — never use the low bits)
|
| 13 |
+
grad_norm_spread — gradient democracy monitor (orders-of-magnitude spread)
|
| 14 |
+
CVScreen — CV@1000-batch early band screen (<0.30 LOW / .35-.50 MID / >.80 HIGH)
|
| 15 |
+
|
| 16 |
+
Smoke on a torch-capable env: python geolip_vitals.py
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
import math
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
BINDING = 0.29154 # radians; the binding/separation constant
|
| 23 |
+
CV_BAND = (0.13, 0.30) # CM CV band (discovery_catalog #4)
|
| 24 |
+
GATE_BAND = (0.012, 0.03) # live invariant candidate (acd_campaign)
|
| 25 |
+
KNUTH32 = 2654435761
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ----------------------------------------------------------------------------- drift
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def anchor_drift(current: torch.Tensor, init: torch.Tensor, tol: float = 0.05) -> dict:
|
| 31 |
+
"""Geodesic drift (radians) of each row of `current` from its row in `init`,
|
| 32 |
+
both row-normalized. Returns mean/std/per-row drift and the fraction of rows
|
| 33 |
+
within +/-tol of BINDING (the GLFM '46%' readout)."""
|
| 34 |
+
a = torch.nn.functional.normalize(current.float(), dim=-1)
|
| 35 |
+
b = torch.nn.functional.normalize(init.float(), dim=-1)
|
| 36 |
+
cos = (a * b).sum(-1).clamp(-1.0, 1.0)
|
| 37 |
+
drift = torch.arccos(cos)
|
| 38 |
+
frac = ((drift - BINDING).abs() <= tol).float().mean()
|
| 39 |
+
return {"mean": drift.mean().item(), "std": drift.std().item(),
|
| 40 |
+
"per_row": drift, "binding_fraction": frac.item()}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# -------------------------------------------------------------------------------- cv
|
| 44 |
+
@torch.no_grad()
|
| 45 |
+
def _pentachoron_volumes(pts: torch.Tensor) -> torch.Tensor:
|
| 46 |
+
"""Batched Cayley-Menger 4-simplex volumes. pts: (B, 5, D) -> (B,) volumes.
|
| 47 |
+
One float64 det over all samples (vol^2 = -det(CM)/9216 for n=4). Built-in
|
| 48 |
+
for speed (the per-sample reference path is ~260x slower in a vitals loop);
|
| 49 |
+
geovocab2 remains the formula's reference implementation, parity-checked
|
| 50 |
+
via cv_reference_check()."""
|
| 51 |
+
B = pts.shape[0]
|
| 52 |
+
d2 = torch.cdist(pts.double(), pts.double()).pow(2) # (B,5,5)
|
| 53 |
+
cm = torch.ones(B, 6, 6, dtype=torch.float64, device=pts.device)
|
| 54 |
+
cm[:, 0, 0] = 0.0
|
| 55 |
+
cm[:, 1:, 1:] = d2
|
| 56 |
+
det = torch.linalg.det(cm)
|
| 57 |
+
return (-det / 9216.0).clamp_min(0.0).sqrt().float()
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@torch.no_grad()
|
| 61 |
+
def pentachoron_cv(rows: torch.Tensor, n_samples: int = 200,
|
| 62 |
+
generator: torch.Generator | None = None) -> float:
|
| 63 |
+
"""CV (std/mean) of Cayley-Menger 4-simplex volumes over n_samples random
|
| 64 |
+
5-row subsets. Rows are row-normalized before measurement. Uses the built-in
|
| 65 |
+
batched CM (float64 det); validate against geovocab2 with
|
| 66 |
+
cv_reference_check() after any change to the volume math."""
|
| 67 |
+
x = torch.nn.functional.normalize(rows.float(), dim=-1)
|
| 68 |
+
n = x.shape[0]
|
| 69 |
+
if n < 5:
|
| 70 |
+
raise ValueError(f"pentachoron_cv needs >=5 rows, got {n}")
|
| 71 |
+
g = generator or torch.Generator(device="cpu").manual_seed(0)
|
| 72 |
+
idx = torch.stack([torch.randperm(n, generator=g)[:5]
|
| 73 |
+
for _ in range(n_samples)]) # (B,5)
|
| 74 |
+
v = _pentachoron_volumes(x[idx].cpu())
|
| 75 |
+
return (v.std() / v.mean().clamp_min(1e-12)).item()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@torch.no_grad()
|
| 79 |
+
def cv_reference_check(n_trials: int = 50, tol: float = 1e-5) -> float:
|
| 80 |
+
"""Parity check of the built-in batched CM against geovocab2's reference
|
| 81 |
+
implementation (the formula's source of truth). Returns max |rel diff|;
|
| 82 |
+
raises if geovocab2 is absent or parity fails. Run after touching
|
| 83 |
+
_pentachoron_volumes."""
|
| 84 |
+
try:
|
| 85 |
+
from geovocab2.shapes.formula.symbolic.cayley_menger import (
|
| 86 |
+
CayleyMengerFromSimplex)
|
| 87 |
+
except Exception as e: # pragma: no cover
|
| 88 |
+
raise ImportError(
|
| 89 |
+
"cv_reference_check requires geovocab2 (install via the geolip-svae "
|
| 90 |
+
"umbrella: pip install git+https://github.com/AbstractEyes/"
|
| 91 |
+
"geolip-svae).") from e
|
| 92 |
+
ref = CayleyMengerFromSimplex()
|
| 93 |
+
g = torch.Generator().manual_seed(0)
|
| 94 |
+
pts = torch.nn.functional.normalize(
|
| 95 |
+
torch.randn(n_trials, 5, 4, generator=g), dim=-1)
|
| 96 |
+
mine = _pentachoron_volumes(pts)
|
| 97 |
+
# compare at float64: the reference computes in the INPUT dtype, and fp32
|
| 98 |
+
# dets lose up to ~4% on near-degenerate pentachora (measured 2026-07-11)
|
| 99 |
+
theirs = torch.stack([ref.forward(p.double())["volume"].float() for p in pts])
|
| 100 |
+
rel = ((mine - theirs).abs() / theirs.abs().clamp_min(1e-12)).max().item()
|
| 101 |
+
if rel > tol:
|
| 102 |
+
raise AssertionError(f"CM parity vs geovocab2 failed: max rel {rel}")
|
| 103 |
+
return rel
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# ------------------------------------------------------------------------- aliveness
|
| 107 |
+
@torch.no_grad()
|
| 108 |
+
def axis_aliveness(oriented_weights: torch.Tensor, alive_thresh: float = 1e-3) -> dict:
|
| 109 |
+
"""`oriented_weights`: (..., 2K) nonnegative oriented-softmax address rows
|
| 110 |
+
(sum to 1 on the last dim). Returns axes-alive count, mean-usage perplexity
|
| 111 |
+
(hppl analogue; healthy hosted reference 125-126/128), and a collapse flag.
|
| 112 |
+
Reference behavior: near-uniform aliveness at div_weight=0 (discovery #22)."""
|
| 113 |
+
w = oriented_weights.reshape(-1, oriented_weights.shape[-1]).float()
|
| 114 |
+
usage = w.mean(0)
|
| 115 |
+
usage = usage / usage.sum().clamp_min(1e-12)
|
| 116 |
+
# an axis is alive if its mean usage exceeds alive_thresh x the uniform share
|
| 117 |
+
alive = int((usage > alive_thresh * (1.0 / usage.numel())).sum())
|
| 118 |
+
ent = -(usage.clamp_min(1e-12) * usage.clamp_min(1e-12).log()).sum()
|
| 119 |
+
ppl = float(ent.exp())
|
| 120 |
+
return {"axes_total": usage.numel(), "axes_alive": alive, "usage_ppl": ppl,
|
| 121 |
+
"collapsed": ppl < 0.05 * usage.numel()}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ------------------------------------------------------------------------------ gates
|
| 125 |
+
@torch.no_grad()
|
| 126 |
+
def gate_stats(gates: torch.Tensor) -> dict:
|
| 127 |
+
"""Gate values (post-sigmoid/clamp). Reports mean and whether it sits in the
|
| 128 |
+
0.012-0.03 band (read-only — the band is a candidate invariant, never a target)."""
|
| 129 |
+
g = gates.float().flatten()
|
| 130 |
+
m = g.mean().item()
|
| 131 |
+
return {"mean": m, "std": g.std().item(),
|
| 132 |
+
"in_band": GATE_BAND[0] <= m <= GATE_BAND[1]}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ------------------------------------------------------------------------------ paths
|
| 136 |
+
@torch.no_grad()
|
| 137 |
+
def path_diversity(ids: torch.Tensor) -> dict:
|
| 138 |
+
"""Unique-path counting with the FIXED multiplicative hash:
|
| 139 |
+
((ids * 2654435761) % 2^32) >> 16 — Knuth needs the HIGH bits; the low-16
|
| 140 |
+
variant produced a retracted ~1,500 path ceiling in a prior campaign.
|
| 141 |
+
`ids`: integer tensor, one composed path id per row (any shape)."""
|
| 142 |
+
x = ids.reshape(-1).to(torch.int64)
|
| 143 |
+
hashed = ((x * KNUTH32) % (1 << 32)) >> 16
|
| 144 |
+
return {"n": int(x.numel()),
|
| 145 |
+
"unique_raw": int(torch.unique(x).numel()),
|
| 146 |
+
"unique_hashed": int(torch.unique(hashed).numel())}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@torch.no_grad()
|
| 150 |
+
def compose_path_ids(stage_indices: list[torch.Tensor], radix: int) -> torch.Tensor:
|
| 151 |
+
"""Compose per-stage discrete indices (each (...,) int in [0, radix)) into a
|
| 152 |
+
single path id, positional base-`radix` — construction, not hashing."""
|
| 153 |
+
out = torch.zeros_like(stage_indices[0], dtype=torch.int64)
|
| 154 |
+
for s in stage_indices:
|
| 155 |
+
out = out * radix + s.to(torch.int64)
|
| 156 |
+
return out
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# --------------------------------------------------------------------- grad democracy
|
| 160 |
+
@torch.no_grad()
|
| 161 |
+
def grad_norm_spread(groups: dict[str, list[torch.nn.Parameter]]) -> dict:
|
| 162 |
+
"""Gradient-democracy monitor. `groups`: name -> params of one parallel member
|
| 163 |
+
(tower/expert). Reports per-group grad norms and the orders-of-magnitude spread.
|
| 164 |
+
Reference: unequalized heterogeneous towers spread ~20 orders (fibonacci dead at
|
| 165 |
+
2.25e-21 under helix); equalized ~0.0 (geofractal gradient-democracy result)."""
|
| 166 |
+
norms = {}
|
| 167 |
+
for name, params in groups.items():
|
| 168 |
+
gs = [p.grad for p in params if p.grad is not None]
|
| 169 |
+
norms[name] = float(torch.sqrt(sum((g.float() ** 2).sum() for g in gs)).item()) \
|
| 170 |
+
if gs else 0.0
|
| 171 |
+
vals = [v for v in norms.values() if v > 0]
|
| 172 |
+
spread = (math.log10(max(vals)) - math.log10(min(vals))) if len(vals) >= 2 else 0.0
|
| 173 |
+
return {"norms": norms, "spread_orders": spread, "dead": [k for k, v in norms.items() if v == 0.0]}
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ----------------------------------------------------------------------------- screen
|
| 177 |
+
class CVScreen:
|
| 178 |
+
"""CV@N early band screen (tri-band ft1): record pentachoron CV at `step_mark`
|
| 179 |
+
batches; classify <0.30 LOW / 0.35-0.50 MID / >0.80 HIGH. Turns ~2h/config
|
| 180 |
+
into ~7min. Readout only."""
|
| 181 |
+
def __init__(self, step_mark: int = 1000):
|
| 182 |
+
self.step_mark = step_mark
|
| 183 |
+
self.recorded: float | None = None
|
| 184 |
+
|
| 185 |
+
def maybe_record(self, step: int, rows: torch.Tensor) -> float | None:
|
| 186 |
+
if self.recorded is None and step >= self.step_mark:
|
| 187 |
+
self.recorded = pentachoron_cv(rows)
|
| 188 |
+
return self.recorded
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def band(self) -> str | None:
|
| 192 |
+
c = self.recorded
|
| 193 |
+
if c is None:
|
| 194 |
+
return None
|
| 195 |
+
if c < 0.30:
|
| 196 |
+
return "LOW"
|
| 197 |
+
if 0.35 <= c <= 0.50:
|
| 198 |
+
return "MID"
|
| 199 |
+
if c > 0.80:
|
| 200 |
+
return "HIGH"
|
| 201 |
+
return "BETWEEN"
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# ------------------------------------------------------------------------------ smoke
|
| 205 |
+
if __name__ == "__main__": # shapes/parse smoke ONLY — no training, ever.
|
| 206 |
+
g = torch.Generator().manual_seed(0)
|
| 207 |
+
K, D = 64, 4
|
| 208 |
+
init = torch.nn.functional.normalize(torch.randn(K, D, generator=g), dim=-1)
|
| 209 |
+
cur = torch.nn.functional.normalize(init + 0.29 * torch.randn(K, D, generator=g), dim=-1)
|
| 210 |
+
print("drift:", {k: v for k, v in anchor_drift(cur, init).items() if k != "per_row"})
|
| 211 |
+
w = torch.softmax(torch.randn(32, 2 * K, generator=g), dim=-1)
|
| 212 |
+
print("aliveness:", axis_aliveness(w))
|
| 213 |
+
print("gates:", gate_stats(torch.full((8,), 0.024)))
|
| 214 |
+
ids = compose_path_ids([torch.randint(0, 16, (4096,), generator=g) for _ in range(4)], 16)
|
| 215 |
+
print("paths:", path_diversity(ids))
|
| 216 |
+
lin = torch.nn.Linear(8, 8)
|
| 217 |
+
lin(torch.randn(4, 8)).sum().backward()
|
| 218 |
+
print("democracy:", grad_norm_spread({"a": list(lin.parameters())}))
|
| 219 |
+
print("OK — vitals smoke passed (pentachoron_cv needs geovocab2; run on GPU env)")
|
exp007_math/qwen_exp001_relay.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""qwen_exp001_relay.py — geolip-aleph-qwen EXPERIMENT 1: the relay retrofit
|
| 2 |
+
at 0.5B. The certified exp013 Track-C recipe (frozen trunk + near-zero-gated
|
| 3 |
+
multi-slot M_hat relays after every block, vs the param-matched zero-init MLP
|
| 4 |
+
adapter ablation) at its next scale rung: Qwen2.5-0.5B (24 blocks, d=896,
|
| 5 |
+
~494M frozen params; relay stack ~2.8M trainable, <0.6%). First campaign of
|
| 6 |
+
the generation-stake line — judged on ppl, the GATE MECHANISM (do the gates
|
| 7 |
+
grow? exp013: aleph gates grew ~3x while MLP gates shrank), relay codebook
|
| 8 |
+
vitals, and GENERATED SAMPLES (the deliverable is a model that generates).
|
| 9 |
+
|
| 10 |
+
Arms (x2 seeds): frozen (eval-only baseline) | relay (MslRelay(896) after
|
| 11 |
+
every block) | mlp (param-matched MLPAdapter(896, hidden=64), zero-init out —
|
| 12 |
+
the exp013 ablation, 114,688 vs 114,944 params per adapter, 0.2%).
|
| 13 |
+
Corpus: wikitext-103-raw-v1 (HF parquet), ~12M-token cache; block 512,
|
| 14 |
+
batch 4, 3000 steps adapters-only, pure Adam lr 1e-3 wd 0 (the exp013
|
| 15 |
+
relay-training regime).
|
| 16 |
+
Riders: trunk FROZEN throughout; pure Adam wd=0; GPU-only verdicts; >=2
|
| 17 |
+
seeds; drift-check before any freeze claim; Colab-safe. Paste order:
|
| 18 |
+
geolip_vitals -> ar_differentiation_bed -> exp013_augmentation_bed -> this.
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
import json
|
| 22 |
+
import math
|
| 23 |
+
import os
|
| 24 |
+
import torch
|
| 25 |
+
import torch.nn as nn
|
| 26 |
+
import torch.nn.functional as F
|
| 27 |
+
|
| 28 |
+
if "MslRelay" not in globals():
|
| 29 |
+
try:
|
| 30 |
+
from ar_differentiation_bed import MslRelay
|
| 31 |
+
from exp013_augmentation_bed import MLPAdapter
|
| 32 |
+
from geolip_vitals import anchor_drift, axis_aliveness
|
| 33 |
+
except ImportError:
|
| 34 |
+
_here = globals().get("__file__")
|
| 35 |
+
if _here is None:
|
| 36 |
+
raise ImportError("paste geolip_vitals + ar_differentiation_bed + "
|
| 37 |
+
"exp013_augmentation_bed first")
|
| 38 |
+
import sys, pathlib
|
| 39 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 40 |
+
from ar_differentiation_bed import MslRelay
|
| 41 |
+
from exp013_augmentation_bed import MLPAdapter
|
| 42 |
+
from geolip_vitals import anchor_drift, axis_aliveness
|
| 43 |
+
|
| 44 |
+
DATA_ROOT = os.environ.get("GEOLIP_DATA", "./data")
|
| 45 |
+
EXP_DIR = os.path.join(DATA_ROOT, "qwen_exp001")
|
| 46 |
+
BASE_MODEL = "Qwen/Qwen2.5-0.5B"
|
| 47 |
+
BLOCK, BATCH, STEPS, LR = 512, 4, 3000, 1e-3
|
| 48 |
+
MAX_TOKENS = 12_000_000
|
| 49 |
+
PROMPTS = ("The history of mathematics begins",
|
| 50 |
+
"In a small village by the sea,",
|
| 51 |
+
"The most important principle of engineering is")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class _QwenBlockWithAdapter(nn.Module):
|
| 55 |
+
"""Wrap a Qwen2 decoder layer: adapter applied to the hidden-states output,
|
| 56 |
+
all other outputs and kwargs passed through untouched."""
|
| 57 |
+
|
| 58 |
+
def __init__(self, block, adapter):
|
| 59 |
+
super().__init__()
|
| 60 |
+
self.block, self.adapter = block, adapter
|
| 61 |
+
|
| 62 |
+
def forward(self, *args, **kwargs):
|
| 63 |
+
out = self.block(*args, **kwargs)
|
| 64 |
+
if isinstance(out, tuple):
|
| 65 |
+
return (self.adapter(out[0]),) + out[1:]
|
| 66 |
+
return self.adapter(out)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _token_cache(device="cpu"):
|
| 70 |
+
"""~12M-token wikitext-103 train stream + val stream, cached to disk."""
|
| 71 |
+
os.makedirs(EXP_DIR, exist_ok=True)
|
| 72 |
+
path = os.path.join(EXP_DIR, "tok_cache.pt")
|
| 73 |
+
if os.path.exists(path):
|
| 74 |
+
blob = torch.load(path, map_location="cpu", weights_only=True)
|
| 75 |
+
return blob["train"], blob["val"]
|
| 76 |
+
from huggingface_hub import hf_hub_download
|
| 77 |
+
import pyarrow.parquet as pq
|
| 78 |
+
from transformers import AutoTokenizer
|
| 79 |
+
tok = AutoTokenizer.from_pretrained(BASE_MODEL)
|
| 80 |
+
|
| 81 |
+
def stream(fname, cap):
|
| 82 |
+
p = hf_hub_download("Salesforce/wikitext", fname, repo_type="dataset")
|
| 83 |
+
lines = [t for t in pq.read_table(p).column("text").to_pylist()
|
| 84 |
+
if t and len(t.strip()) > 40]
|
| 85 |
+
ids = []
|
| 86 |
+
total = 0
|
| 87 |
+
chunk = []
|
| 88 |
+
csz = 0
|
| 89 |
+
for ln in lines:
|
| 90 |
+
chunk.append(ln)
|
| 91 |
+
csz += len(ln)
|
| 92 |
+
if csz > 500_000:
|
| 93 |
+
e = tok("".join(chunk), return_tensors="pt").input_ids[0]
|
| 94 |
+
ids.append(e)
|
| 95 |
+
total += e.numel()
|
| 96 |
+
chunk, csz = [], 0
|
| 97 |
+
if total >= cap:
|
| 98 |
+
break
|
| 99 |
+
if chunk and total < cap:
|
| 100 |
+
e = tok("".join(chunk), return_tensors="pt").input_ids[0]
|
| 101 |
+
ids.append(e)
|
| 102 |
+
return torch.cat(ids)[:cap]
|
| 103 |
+
|
| 104 |
+
tr = stream("wikitext-103-raw-v1/train-00000-of-00002.parquet", MAX_TOKENS)
|
| 105 |
+
va = stream("wikitext-103-raw-v1/validation-00000-of-00001.parquet",
|
| 106 |
+
600_000)
|
| 107 |
+
torch.save({"train": tr, "val": va}, path)
|
| 108 |
+
print(f"token cache: train {tr.numel():,} val {va.numel():,}", flush=True)
|
| 109 |
+
return tr, va
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _batch(stream, batch, block, device, g):
|
| 113 |
+
ix = torch.randint(0, stream.numel() - block - 1, (batch,), generator=g)
|
| 114 |
+
x = torch.stack([stream[i:i + block] for i in ix]).to(device)
|
| 115 |
+
y = torch.stack([stream[i + 1:i + block + 1] for i in ix]).to(device)
|
| 116 |
+
return x.long(), y.long()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def load_qwen(arm: str, seed: int = 0, device="cuda"):
|
| 120 |
+
from transformers import AutoModelForCausalLM
|
| 121 |
+
torch.manual_seed(seed)
|
| 122 |
+
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL,
|
| 123 |
+
torch_dtype=torch.float32)
|
| 124 |
+
model.config.use_cache = False
|
| 125 |
+
for p in model.parameters():
|
| 126 |
+
p.requires_grad_(False)
|
| 127 |
+
adapters = None
|
| 128 |
+
if arm != "frozen":
|
| 129 |
+
d = model.config.hidden_size
|
| 130 |
+
mk = (lambda: MslRelay(d)) if arm == "relay" else (lambda: MLPAdapter(d))
|
| 131 |
+
adapters = nn.ModuleList([mk() for _ in model.model.layers])
|
| 132 |
+
model.model.layers = nn.ModuleList(
|
| 133 |
+
[_QwenBlockWithAdapter(b, a)
|
| 134 |
+
for b, a in zip(model.model.layers, adapters)])
|
| 135 |
+
return model.to(device), adapters
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@torch.no_grad()
|
| 139 |
+
def eval_ppl(model, va, device="cuda", n=40, g=None):
|
| 140 |
+
g = g or torch.Generator().manual_seed(0)
|
| 141 |
+
model.eval()
|
| 142 |
+
ls = []
|
| 143 |
+
for _ in range(n):
|
| 144 |
+
x, y = _batch(va, BATCH, BLOCK, device, g)
|
| 145 |
+
logits = model(input_ids=x).logits
|
| 146 |
+
ls.append(F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 147 |
+
y.reshape(-1)).item())
|
| 148 |
+
return math.exp(sum(ls) / len(ls))
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@torch.no_grad()
|
| 152 |
+
def sample(model, device="cuda", max_new=80):
|
| 153 |
+
from transformers import AutoTokenizer
|
| 154 |
+
tok = AutoTokenizer.from_pretrained(BASE_MODEL)
|
| 155 |
+
model.eval()
|
| 156 |
+
model.config.use_cache = True
|
| 157 |
+
outs = {}
|
| 158 |
+
for p in PROMPTS:
|
| 159 |
+
ids = tok(p, return_tensors="pt").input_ids.to(device)
|
| 160 |
+
out = model.generate(ids, max_new_tokens=max_new, do_sample=False,
|
| 161 |
+
pad_token_id=tok.eos_token_id)
|
| 162 |
+
outs[p] = tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True)
|
| 163 |
+
model.config.use_cache = False
|
| 164 |
+
return outs
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def adapter_vitals(adapters):
|
| 168 |
+
if adapters is None:
|
| 169 |
+
return None
|
| 170 |
+
gates = [round(torch.sigmoid(a.gate).item(), 4) for a in adapters
|
| 171 |
+
if hasattr(a, "gate")]
|
| 172 |
+
drifts = []
|
| 173 |
+
for a in adapters:
|
| 174 |
+
ad = getattr(a, "addr", None)
|
| 175 |
+
if ad is not None:
|
| 176 |
+
drifts.append(round(anchor_drift(ad.codebook, ad.home)["mean"], 3))
|
| 177 |
+
return {"gates": gates[:6] + ["..."] if len(gates) > 6 else gates,
|
| 178 |
+
"gate_mean": round(sum(gates) / max(len(gates), 1), 4),
|
| 179 |
+
"drift": drifts[:6] + ["..."] if len(drifts) > 6 else drifts}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def run_arm(arm: str, seed: int = 0, steps=STEPS, device="cuda"):
|
| 183 |
+
if not torch.cuda.is_available():
|
| 184 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 185 |
+
os.makedirs(EXP_DIR, exist_ok=True)
|
| 186 |
+
tr, va = _token_cache()
|
| 187 |
+
model, adapters = load_qwen(arm, seed=seed, device=device)
|
| 188 |
+
ledger = open(os.path.join(EXP_DIR, "ledger.jsonl"), "a", encoding="utf-8")
|
| 189 |
+
if arm == "frozen":
|
| 190 |
+
ppl = eval_ppl(model, va, device=device)
|
| 191 |
+
rec = {"exp": "q1", "arm": arm, "seed": seed, "ppl": round(ppl, 3),
|
| 192 |
+
"trainable": 0, "samples": sample(model, device=device)}
|
| 193 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.flush()
|
| 194 |
+
print(f"[q1 frozen s{seed}] FINAL ppl={ppl:.3f}", flush=True)
|
| 195 |
+
ledger.close()
|
| 196 |
+
return ppl
|
| 197 |
+
g = torch.Generator().manual_seed(seed)
|
| 198 |
+
params = [p for p in adapters.parameters()]
|
| 199 |
+
n_train = sum(p.numel() for p in params)
|
| 200 |
+
opt = torch.optim.Adam(params, lr=LR, weight_decay=0.0)
|
| 201 |
+
model.train()
|
| 202 |
+
for step in range(1, steps + 1):
|
| 203 |
+
x, y = _batch(tr, BATCH, BLOCK, device, g)
|
| 204 |
+
logits = model(input_ids=x).logits
|
| 205 |
+
loss = F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 206 |
+
y.reshape(-1))
|
| 207 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 208 |
+
if step % 500 == 0:
|
| 209 |
+
v = adapter_vitals(adapters)
|
| 210 |
+
print(f"[q1 {arm} s{seed} step {step}] loss={loss.item():.3f} "
|
| 211 |
+
f"gate_mean={v['gate_mean']}", flush=True)
|
| 212 |
+
ppl = eval_ppl(model, va, device=device)
|
| 213 |
+
rec = {"exp": "q1", "arm": arm, "seed": seed, "ppl": round(ppl, 3),
|
| 214 |
+
"steps": steps, "trainable": n_train,
|
| 215 |
+
"vitals": adapter_vitals(adapters),
|
| 216 |
+
"samples": sample(model, device=device)}
|
| 217 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.flush()
|
| 218 |
+
print(f"[q1 {arm} s{seed}] FINAL ppl={ppl:.3f} "
|
| 219 |
+
f"gate_mean={rec['vitals']['gate_mean']} trainable={n_train:,}",
|
| 220 |
+
flush=True)
|
| 221 |
+
torch.save({"arm": arm, "seed": seed,
|
| 222 |
+
"adapters": {k: v.cpu() for k, v in
|
| 223 |
+
adapters.state_dict().items()}},
|
| 224 |
+
os.path.join(EXP_DIR, f"q1_{arm}_s{seed}.pt"))
|
| 225 |
+
ledger.close()
|
| 226 |
+
del model, adapters
|
| 227 |
+
torch.cuda.empty_cache()
|
| 228 |
+
return ppl
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def run_exp001(seeds=(0, 1), device="cuda"):
|
| 232 |
+
run_arm("frozen", seed=0, device=device) # baseline once (no training)
|
| 233 |
+
for seed in seeds:
|
| 234 |
+
for arm in ("relay", "mlp"):
|
| 235 |
+
run_arm(arm, seed=seed, device=device)
|
| 236 |
+
print("=== qwen exp001 COMPLETE ===", flush=True)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def smoke():
|
| 240 |
+
"""Shapes/parse only — no substrate download, no training."""
|
| 241 |
+
r = MslRelay(896)
|
| 242 |
+
m = MLPAdapter(896)
|
| 243 |
+
x = torch.randn(2, 8, 896)
|
| 244 |
+
assert r(x).shape == x.shape and m(x).shape == x.shape
|
| 245 |
+
rp = sum(p.numel() for p in r.parameters())
|
| 246 |
+
mp = sum(p.numel() for p in m.parameters())
|
| 247 |
+
assert abs(rp - mp) / rp < 0.01, (rp, mp) # param-matched adapters
|
| 248 |
+
blk = nn.Linear(896, 896) # tuple-passthrough check
|
| 249 |
+
class TupBlock(nn.Module):
|
| 250 |
+
def forward(self, h, **kw):
|
| 251 |
+
return (blk(h), "aux")
|
| 252 |
+
w = _QwenBlockWithAdapter(TupBlock(), r)
|
| 253 |
+
out = w(x, position_ids=None)
|
| 254 |
+
assert isinstance(out, tuple) and out[0].shape == x.shape and out[1] == "aux"
|
| 255 |
+
(out[0].sum()).backward()
|
| 256 |
+
assert r.addr.codebook.grad is not None
|
| 257 |
+
print(f"qwen exp001 smoke passed (relay {rp:,} ~ mlp {mp:,} params/adapter;"
|
| 258 |
+
" full run needs GPU + transformers + the 0.5B download)")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _in_notebook():
|
| 262 |
+
try:
|
| 263 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 264 |
+
return True
|
| 265 |
+
except NameError:
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
if __name__ == "__main__":
|
| 270 |
+
smoke() if not _in_notebook() else (smoke(),
|
| 271 |
+
print("Notebook: run_exp001() on GPU."))
|
exp007_math/qwen_exp002_refine.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""qwen_exp002_refine.py — geolip-aleph-qwen EXPERIMENT 2: refine the relay.
|
| 2 |
+
exp001 verdict shape: the relay retrofit works at 0.5B (frozen 17.80 ->
|
| 3 |
+
~14.09, gates grow 0.047 -> 0.081 = trunk opt-in) but the param-matched MLP
|
| 4 |
+
adapter is at parity-or-ahead on ppl — the certified GPT-2 ordering did not
|
| 5 |
+
transfer as-is. exp002 races PROTOTYPICAL ENHANCEMENTS of the relay, each
|
| 6 |
+
grounded in a certified law, at an equalized WIDER budget (~230K/adapter),
|
| 7 |
+
against the honest wide MLP control.
|
| 8 |
+
|
| 9 |
+
Arms (frozen Qwen2.5-0.5B trunk, adapters after every block unless noted):
|
| 10 |
+
relay32 — MslRelay with n_slots=32 (slot dose-response: exp012 L-AR3,
|
| 11 |
+
task monotone in slot width)
|
| 12 |
+
relay_3tau — multi-slot read at 3 temperatures (0.05/0.1/0.3), concatenated
|
| 13 |
+
(the certified rule-of-3 stroboscope, exp012 v2: -1.08 bpb)
|
| 14 |
+
relay_pw — slots -> M_hat -> PATCHWORK consumer Linear(64,178) ->
|
| 15 |
+
SquaredReLU -> LN -> Linear(178,896) (the constellation
|
| 16 |
+
consumption spec replaces the bare linear out)
|
| 17 |
+
relay_deep — exp001's TOTAL budget concentrated on the last 12 blocks
|
| 18 |
+
(n_slots=32 there, nothing on blocks 0-11): the depth-gradient
|
| 19 |
+
law (cultivation concentrates near the prediction gradient)
|
| 20 |
+
as an architecture decision; total params == exp001 relay
|
| 21 |
+
mlp_wide — MLPAdapter hidden=128 (the wide-budget capacity control)
|
| 22 |
+
All near-zero gated (init -3.0). Training identical to exp001: block 512,
|
| 23 |
+
batch 4, 3000 steps, pure Adam lr 1e-3 wd 0, adapters only. Judged: val ppl
|
| 24 |
+
vs exp001's relay (14.093/14.091) and mlp (13.927/...) + gate mechanism +
|
| 25 |
+
vitals + generated samples. Seed 0 sweep first; seed 1 for the top arms.
|
| 26 |
+
Riders: trunk frozen; >=2 seeds before any claim; GPU-only; Colab-safe.
|
| 27 |
+
Paste order: geolip_vitals -> ar_differentiation_bed ->
|
| 28 |
+
exp013_augmentation_bed -> qwen_exp001_relay -> this file.
|
| 29 |
+
"""
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
import json
|
| 32 |
+
import math
|
| 33 |
+
import os
|
| 34 |
+
import torch
|
| 35 |
+
import torch.nn as nn
|
| 36 |
+
import torch.nn.functional as F
|
| 37 |
+
|
| 38 |
+
if "run_arm" not in globals():
|
| 39 |
+
try:
|
| 40 |
+
from ar_differentiation_bed import AlephAddress, MslRelay
|
| 41 |
+
from exp013_augmentation_bed import MLPAdapter
|
| 42 |
+
from geolip_vitals import anchor_drift
|
| 43 |
+
from qwen_exp001_relay import (_QwenBlockWithAdapter, _token_cache,
|
| 44 |
+
_batch, eval_ppl, sample, EXP_DIR as
|
| 45 |
+
Q1_DIR, BASE_MODEL, BLOCK, BATCH, LR)
|
| 46 |
+
except ImportError:
|
| 47 |
+
_here = globals().get("__file__")
|
| 48 |
+
if _here is None:
|
| 49 |
+
raise ImportError("paste the qwen stack first")
|
| 50 |
+
import sys, pathlib
|
| 51 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 52 |
+
from ar_differentiation_bed import AlephAddress, MslRelay
|
| 53 |
+
from exp013_augmentation_bed import MLPAdapter
|
| 54 |
+
from geolip_vitals import anchor_drift
|
| 55 |
+
from qwen_exp001_relay import (_QwenBlockWithAdapter, _token_cache,
|
| 56 |
+
_batch, eval_ppl, sample, EXP_DIR as
|
| 57 |
+
Q1_DIR, BASE_MODEL, BLOCK, BATCH, LR)
|
| 58 |
+
|
| 59 |
+
EXP2_DIR = os.path.join(os.path.dirname(Q1_DIR), "qwen_exp002")
|
| 60 |
+
STEPS = 3000
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class SquaredReLU(nn.Module):
|
| 64 |
+
def forward(self, x):
|
| 65 |
+
return F.relu(x) ** 2
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class Relay3Tau(nn.Module):
|
| 69 |
+
"""Multi-slot M_hat at 3 temperatures, concatenated (rule-of-3 strobe)."""
|
| 70 |
+
TAUS = (0.05, 0.1, 0.3)
|
| 71 |
+
|
| 72 |
+
def __init__(self, d: int, n_slots: int = 16, K: int = 64):
|
| 73 |
+
super().__init__()
|
| 74 |
+
self.n_slots = n_slots
|
| 75 |
+
self.proj = nn.Linear(d, n_slots * 4, bias=False)
|
| 76 |
+
self.out = nn.Linear(n_slots * 4 * len(self.TAUS), d, bias=False)
|
| 77 |
+
nn.init.orthogonal_(self.proj.weight)
|
| 78 |
+
nn.init.orthogonal_(self.out.weight)
|
| 79 |
+
self.addr = AlephAddress(K, 4)
|
| 80 |
+
self.gate = nn.Parameter(torch.tensor(-3.0))
|
| 81 |
+
|
| 82 |
+
def forward(self, x):
|
| 83 |
+
B, n, _ = x.shape
|
| 84 |
+
slots = self.proj(x).view(B, n, self.n_slots, 4)
|
| 85 |
+
feats = []
|
| 86 |
+
for t in self.TAUS:
|
| 87 |
+
u = self.addr._u(slots) * (self.addr.tau / t)
|
| 88 |
+
m = u.abs().amax(dim=-1, keepdim=True)
|
| 89 |
+
ep, en = torch.exp(u - m), torch.exp(-u - m)
|
| 90 |
+
A = F.normalize(self.addr.codebook, dim=-1)
|
| 91 |
+
feats.append((((ep - en) @ A)
|
| 92 |
+
/ (ep + en).sum(dim=-1, keepdim=True)).reshape(B, n, -1))
|
| 93 |
+
return x + torch.sigmoid(self.gate) * self.out(torch.cat(feats, -1))
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class RelayPatchwork(nn.Module):
|
| 97 |
+
"""Multi-slot M_hat -> constellation-spec consumer (SquaredReLU patchwork)."""
|
| 98 |
+
|
| 99 |
+
def __init__(self, d: int, n_slots: int = 16, K: int = 64, hidden: int = 178):
|
| 100 |
+
super().__init__()
|
| 101 |
+
self.n_slots = n_slots
|
| 102 |
+
self.proj = nn.Linear(d, n_slots * 4, bias=False)
|
| 103 |
+
nn.init.orthogonal_(self.proj.weight)
|
| 104 |
+
self.addr = AlephAddress(K, 4)
|
| 105 |
+
self.consume = nn.Sequential(
|
| 106 |
+
nn.Linear(n_slots * 4, hidden), SquaredReLU(),
|
| 107 |
+
nn.LayerNorm(hidden), nn.Linear(hidden, d))
|
| 108 |
+
nn.init.zeros_(self.consume[-1].weight) # zero-init out (theme D)
|
| 109 |
+
self.gate = nn.Parameter(torch.tensor(-3.0))
|
| 110 |
+
|
| 111 |
+
def forward(self, x):
|
| 112 |
+
B, n, _ = x.shape
|
| 113 |
+
slots = self.proj(x).view(B, n, self.n_slots, 4)
|
| 114 |
+
feats = self.addr.m_hat(slots).reshape(B, n, -1)
|
| 115 |
+
return x + torch.sigmoid(self.gate) * self.consume(feats)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def build_adapters(arm: str, model):
|
| 119 |
+
d = model.config.hidden_size
|
| 120 |
+
L = len(model.model.layers)
|
| 121 |
+
if arm == "relay32":
|
| 122 |
+
mk = [lambda: MslRelay(d, n_slots=32) for _ in range(L)]
|
| 123 |
+
elif arm == "relay_3tau":
|
| 124 |
+
mk = [lambda: Relay3Tau(d) for _ in range(L)]
|
| 125 |
+
elif arm == "relay_pw":
|
| 126 |
+
mk = [lambda: RelayPatchwork(d) for _ in range(L)]
|
| 127 |
+
elif arm == "relay_deep":
|
| 128 |
+
mk = [None] * (L // 2) + [lambda: MslRelay(d, n_slots=32)
|
| 129 |
+
for _ in range(L - L // 2)]
|
| 130 |
+
elif arm == "mlp_wide":
|
| 131 |
+
mk = [lambda: MLPAdapter(d, hidden=128) for _ in range(L)]
|
| 132 |
+
else:
|
| 133 |
+
raise ValueError(arm)
|
| 134 |
+
adapters = nn.ModuleList([m() if m else nn.Identity() for m in mk])
|
| 135 |
+
model.model.layers = nn.ModuleList(
|
| 136 |
+
[_QwenBlockWithAdapter(b, a) if not isinstance(a, nn.Identity) else b
|
| 137 |
+
for b, a in zip(model.model.layers, adapters)])
|
| 138 |
+
return adapters
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def adapter_vitals(adapters):
|
| 142 |
+
gates, drifts = [], []
|
| 143 |
+
for a in adapters:
|
| 144 |
+
if hasattr(a, "gate"):
|
| 145 |
+
gates.append(round(torch.sigmoid(a.gate).item(), 4))
|
| 146 |
+
ad = getattr(a, "addr", None)
|
| 147 |
+
if ad is not None:
|
| 148 |
+
drifts.append(round(anchor_drift(ad.codebook, ad.home)["mean"], 3))
|
| 149 |
+
return {"gate_mean": round(sum(gates) / max(len(gates), 1), 4),
|
| 150 |
+
"gates_head_tail": gates[:3] + gates[-3:],
|
| 151 |
+
"drift_head_tail": (drifts[:3] + drifts[-3:]) if drifts else None}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def run_arm2(arm: str, seed: int = 0, steps=STEPS, device="cuda"):
|
| 155 |
+
if not torch.cuda.is_available():
|
| 156 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 157 |
+
os.makedirs(EXP2_DIR, exist_ok=True)
|
| 158 |
+
from transformers import AutoModelForCausalLM
|
| 159 |
+
tr, va = _token_cache()
|
| 160 |
+
torch.manual_seed(seed)
|
| 161 |
+
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL,
|
| 162 |
+
torch_dtype=torch.float32)
|
| 163 |
+
model.config.use_cache = False
|
| 164 |
+
for p in model.parameters():
|
| 165 |
+
p.requires_grad_(False)
|
| 166 |
+
adapters = build_adapters(arm, model)
|
| 167 |
+
model = model.to(device)
|
| 168 |
+
n_train = sum(p.numel() for p in adapters.parameters())
|
| 169 |
+
g = torch.Generator().manual_seed(seed)
|
| 170 |
+
opt = torch.optim.Adam(adapters.parameters(), lr=LR, weight_decay=0.0)
|
| 171 |
+
model.train()
|
| 172 |
+
for step in range(1, steps + 1):
|
| 173 |
+
x, y = _batch(tr, BATCH, BLOCK, device, g)
|
| 174 |
+
logits = model(input_ids=x).logits
|
| 175 |
+
loss = F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 176 |
+
y.reshape(-1))
|
| 177 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 178 |
+
if step % 1000 == 0:
|
| 179 |
+
print(f"[q2 {arm} s{seed} step {step}] loss={loss.item():.3f}",
|
| 180 |
+
flush=True)
|
| 181 |
+
ppl = eval_ppl(model, va, device=device)
|
| 182 |
+
v = adapter_vitals(adapters)
|
| 183 |
+
rec = {"exp": "q2", "arm": arm, "seed": seed, "ppl": round(ppl, 3),
|
| 184 |
+
"steps": steps, "trainable": n_train, "vitals": v,
|
| 185 |
+
"samples": sample(model, device=device)}
|
| 186 |
+
ledger = open(os.path.join(EXP2_DIR, "ledger.jsonl"), "a", encoding="utf-8")
|
| 187 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.close()
|
| 188 |
+
print(f"[q2 {arm} s{seed}] FINAL ppl={ppl:.3f} gate_mean={v['gate_mean']} "
|
| 189 |
+
f"trainable={n_train:,}", flush=True)
|
| 190 |
+
torch.save({"arm": arm, "seed": seed,
|
| 191 |
+
"adapters": {k: v2.cpu() for k, v2 in
|
| 192 |
+
adapters.state_dict().items()}},
|
| 193 |
+
os.path.join(EXP2_DIR, f"q2_{arm}_s{seed}.pt"))
|
| 194 |
+
del model, adapters
|
| 195 |
+
torch.cuda.empty_cache()
|
| 196 |
+
return ppl
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
ARMS = ("relay32", "relay_3tau", "relay_pw", "relay_deep", "mlp_wide")
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def run_wave_a(arms=ARMS, seed=0, device="cuda"):
|
| 203 |
+
for arm in arms:
|
| 204 |
+
run_arm2(arm, seed=seed, device=device)
|
| 205 |
+
print("=== qwen exp002 WAVE A COMPLETE ===", flush=True)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def smoke():
|
| 209 |
+
x = torch.randn(2, 8, 896)
|
| 210 |
+
for cls, kw in ((Relay3Tau, {}), (RelayPatchwork, {}),):
|
| 211 |
+
a = cls(896, **kw)
|
| 212 |
+
y = a(x)
|
| 213 |
+
assert y.shape == x.shape
|
| 214 |
+
y.sum().backward()
|
| 215 |
+
assert a.addr.codebook.grad is not None
|
| 216 |
+
a.zero_grad()
|
| 217 |
+
p32 = sum(p.numel() for p in MslRelay(896, n_slots=32).parameters())
|
| 218 |
+
p3t = sum(p.numel() for p in Relay3Tau(896).parameters())
|
| 219 |
+
ppw = sum(p.numel() for p in RelayPatchwork(896).parameters())
|
| 220 |
+
pmw = sum(p.numel() for p in MLPAdapter(896, hidden=128).parameters())
|
| 221 |
+
lo, hi = min(p32, p3t, ppw, pmw), max(p32, p3t, ppw, pmw)
|
| 222 |
+
assert (hi - lo) / hi < 0.12, (p32, p3t, ppw, pmw) # budget-equalized ~10%
|
| 223 |
+
print(f"qwen exp002 smoke passed (relay32 {p32:,} | 3tau {p3t:,} | "
|
| 224 |
+
f"pw {ppw:,} | mlp_wide {pmw:,} per adapter)")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _in_notebook():
|
| 228 |
+
try:
|
| 229 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 230 |
+
return True
|
| 231 |
+
except NameError:
|
| 232 |
+
return False
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
smoke() if not _in_notebook() else (smoke(),
|
| 237 |
+
print("Notebook: run_wave_a() on GPU."))
|
exp007_math/qwen_exp003_instruct.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""qwen_exp003_instruct.py — geolip-aleph-qwen EXPERIMENT 3: instruct-task
|
| 2 |
+
adapters + register differentiation on the discrete surface.
|
| 3 |
+
Hypothesis under test (Phil): continuation-trained adapters "double up"
|
| 4 |
+
values — aligning REGISTERS to overlapping internal states; instruct-task
|
| 5 |
+
training should differentiate them cleanly. The json-coco-format dataset
|
| 6 |
+
(AbstractPhil/json-coco-format) is the ideal instrument: the SAME 22k COCO
|
| 7 |
+
captions pass through THREE structurally distinct output registers —
|
| 8 |
+
task_1 hallucination_reduction (grounded literal extraction), task_2
|
| 9 |
+
useful_generalization (bracketed generics), task_3 generic_symbolism
|
| 10 |
+
(positional placeholders) — in native messages+tools shape.
|
| 11 |
+
|
| 12 |
+
Substrate: Qwen2.5-0.5B-INSTRUCT, frozen; adapters = the exp002-certified
|
| 13 |
+
relay_pw (aleph addressing + constellation patchwork consumer) after every
|
| 14 |
+
block. Arms (x2 seeds): frozen (zero-shot) | relay_pw_task1 (the
|
| 15 |
+
hallucination-reduction cell — Phil's prior task-1 adapter, reproduced on the
|
| 16 |
+
relay stack) | relay_pw_all (multi-task on all three) | mlp_all
|
| 17 |
+
(MLPAdapter(128), budget-matched control; its 1/2-seed divergence rate at
|
| 18 |
+
this width is itself a standing datum).
|
| 19 |
+
|
| 20 |
+
JUDGES:
|
| 21 |
+
1. held-out per-task PPL (split BY CAPTION hash — no caption leaks across
|
| 22 |
+
tasks or splits);
|
| 23 |
+
2. TASK VALIDITY on generated outputs (validators re-implemented from the
|
| 24 |
+
dataset README: JSON parse + schema keys; task_1 per-leaf grounding
|
| 25 |
+
substring check; task_2 ^\\[[a-z_]+\\]$; task_3 typed monotonic
|
| 26 |
+
placeholders) — hallucination reduction as a measured rate;
|
| 27 |
+
3. THE REGISTER PROBE: sign-codes from the relay stack for the same held-out
|
| 28 |
+
captions under all four registers (3 task system-prompts + plain
|
| 29 |
+
continuation); separation = mean inter-register Hamming minus mean
|
| 30 |
+
intra-register Hamming, per layer — measured on BOTH the instruct-trained
|
| 31 |
+
stack and the exp002 WIKITEXT-trained stack (the overlap-vs-differentiate
|
| 32 |
+
comparison the hypothesis asks for).
|
| 33 |
+
Training: block 512, batch 4, 3000 steps, adapters only, pure Adam lr 1e-3
|
| 34 |
+
wd 0 (the certified regime). Full-conversation LM loss (no prompt masking) —
|
| 35 |
+
consistent with the line's stream training; noted as an instrument property.
|
| 36 |
+
Riders: trunk frozen; >=2 seeds; GPU-only; Colab-safe. Paste order:
|
| 37 |
+
geolip_vitals -> ar_differentiation_bed -> exp013_augmentation_bed ->
|
| 38 |
+
qwen_exp001_relay -> qwen_exp002_refine -> this file.
|
| 39 |
+
"""
|
| 40 |
+
from __future__ import annotations
|
| 41 |
+
import json
|
| 42 |
+
import math
|
| 43 |
+
import os
|
| 44 |
+
import re
|
| 45 |
+
import torch
|
| 46 |
+
import torch.nn as nn
|
| 47 |
+
import torch.nn.functional as F
|
| 48 |
+
|
| 49 |
+
if "RelayPatchwork" not in globals():
|
| 50 |
+
try:
|
| 51 |
+
from exp013_augmentation_bed import MLPAdapter
|
| 52 |
+
from qwen_exp001_relay import _QwenBlockWithAdapter, _batch
|
| 53 |
+
from qwen_exp002_refine import RelayPatchwork
|
| 54 |
+
except ImportError:
|
| 55 |
+
_here = globals().get("__file__")
|
| 56 |
+
if _here is None:
|
| 57 |
+
raise ImportError("paste the qwen stack first")
|
| 58 |
+
import sys, pathlib
|
| 59 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 60 |
+
from exp013_augmentation_bed import MLPAdapter
|
| 61 |
+
from qwen_exp001_relay import _QwenBlockWithAdapter, _batch
|
| 62 |
+
from qwen_exp002_refine import RelayPatchwork
|
| 63 |
+
|
| 64 |
+
DATA_ROOT = os.environ.get("GEOLIP_DATA", "./data")
|
| 65 |
+
EXP3_DIR = os.path.join(DATA_ROOT, "qwen_exp003")
|
| 66 |
+
DATASET = "AbstractPhil/json-coco-format"
|
| 67 |
+
INSTRUCT_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 68 |
+
BLOCK, BATCH, STEPS, LR = 512, 4, 3000, 1e-3
|
| 69 |
+
TASKS = ("task_1", "task_2", "task_3")
|
| 70 |
+
HOLDOUT_MOD = 50 # caption-hash % 50 == 0 -> held out (~2%)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# ================================ data ============================================
|
| 74 |
+
def _rows(task):
|
| 75 |
+
from huggingface_hub import hf_hub_download
|
| 76 |
+
p = hf_hub_download(DATASET, f"data/{task}.jsonl", repo_type="dataset")
|
| 77 |
+
return [json.loads(l) for l in open(p, encoding="utf-8")]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _render(tok, row, gen_prompt=False):
|
| 81 |
+
"""Chat-template render; stringify tool arguments if the template needs it."""
|
| 82 |
+
msgs = row["messages"][:2] if gen_prompt else row["messages"]
|
| 83 |
+
try:
|
| 84 |
+
return tok.apply_chat_template(msgs, tools=row["tools"],
|
| 85 |
+
tokenize=False,
|
| 86 |
+
add_generation_prompt=gen_prompt)
|
| 87 |
+
except Exception:
|
| 88 |
+
msgs = json.loads(json.dumps(msgs))
|
| 89 |
+
for m in msgs:
|
| 90 |
+
for tc in m.get("tool_calls", []) or []:
|
| 91 |
+
a = tc["function"]["arguments"]
|
| 92 |
+
if isinstance(a, dict):
|
| 93 |
+
tc["function"]["arguments"] = json.dumps(a)
|
| 94 |
+
return tok.apply_chat_template(msgs, tools=row["tools"],
|
| 95 |
+
tokenize=False,
|
| 96 |
+
add_generation_prompt=gen_prompt)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _is_holdout(prompt: str) -> bool:
|
| 100 |
+
import hashlib
|
| 101 |
+
return int(hashlib.md5(prompt.encode()).hexdigest(), 16) % HOLDOUT_MOD == 0
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def build_caches():
|
| 105 |
+
"""Token streams per task split (train/holdout) + eval row lists, cached."""
|
| 106 |
+
os.makedirs(EXP3_DIR, exist_ok=True)
|
| 107 |
+
path = os.path.join(EXP3_DIR, "instruct_cache.pt")
|
| 108 |
+
if os.path.exists(path):
|
| 109 |
+
return torch.load(path, map_location="cpu", weights_only=False)
|
| 110 |
+
from transformers import AutoTokenizer
|
| 111 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 112 |
+
blob = {"train": {}, "hold": {}, "eval_rows": {}}
|
| 113 |
+
for task in TASKS:
|
| 114 |
+
rows = _rows(task)
|
| 115 |
+
tr_txt, ho_txt, ev = [], [], []
|
| 116 |
+
for r in rows:
|
| 117 |
+
if _is_holdout(r["prompt"]):
|
| 118 |
+
ho_txt.append(_render(tok, r))
|
| 119 |
+
if len(ev) < 64:
|
| 120 |
+
ev.append({"prompt": r["prompt"],
|
| 121 |
+
"gen_prompt": _render(tok, r, gen_prompt=True)})
|
| 122 |
+
else:
|
| 123 |
+
tr_txt.append(_render(tok, r))
|
| 124 |
+
def stream(txts):
|
| 125 |
+
ids = tok("\n".join(txts), return_tensors="pt").input_ids[0]
|
| 126 |
+
return ids
|
| 127 |
+
blob["train"][task] = stream(tr_txt)
|
| 128 |
+
blob["hold"][task] = stream(ho_txt)
|
| 129 |
+
blob["eval_rows"][task] = ev
|
| 130 |
+
print(f"[cache] {task}: train {blob['train'][task].numel():,} tok, "
|
| 131 |
+
f"hold {blob['hold'][task].numel():,} tok, eval {len(ev)} rows",
|
| 132 |
+
flush=True)
|
| 133 |
+
torch.save(blob, path)
|
| 134 |
+
return blob
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# ============================ validators (per the dataset README) =================
|
| 138 |
+
# core content keys; style/mood are constant-null in the data and models may
|
| 139 |
+
# legitimately omit them (validators only read subjects/actions/setting)
|
| 140 |
+
SCHEMA_KEYS = {"subjects", "actions", "setting"}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def parse_tool_json(text: str):
|
| 144 |
+
# GREEDY inner match — the arguments JSON is nested; a non-greedy match
|
| 145 |
+
# truncates at the first '}' (the v1 parser bug, caught by a perfect
|
| 146 |
+
# generation scoring 0)
|
| 147 |
+
m = re.search(r"<tool_call>\s*(\{.*\})\s*</tool_call>", text, re.S)
|
| 148 |
+
raw = m.group(1) if m else None
|
| 149 |
+
if raw is None:
|
| 150 |
+
m = re.search(r"\{.*\}", text, re.S)
|
| 151 |
+
raw = m.group(0) if m else None
|
| 152 |
+
if raw is None:
|
| 153 |
+
return None
|
| 154 |
+
try:
|
| 155 |
+
obj = json.loads(raw)
|
| 156 |
+
except Exception:
|
| 157 |
+
return None
|
| 158 |
+
if isinstance(obj, dict) and "arguments" in obj:
|
| 159 |
+
obj = obj["arguments"]
|
| 160 |
+
if isinstance(obj, str):
|
| 161 |
+
try:
|
| 162 |
+
obj = json.loads(obj)
|
| 163 |
+
except Exception:
|
| 164 |
+
return None
|
| 165 |
+
return obj if isinstance(obj, dict) else None
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _leaves(args, with_setting=True):
|
| 169 |
+
for s in args.get("subjects") or []:
|
| 170 |
+
if isinstance(s, dict):
|
| 171 |
+
if s.get("name"):
|
| 172 |
+
yield str(s["name"])
|
| 173 |
+
for a in s.get("attributes") or []:
|
| 174 |
+
yield str(a)
|
| 175 |
+
for a in args.get("actions") or []:
|
| 176 |
+
yield str(a)
|
| 177 |
+
# setting is an ENUM categorization in task_1 (not verbatim-grounded);
|
| 178 |
+
# format checks in task_2/3 do cover it
|
| 179 |
+
if with_setting and args.get("setting"):
|
| 180 |
+
yield str(args["setting"])
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def validate(task: str, args: dict, caption: str):
|
| 184 |
+
if args is None or not SCHEMA_KEYS.issubset(args.keys()):
|
| 185 |
+
return False
|
| 186 |
+
leaves = list(_leaves(args, with_setting=(task != "task_1")))
|
| 187 |
+
if not leaves:
|
| 188 |
+
return False
|
| 189 |
+
if task == "task_1":
|
| 190 |
+
cap = caption.lower()
|
| 191 |
+
return all(all(w in cap for w in lf.lower().split()) for lf in leaves)
|
| 192 |
+
if task == "task_2":
|
| 193 |
+
return all(re.fullmatch(r"\[[a-z_]+\]", lf) for lf in leaves)
|
| 194 |
+
if task == "task_3":
|
| 195 |
+
ok, counters = True, {}
|
| 196 |
+
for lf in leaves:
|
| 197 |
+
m = re.fullmatch(r"\[(ENTITY|ATTRIBUTE|ACTION|[A-Z_]+?)(?:_(\d+))?\]", lf)
|
| 198 |
+
if not m:
|
| 199 |
+
return False
|
| 200 |
+
if m.group(2):
|
| 201 |
+
k, n = m.group(1), int(m.group(2))
|
| 202 |
+
ok &= n == counters.get(k, 0) + 1 or n <= counters.get(k, 0) + 1
|
| 203 |
+
counters[k] = max(counters.get(k, 0), n)
|
| 204 |
+
return ok
|
| 205 |
+
return False
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# ================================ arms ============================================
|
| 209 |
+
def load_instruct(arm: str, seed: int = 0, device="cuda"):
|
| 210 |
+
from transformers import AutoModelForCausalLM
|
| 211 |
+
torch.manual_seed(seed)
|
| 212 |
+
model = AutoModelForCausalLM.from_pretrained(INSTRUCT_MODEL,
|
| 213 |
+
dtype=torch.float32)
|
| 214 |
+
model.config.use_cache = False
|
| 215 |
+
for p in model.parameters():
|
| 216 |
+
p.requires_grad_(False)
|
| 217 |
+
adapters = None
|
| 218 |
+
if arm != "frozen":
|
| 219 |
+
d = model.config.hidden_size
|
| 220 |
+
mk = (lambda: RelayPatchwork(d)) if arm.startswith("relay_pw") \
|
| 221 |
+
else (lambda: MLPAdapter(d, hidden=128))
|
| 222 |
+
adapters = nn.ModuleList([mk() for _ in model.model.layers])
|
| 223 |
+
model.model.layers = nn.ModuleList(
|
| 224 |
+
[_QwenBlockWithAdapter(b, a)
|
| 225 |
+
for b, a in zip(model.model.layers, adapters)])
|
| 226 |
+
return model.to(device), adapters
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@torch.no_grad()
|
| 230 |
+
def ppl_per_task(model, blob, device="cuda", n=25):
|
| 231 |
+
out = {}
|
| 232 |
+
for task in TASKS:
|
| 233 |
+
g = torch.Generator().manual_seed(0)
|
| 234 |
+
ls = []
|
| 235 |
+
for _ in range(n):
|
| 236 |
+
x, y = _batch(blob["hold"][task], BATCH, BLOCK, device, g)
|
| 237 |
+
logits = model(input_ids=x).logits
|
| 238 |
+
ls.append(F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 239 |
+
y.reshape(-1)).item())
|
| 240 |
+
out[task] = round(math.exp(sum(ls) / len(ls)), 3)
|
| 241 |
+
return out
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
@torch.no_grad()
|
| 245 |
+
def task_validity(model, blob, device="cuda", n_eval=48, max_new=200):
|
| 246 |
+
from transformers import AutoTokenizer
|
| 247 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 248 |
+
model.eval()
|
| 249 |
+
model.config.use_cache = True
|
| 250 |
+
out = {}
|
| 251 |
+
for task in TASKS:
|
| 252 |
+
rows = blob["eval_rows"][task][:n_eval]
|
| 253 |
+
js = va = 0
|
| 254 |
+
for r in rows:
|
| 255 |
+
ids = tok(r["gen_prompt"], return_tensors="pt").input_ids.to(device)
|
| 256 |
+
gen = model.generate(ids, max_new_tokens=max_new, do_sample=False,
|
| 257 |
+
pad_token_id=tok.eos_token_id)
|
| 258 |
+
txt = tok.decode(gen[0][ids.shape[1]:], skip_special_tokens=False)
|
| 259 |
+
args = parse_tool_json(txt)
|
| 260 |
+
if args is not None and SCHEMA_KEYS.issubset(args.keys()):
|
| 261 |
+
js += 1
|
| 262 |
+
if validate(task, args, r["prompt"]):
|
| 263 |
+
va += 1
|
| 264 |
+
out[task] = {"json_valid": round(js / len(rows), 4),
|
| 265 |
+
"task_valid": round(va / len(rows), 4)}
|
| 266 |
+
model.config.use_cache = False
|
| 267 |
+
return out
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
# ============================ the register probe ==================================
|
| 271 |
+
@torch.no_grad()
|
| 272 |
+
def register_probe(model, adapters, blob, device="cuda", n_caps=24):
|
| 273 |
+
"""Sign-codes per relay layer for the SAME captions under 4 registers
|
| 274 |
+
(task_1/2/3 gen-prompts + plain continuation). Separation per layer =
|
| 275 |
+
mean inter-register Hamming - mean intra-register Hamming."""
|
| 276 |
+
from transformers import AutoTokenizer
|
| 277 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 278 |
+
model.eval()
|
| 279 |
+
caps = [r["prompt"] for r in blob["eval_rows"]["task_1"][:n_caps]]
|
| 280 |
+
regs = {}
|
| 281 |
+
for task in TASKS:
|
| 282 |
+
by_prompt = {r["prompt"]: r["gen_prompt"]
|
| 283 |
+
for r in blob["eval_rows"][task]}
|
| 284 |
+
regs[task] = [by_prompt.get(c, "") for c in caps]
|
| 285 |
+
regs["plain"] = [c for c in caps]
|
| 286 |
+
layer_ids = [0, 8, 16, 23]
|
| 287 |
+
# hook-based collection (one pass per register, hooks read the codes)
|
| 288 |
+
codes = {reg: {i: [] for i in layer_ids} for reg in regs}
|
| 289 |
+
for reg, prompts in regs.items():
|
| 290 |
+
hooks, store = [], {i: [] for i in layer_ids}
|
| 291 |
+
|
| 292 |
+
def mk(i, a):
|
| 293 |
+
def h(mod, inp, out):
|
| 294 |
+
x = inp[0][:, -1:]
|
| 295 |
+
slots = a.proj(x).view(*x.shape[:-1], a.n_slots, 4)
|
| 296 |
+
A = F.normalize(a.addr.codebook, dim=-1)
|
| 297 |
+
cos = F.normalize(slots, dim=-1) @ A.T
|
| 298 |
+
win = cos.abs().argmax(-1)
|
| 299 |
+
sgn = (torch.gather(cos, -1, win.unsqueeze(-1))
|
| 300 |
+
.squeeze(-1) > 0).long()
|
| 301 |
+
store[i].append((win * 2 + sgn).reshape(-1).cpu())
|
| 302 |
+
return h
|
| 303 |
+
for i in layer_ids:
|
| 304 |
+
hooks.append(adapters[i].register_forward_hook(mk(i, adapters[i])))
|
| 305 |
+
for pr in prompts:
|
| 306 |
+
if pr:
|
| 307 |
+
ids = tok(pr, return_tensors="pt").input_ids.to(device)
|
| 308 |
+
model(input_ids=ids)
|
| 309 |
+
for h in hooks:
|
| 310 |
+
h.remove()
|
| 311 |
+
for i in layer_ids:
|
| 312 |
+
codes[reg][i] = torch.stack(store[i]) if store[i] else None
|
| 313 |
+
sep = {}
|
| 314 |
+
for i in layer_ids:
|
| 315 |
+
mats = {r: codes[r][i] for r in codes if codes[r][i] is not None}
|
| 316 |
+
names = list(mats)
|
| 317 |
+
def ham(a, b):
|
| 318 |
+
return (a.unsqueeze(1) != b.unsqueeze(0)).float().mean().item()
|
| 319 |
+
intra = sum(ham(mats[r], mats[r]) for r in names) / len(names)
|
| 320 |
+
pairs = [(a, b) for ai, a in enumerate(names) for b in names[ai + 1:]]
|
| 321 |
+
inter = sum(ham(mats[a], mats[b]) for a, b in pairs) / len(pairs)
|
| 322 |
+
sep[f"L{i}"] = {"inter": round(inter, 4), "intra": round(intra, 4),
|
| 323 |
+
"sep": round(inter - intra, 4)}
|
| 324 |
+
return sep
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
# ================================ runner ==========================================
|
| 328 |
+
def train_arm3(arm: str, seed: int = 0, steps=STEPS, device="cuda"):
|
| 329 |
+
if not torch.cuda.is_available():
|
| 330 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 331 |
+
os.makedirs(EXP3_DIR, exist_ok=True)
|
| 332 |
+
blob = build_caches()
|
| 333 |
+
model, adapters = load_instruct(arm, seed=seed, device=device)
|
| 334 |
+
ledger = open(os.path.join(EXP3_DIR, "ledger.jsonl"), "a", encoding="utf-8")
|
| 335 |
+
if arm == "frozen":
|
| 336 |
+
rec = {"exp": "q3", "arm": arm, "seed": seed,
|
| 337 |
+
"ppl": ppl_per_task(model, blob, device=device),
|
| 338 |
+
"validity": task_validity(model, blob, device=device)}
|
| 339 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.close()
|
| 340 |
+
print(f"[q3 frozen] FINAL {rec['ppl']} {rec['validity']}", flush=True)
|
| 341 |
+
return
|
| 342 |
+
# training stream: task_1 only, or all three interleaved
|
| 343 |
+
tasks = ("task_1",) if arm.endswith("task1") else TASKS
|
| 344 |
+
g = torch.Generator().manual_seed(seed)
|
| 345 |
+
opt = torch.optim.Adam(adapters.parameters(), lr=LR, weight_decay=0.0)
|
| 346 |
+
model.train()
|
| 347 |
+
for step in range(1, steps + 1):
|
| 348 |
+
task = tasks[int(torch.randint(len(tasks), (1,), generator=g))]
|
| 349 |
+
x, y = _batch(blob["train"][task], BATCH, BLOCK, device, g)
|
| 350 |
+
logits = model(input_ids=x).logits
|
| 351 |
+
loss = F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 352 |
+
y.reshape(-1))
|
| 353 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 354 |
+
if step % 1000 == 0:
|
| 355 |
+
print(f"[q3 {arm} s{seed} step {step}] loss={loss.item():.3f}",
|
| 356 |
+
flush=True)
|
| 357 |
+
rec = {"exp": "q3", "arm": arm, "seed": seed, "steps": steps,
|
| 358 |
+
"trainable": sum(p.numel() for p in adapters.parameters()),
|
| 359 |
+
"ppl": ppl_per_task(model, blob, device=device),
|
| 360 |
+
"validity": task_validity(model, blob, device=device)}
|
| 361 |
+
if arm.startswith("relay_pw"):
|
| 362 |
+
rec["register_sep"] = register_probe(model, adapters, blob,
|
| 363 |
+
device=device)
|
| 364 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.close()
|
| 365 |
+
print(f"[q3 {arm} s{seed}] FINAL ppl={rec['ppl']} "
|
| 366 |
+
f"validity={rec['validity']} sep={rec.get('register_sep')}",
|
| 367 |
+
flush=True)
|
| 368 |
+
torch.save({"arm": arm, "seed": seed,
|
| 369 |
+
"adapters": {k: v.cpu() for k, v in
|
| 370 |
+
adapters.state_dict().items()}},
|
| 371 |
+
os.path.join(EXP3_DIR, f"q3_{arm}_s{seed}.pt"))
|
| 372 |
+
del model, adapters
|
| 373 |
+
torch.cuda.empty_cache()
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def wikitext_stack_probe(device="cuda"):
|
| 377 |
+
"""The comparison cell: the exp002 WIKITEXT-trained relay_pw stack probed
|
| 378 |
+
on the same registers (overlap prediction) — loaded onto the BASE model it
|
| 379 |
+
was trained on, prompts rendered with the instruct template regardless
|
| 380 |
+
(identical probe inputs across stacks)."""
|
| 381 |
+
from transformers import AutoModelForCausalLM
|
| 382 |
+
blob = build_caches()
|
| 383 |
+
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B",
|
| 384 |
+
dtype=torch.float32)
|
| 385 |
+
model.config.use_cache = False
|
| 386 |
+
for p in model.parameters():
|
| 387 |
+
p.requires_grad_(False)
|
| 388 |
+
d = model.config.hidden_size
|
| 389 |
+
adapters = nn.ModuleList([RelayPatchwork(d) for _ in model.model.layers])
|
| 390 |
+
ck = torch.load(os.path.join(DATA_ROOT, "qwen_exp002", "q2_relay_pw_s0.pt"),
|
| 391 |
+
map_location="cpu", weights_only=True)
|
| 392 |
+
adapters.load_state_dict(ck["adapters"])
|
| 393 |
+
model.model.layers = nn.ModuleList(
|
| 394 |
+
[_QwenBlockWithAdapter(b, a)
|
| 395 |
+
for b, a in zip(model.model.layers, adapters)])
|
| 396 |
+
model = model.to(device).eval()
|
| 397 |
+
sep = register_probe(model, adapters, blob, device=device)
|
| 398 |
+
ledger = open(os.path.join(EXP3_DIR, "ledger.jsonl"), "a", encoding="utf-8")
|
| 399 |
+
ledger.write(json.dumps({"exp": "q3", "arm": "wikitext_stack_probe",
|
| 400 |
+
"seed": 0, "register_sep": sep}) + "\n")
|
| 401 |
+
ledger.close()
|
| 402 |
+
print(f"[q3 wikitext_stack_probe] sep={sep}", flush=True)
|
| 403 |
+
del model, adapters
|
| 404 |
+
torch.cuda.empty_cache()
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def run_exp003(seeds=(0, 1), device="cuda"):
|
| 408 |
+
train_arm3("frozen", seed=0, device=device)
|
| 409 |
+
wikitext_stack_probe(device=device)
|
| 410 |
+
for seed in seeds:
|
| 411 |
+
for arm in ("relay_pw_task1", "relay_pw_all", "mlp_all"):
|
| 412 |
+
train_arm3(arm, seed=seed, device=device)
|
| 413 |
+
print("=== qwen exp003 COMPLETE ===", flush=True)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def smoke():
|
| 417 |
+
ok = parse_tool_json('x <tool_call>\n{"name":"emit_caption_schema",'
|
| 418 |
+
'"arguments":{"subjects":[{"name":"cat","attributes":'
|
| 419 |
+
'["black"]}],"actions":["sitting"],"setting":"indoor",'
|
| 420 |
+
'"style":null,"mood":null}}\n</tool_call>')
|
| 421 |
+
assert ok and validate("task_1", ok, "A black cat sitting indoor scene.")
|
| 422 |
+
assert not validate("task_1", ok, "A dog on grass.") # ungrounded
|
| 423 |
+
t2 = {"subjects": [{"name": "[pet]", "attributes": ["[color]"]}],
|
| 424 |
+
"actions": ["[resting]"], "setting": "[indoor]", "style": None,
|
| 425 |
+
"mood": None}
|
| 426 |
+
assert validate("task_2", t2, "")
|
| 427 |
+
t3 = {"subjects": [{"name": "[ENTITY_1]", "attributes": ["[ATTRIBUTE_1]"]}],
|
| 428 |
+
"actions": ["[ACTION_1]"], "setting": "[INDOOR]", "style": None,
|
| 429 |
+
"mood": None}
|
| 430 |
+
assert validate("task_3", t3, "")
|
| 431 |
+
assert not validate("task_2", t3, "") # cross-task
|
| 432 |
+
r = RelayPatchwork(896)
|
| 433 |
+
m = MLPAdapter(896, hidden=128)
|
| 434 |
+
rp = sum(p.numel() for p in r.parameters())
|
| 435 |
+
mp = sum(p.numel() for p in m.parameters())
|
| 436 |
+
assert abs(rp - mp) / rp < 0.01, (rp, mp) # budget-matched
|
| 437 |
+
print(f"qwen exp003 smoke passed (validators OK; relay_pw {rp:,} ~ "
|
| 438 |
+
f"mlp128 {mp:,}; full run needs GPU + instruct download)")
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def _in_notebook():
|
| 442 |
+
try:
|
| 443 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 444 |
+
return True
|
| 445 |
+
except NameError:
|
| 446 |
+
return False
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
if __name__ == "__main__":
|
| 450 |
+
smoke() if not _in_notebook() else (smoke(),
|
| 451 |
+
print("Notebook: run_exp003() on GPU."))
|
exp007_math/qwen_exp006_story.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""qwen_exp006_story.py — geolip-aleph-qwen EXPERIMENT 6: story anchors —
|
| 2 |
+
one relay_pw stack, FOUR methods of requesting a story (Phil's overnight
|
| 3 |
+
directive 2026-07-12: "tinystories with multiple methods of requesting
|
| 4 |
+
stories"). The register-differentiation program moves from extraction
|
| 5 |
+
registers (exp003) and composite emission (exp004) to open-ended GENERATION
|
| 6 |
+
with a per-register measurable constraint.
|
| 7 |
+
|
| 8 |
+
DATA: roneneldan/TinyStories validation parquet (single 10MB file), stories
|
| 9 |
+
40-180 words, ~4.8k after shuffle, story-hash holdout 10%. From EACH story
|
| 10 |
+
four request registers are derived deterministically:
|
| 11 |
+
cont — continuation: user gives the opening 2 sentences, assistant
|
| 12 |
+
completes the story (no tools);
|
| 13 |
+
inst — instruct: "Tell me a short story about {topic}" (topic = 2 salient
|
| 14 |
+
content words), assistant tells the full story;
|
| 15 |
+
kw — keyword-constrained: "Write a short story using these words:
|
| 16 |
+
w1, w2, w3" (3 content words hash-sampled from the story) — the
|
| 17 |
+
constraint is CHECKABLE at judge time;
|
| 18 |
+
json — tool call: emit_story({title, story}) — the structured register.
|
| 19 |
+
|
| 20 |
+
ARMS: frozen (zero-shot baseline) | relay_pw_story (ONE stack, all four
|
| 21 |
+
registers interleaved — exp003 showed multi-task resolves capture). 3000
|
| 22 |
+
steps, block 512, batch 4, pure Adam lr 1e-3 wd 0.
|
| 23 |
+
|
| 24 |
+
JUDGES: per-register held-out ppl; GENERATION validity n=12/register
|
| 25 |
+
(terminates before cap + register-specific checks: cont/inst/kw word counts
|
| 26 |
+
and sentence shape, kw = all 3 keywords present, json = parse + keys +
|
| 27 |
+
story length; novelty check — the continuation must not just parrot its
|
| 28 |
+
prompt); the REGISTER PROBE (sign-code separation across the 4 request
|
| 29 |
+
methods, exp003 gauge). Riders: trunk frozen; GPU-only verdicts; Colab-safe;
|
| 30 |
+
step-50 peak_mem telemetry (the WDDM sysmem-spill law).
|
| 31 |
+
Paste order: geolip_vitals -> ar_differentiation_bed ->
|
| 32 |
+
exp013_augmentation_bed -> qwen_exp001_relay -> qwen_exp002_refine ->
|
| 33 |
+
qwen_exp003_instruct -> this file.
|
| 34 |
+
"""
|
| 35 |
+
from __future__ import annotations
|
| 36 |
+
import hashlib
|
| 37 |
+
import json
|
| 38 |
+
import math
|
| 39 |
+
import os
|
| 40 |
+
import re
|
| 41 |
+
import torch
|
| 42 |
+
import torch.nn as nn
|
| 43 |
+
import torch.nn.functional as F
|
| 44 |
+
|
| 45 |
+
if "parse_tool_json" not in globals():
|
| 46 |
+
try:
|
| 47 |
+
from qwen_exp001_relay import _QwenBlockWithAdapter, _batch
|
| 48 |
+
from qwen_exp002_refine import RelayPatchwork
|
| 49 |
+
from qwen_exp003_instruct import parse_tool_json, INSTRUCT_MODEL
|
| 50 |
+
except ImportError:
|
| 51 |
+
_here = globals().get("__file__")
|
| 52 |
+
if _here is None:
|
| 53 |
+
raise ImportError("paste the qwen stack first")
|
| 54 |
+
import sys, pathlib
|
| 55 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 56 |
+
from qwen_exp001_relay import _QwenBlockWithAdapter, _batch
|
| 57 |
+
from qwen_exp002_refine import RelayPatchwork
|
| 58 |
+
from qwen_exp003_instruct import parse_tool_json, INSTRUCT_MODEL
|
| 59 |
+
|
| 60 |
+
DATA_ROOT = os.environ.get("GEOLIP_DATA", "./data")
|
| 61 |
+
EXP6_DIR = os.path.join(DATA_ROOT, "qwen_exp006")
|
| 62 |
+
BLOCK, BATCH, STEPS, LR = 512, 4, 3000, 1e-3
|
| 63 |
+
MAX_NEW, N_EVAL = 320, 12
|
| 64 |
+
REGS = ("cont", "inst", "kw", "json")
|
| 65 |
+
|
| 66 |
+
STORY_SRC = ("roneneldan/TinyStories",
|
| 67 |
+
"data/validation-00000-of-00001-869c898b519ad725.parquet")
|
| 68 |
+
|
| 69 |
+
TOOL = [{
|
| 70 |
+
"type": "function",
|
| 71 |
+
"function": {
|
| 72 |
+
"name": "emit_story",
|
| 73 |
+
"description": "Return a short story with a title.",
|
| 74 |
+
"parameters": {
|
| 75 |
+
"type": "object",
|
| 76 |
+
"properties": {
|
| 77 |
+
"title": {"type": "string"},
|
| 78 |
+
"story": {"type": "string"},
|
| 79 |
+
},
|
| 80 |
+
"required": ["title", "story"],
|
| 81 |
+
},
|
| 82 |
+
},
|
| 83 |
+
}]
|
| 84 |
+
|
| 85 |
+
SYS = {
|
| 86 |
+
"cont": "Continue the story the user started. Keep the same characters "
|
| 87 |
+
"and simple words. Finish the story.",
|
| 88 |
+
"inst": "You are a storyteller. Tell one short story in simple words. "
|
| 89 |
+
"Give it a beginning, a middle, and an end.",
|
| 90 |
+
"kw": "You are a storyteller. Write one short story in simple words "
|
| 91 |
+
"that uses every word the user requires.",
|
| 92 |
+
"json": "Return one short story by calling the emit_story tool with a "
|
| 93 |
+
"title and the story text.",
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
_STOP = set("the a an and or but so then was were is are had has have he she "
|
| 97 |
+
"it they them his her its their there this that with very to of "
|
| 98 |
+
"in on at for said says say not too you i we one day once upon "
|
| 99 |
+
"time when all out up down back went go going got get".split())
|
| 100 |
+
|
| 101 |
+
_SENT_RE = re.compile(r"(?<=[.!?])\s+")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _sentences(story: str):
|
| 105 |
+
return [s.strip() for s in _SENT_RE.split(story.strip()) if s.strip()]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _content_words(story: str):
|
| 109 |
+
seen, out = set(), []
|
| 110 |
+
for w in re.findall(r"[A-Za-z']+", story):
|
| 111 |
+
lw = w.lower()
|
| 112 |
+
if len(lw) > 3 and lw not in _STOP and lw not in seen:
|
| 113 |
+
seen.add(lw)
|
| 114 |
+
out.append(lw)
|
| 115 |
+
return out
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _h(story: str) -> int:
|
| 119 |
+
return int(hashlib.md5(story.encode()).hexdigest(), 16)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def derive_registers(story: str):
|
| 123 |
+
"""Deterministic 4-register derivation for one story; None if the story
|
| 124 |
+
can't support a register (too few sentences/content words)."""
|
| 125 |
+
sents = _sentences(story)
|
| 126 |
+
words = _content_words(story)
|
| 127 |
+
if len(sents) < 4 or len(words) < 5:
|
| 128 |
+
return None
|
| 129 |
+
h = _h(story)
|
| 130 |
+
opening = " ".join(sents[:2])
|
| 131 |
+
rest = " ".join(sents[2:])
|
| 132 |
+
topic = " and ".join(words[:2])
|
| 133 |
+
kws = [words[(h >> (8 * i)) % len(words)] for i in range(3)]
|
| 134 |
+
if len(set(kws)) < 3: # hash collision -> first three
|
| 135 |
+
kws = words[:3]
|
| 136 |
+
title = " ".join(story.split()[:5]) + "..."
|
| 137 |
+
return {
|
| 138 |
+
"cont": {"user": opening, "target": rest},
|
| 139 |
+
"inst": {"user": f"Tell me a short story about {topic}.",
|
| 140 |
+
"target": story},
|
| 141 |
+
"kw": {"user": "Write a short story using these words: "
|
| 142 |
+
f"{kws[0]}, {kws[1]}, {kws[2]}.",
|
| 143 |
+
"target": story, "kws": kws},
|
| 144 |
+
"json": {"user": f"Tell me a short story about {topic}.",
|
| 145 |
+
"target": {"title": title, "story": story}},
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _render(tok, reg, user, target=None, gen_prompt=False):
|
| 150 |
+
msgs = [{"role": "system", "content": SYS[reg]},
|
| 151 |
+
{"role": "user", "content": user}]
|
| 152 |
+
tools = TOOL if reg == "json" else None
|
| 153 |
+
if not gen_prompt:
|
| 154 |
+
if reg == "json":
|
| 155 |
+
msgs.append({"role": "assistant", "tool_calls": [{
|
| 156 |
+
"type": "function",
|
| 157 |
+
"function": {"name": "emit_story", "arguments": target}}]})
|
| 158 |
+
else:
|
| 159 |
+
msgs.append({"role": "assistant", "content": target})
|
| 160 |
+
try:
|
| 161 |
+
return tok.apply_chat_template(msgs, tools=tools, tokenize=False,
|
| 162 |
+
add_generation_prompt=gen_prompt)
|
| 163 |
+
except Exception:
|
| 164 |
+
msgs2 = json.loads(json.dumps(msgs))
|
| 165 |
+
for m in msgs2:
|
| 166 |
+
for tc in m.get("tool_calls", []) or []:
|
| 167 |
+
if isinstance(tc["function"]["arguments"], dict):
|
| 168 |
+
tc["function"]["arguments"] = json.dumps(
|
| 169 |
+
tc["function"]["arguments"])
|
| 170 |
+
return tok.apply_chat_template(msgs2, tools=tools, tokenize=False,
|
| 171 |
+
add_generation_prompt=gen_prompt)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def build_caches():
|
| 175 |
+
os.makedirs(EXP6_DIR, exist_ok=True)
|
| 176 |
+
path = os.path.join(EXP6_DIR, "story_cache.pt")
|
| 177 |
+
if os.path.exists(path):
|
| 178 |
+
return torch.load(path, map_location="cpu", weights_only=False)
|
| 179 |
+
from huggingface_hub import hf_hub_download
|
| 180 |
+
import pyarrow.parquet as pq
|
| 181 |
+
from transformers import AutoTokenizer
|
| 182 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 183 |
+
p = hf_hub_download(STORY_SRC[0], STORY_SRC[1], repo_type="dataset")
|
| 184 |
+
texts = pq.read_table(p, columns=["text"]).column("text").to_pylist()
|
| 185 |
+
stories = [t.strip() for t in texts
|
| 186 |
+
if t and 40 <= len(t.split()) <= 180]
|
| 187 |
+
g = torch.Generator().manual_seed(0)
|
| 188 |
+
stories = [stories[i] for i in
|
| 189 |
+
torch.randperm(len(stories), generator=g).tolist()][:4800]
|
| 190 |
+
train_txt = {r: [] for r in REGS}
|
| 191 |
+
hold_txt = {r: [] for r in REGS}
|
| 192 |
+
eval_rows = {r: [] for r in REGS}
|
| 193 |
+
n_used = 0
|
| 194 |
+
for st in stories:
|
| 195 |
+
d = derive_registers(st)
|
| 196 |
+
if d is None:
|
| 197 |
+
continue
|
| 198 |
+
n_used += 1
|
| 199 |
+
holdout = _h(st) % 10 == 0
|
| 200 |
+
for r in REGS:
|
| 201 |
+
txt = _render(tok, r, d[r]["user"], d[r]["target"])
|
| 202 |
+
if holdout:
|
| 203 |
+
hold_txt[r].append(txt)
|
| 204 |
+
if len(eval_rows[r]) < 48:
|
| 205 |
+
eval_rows[r].append({
|
| 206 |
+
"user": d[r]["user"],
|
| 207 |
+
"gen_prompt": _render(tok, r, d[r]["user"],
|
| 208 |
+
gen_prompt=True),
|
| 209 |
+
"kws": d[r].get("kws"),
|
| 210 |
+
"opening": d["cont"]["user"] if r == "cont" else None})
|
| 211 |
+
else:
|
| 212 |
+
train_txt[r].append(txt)
|
| 213 |
+
blob = {
|
| 214 |
+
"train": {r: tok("\n".join(train_txt[r]),
|
| 215 |
+
return_tensors="pt").input_ids[0] for r in REGS},
|
| 216 |
+
"hold": {r: tok("\n".join(hold_txt[r]),
|
| 217 |
+
return_tensors="pt").input_ids[0] for r in REGS},
|
| 218 |
+
"eval_rows": eval_rows,
|
| 219 |
+
"stats": {"n_stories": n_used,
|
| 220 |
+
"n_train": len(train_txt["cont"]),
|
| 221 |
+
"n_hold": len(hold_txt["cont"])},
|
| 222 |
+
}
|
| 223 |
+
torch.save(blob, path)
|
| 224 |
+
print(f"[cache q6] {blob['stats']}", flush=True)
|
| 225 |
+
return blob
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# ================================ arms ============================================
|
| 229 |
+
def load_arm6(arm: str, seed: int = 0, device="cuda"):
|
| 230 |
+
from transformers import AutoModelForCausalLM
|
| 231 |
+
torch.manual_seed(seed)
|
| 232 |
+
model = AutoModelForCausalLM.from_pretrained(INSTRUCT_MODEL,
|
| 233 |
+
dtype=torch.float32)
|
| 234 |
+
model.config.use_cache = False
|
| 235 |
+
for p in model.parameters():
|
| 236 |
+
p.requires_grad_(False)
|
| 237 |
+
adapters = None
|
| 238 |
+
if arm != "frozen":
|
| 239 |
+
d = model.config.hidden_size
|
| 240 |
+
adapters = nn.ModuleList([RelayPatchwork(d)
|
| 241 |
+
for _ in model.model.layers])
|
| 242 |
+
model.model.layers = nn.ModuleList(
|
| 243 |
+
[_QwenBlockWithAdapter(b, a)
|
| 244 |
+
for b, a in zip(model.model.layers, adapters)])
|
| 245 |
+
return model.to(device), adapters
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# ================================ judges ==========================================
|
| 249 |
+
@torch.no_grad()
|
| 250 |
+
def ppl_per_reg(model, blob, device="cuda", n=15):
|
| 251 |
+
g = torch.Generator().manual_seed(0)
|
| 252 |
+
model.eval()
|
| 253 |
+
out = {}
|
| 254 |
+
for r in REGS:
|
| 255 |
+
ls = []
|
| 256 |
+
for _ in range(n):
|
| 257 |
+
x, y = _batch(blob["hold"][r], BATCH, BLOCK, device, g)
|
| 258 |
+
logits = model(input_ids=x).logits
|
| 259 |
+
ls.append(F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 260 |
+
y.reshape(-1)).item())
|
| 261 |
+
out[r] = round(math.exp(sum(ls) / len(ls)), 3)
|
| 262 |
+
return out
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _word_count(t: str) -> int:
|
| 266 |
+
return len(re.findall(r"[A-Za-z']+", t))
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def check_gen(reg: str, row: dict, txt: str, closed: bool):
|
| 270 |
+
"""Register-specific validity for one generation. Returns (ok, reason)."""
|
| 271 |
+
if not closed:
|
| 272 |
+
return False, "no_eos"
|
| 273 |
+
body = txt.split("<|im_end|>")[0].strip()
|
| 274 |
+
if reg == "json":
|
| 275 |
+
args = parse_tool_json(body)
|
| 276 |
+
if args is None:
|
| 277 |
+
return False, "json_error"
|
| 278 |
+
if not isinstance(args.get("title"), str) or \
|
| 279 |
+
not isinstance(args.get("story"), str):
|
| 280 |
+
return False, "missing_keys"
|
| 281 |
+
if _word_count(args["story"]) < 30:
|
| 282 |
+
return False, "too_short"
|
| 283 |
+
return True, "ok"
|
| 284 |
+
if _word_count(body) < (40 if reg == "inst" else 30):
|
| 285 |
+
return False, "too_short"
|
| 286 |
+
if len(_sentences(body)) < 2:
|
| 287 |
+
return False, "not_story_shaped"
|
| 288 |
+
if reg == "kw":
|
| 289 |
+
low = body.lower()
|
| 290 |
+
if not all(k in low for k in row["kws"]):
|
| 291 |
+
return False, "missing_keyword"
|
| 292 |
+
if reg == "cont":
|
| 293 |
+
# novelty: the continuation must not just parrot its opening
|
| 294 |
+
op_words = set(row["opening"].lower().split())
|
| 295 |
+
new = [w for w in body.lower().split() if w not in op_words]
|
| 296 |
+
if len(new) < 15:
|
| 297 |
+
return False, "parrot"
|
| 298 |
+
return True, "ok"
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
@torch.no_grad()
|
| 302 |
+
def story_validity(model, blob, device="cuda", n_eval=N_EVAL):
|
| 303 |
+
from transformers import AutoTokenizer
|
| 304 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 305 |
+
model.eval()
|
| 306 |
+
model.config.use_cache = True
|
| 307 |
+
out = {}
|
| 308 |
+
for r in REGS:
|
| 309 |
+
oks, reasons, samples = 0, {}, []
|
| 310 |
+
rows = blob["eval_rows"][r][:n_eval]
|
| 311 |
+
for i, row in enumerate(rows):
|
| 312 |
+
ids = tok(row["gen_prompt"],
|
| 313 |
+
return_tensors="pt").input_ids.to(device)
|
| 314 |
+
gen = model.generate(ids, max_new_tokens=MAX_NEW, do_sample=False,
|
| 315 |
+
pad_token_id=tok.eos_token_id)
|
| 316 |
+
txt = tok.decode(gen[0][ids.shape[1]:], skip_special_tokens=False)
|
| 317 |
+
closed = len(gen[0]) - ids.shape[1] < MAX_NEW or \
|
| 318 |
+
"<|im_end|>" in txt
|
| 319 |
+
ok, why = check_gen(r, row, txt, closed)
|
| 320 |
+
oks += int(ok)
|
| 321 |
+
reasons[why] = reasons.get(why, 0) + 1
|
| 322 |
+
if i < 2:
|
| 323 |
+
samples.append(txt[:300])
|
| 324 |
+
out[r] = {"valid": round(oks / len(rows), 3), "reasons": reasons,
|
| 325 |
+
"samples": samples}
|
| 326 |
+
model.config.use_cache = False
|
| 327 |
+
return out
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
@torch.no_grad()
|
| 331 |
+
def register_probe6(model, adapters, blob, device="cuda", n=24):
|
| 332 |
+
"""exp003 gauge on the 4 request registers: sign-code separation per
|
| 333 |
+
relay layer = mean inter-register Hamming - mean intra-register."""
|
| 334 |
+
from transformers import AutoTokenizer
|
| 335 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 336 |
+
model.eval()
|
| 337 |
+
layer_ids = [0, 8, 16, 23]
|
| 338 |
+
codes = {r: {i: [] for i in layer_ids} for r in REGS}
|
| 339 |
+
for r in REGS:
|
| 340 |
+
prompts = [row["gen_prompt"] for row in blob["eval_rows"][r][:n]]
|
| 341 |
+
store = {i: [] for i in layer_ids}
|
| 342 |
+
hooks = []
|
| 343 |
+
|
| 344 |
+
def mk(i, a):
|
| 345 |
+
def h(mod, inp, out):
|
| 346 |
+
x = inp[0][:, -1:]
|
| 347 |
+
slots = a.proj(x).view(*x.shape[:-1], a.n_slots, 4)
|
| 348 |
+
A = F.normalize(a.addr.codebook, dim=-1)
|
| 349 |
+
cos = F.normalize(slots, dim=-1) @ A.T
|
| 350 |
+
win = cos.abs().argmax(-1)
|
| 351 |
+
sgn = (torch.gather(cos, -1, win.unsqueeze(-1))
|
| 352 |
+
.squeeze(-1) > 0).long()
|
| 353 |
+
store[i].append((win * 2 + sgn).reshape(-1).cpu())
|
| 354 |
+
return h
|
| 355 |
+
for i in layer_ids:
|
| 356 |
+
hooks.append(adapters[i].register_forward_hook(
|
| 357 |
+
mk(i, adapters[i])))
|
| 358 |
+
for pr in prompts:
|
| 359 |
+
ids = tok(pr, return_tensors="pt").input_ids.to(device)
|
| 360 |
+
model(input_ids=ids)
|
| 361 |
+
for h in hooks:
|
| 362 |
+
h.remove()
|
| 363 |
+
for i in layer_ids:
|
| 364 |
+
codes[r][i] = torch.stack(store[i])
|
| 365 |
+
sep = {}
|
| 366 |
+
for i in layer_ids:
|
| 367 |
+
def ham(a, b):
|
| 368 |
+
return (a.unsqueeze(1) != b.unsqueeze(0)).float().mean().item()
|
| 369 |
+
intra = sum(ham(codes[r][i], codes[r][i]) for r in REGS) / len(REGS)
|
| 370 |
+
pairs = [(a, b) for ai, a in enumerate(REGS) for b in REGS[ai + 1:]]
|
| 371 |
+
inter = sum(ham(codes[a][i], codes[b][i]) for a, b in pairs) \
|
| 372 |
+
/ len(pairs)
|
| 373 |
+
sep[f"L{i}"] = {"inter": round(inter, 4), "intra": round(intra, 4),
|
| 374 |
+
"sep": round(inter - intra, 4)}
|
| 375 |
+
return sep
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# ================================ runner ==========================================
|
| 379 |
+
def run_arm6(arm: str, seed: int = 0, steps=STEPS, device="cuda"):
|
| 380 |
+
if not torch.cuda.is_available():
|
| 381 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 382 |
+
os.makedirs(EXP6_DIR, exist_ok=True)
|
| 383 |
+
blob = build_caches()
|
| 384 |
+
model, adapters = load_arm6(arm, seed=seed, device=device)
|
| 385 |
+
ledger = open(os.path.join(EXP6_DIR, "ledger.jsonl"), "a",
|
| 386 |
+
encoding="utf-8")
|
| 387 |
+
if arm != "frozen":
|
| 388 |
+
g = torch.Generator().manual_seed(seed)
|
| 389 |
+
opt = torch.optim.Adam(adapters.parameters(), lr=LR, weight_decay=0.0)
|
| 390 |
+
model.train()
|
| 391 |
+
for step in range(1, steps + 1):
|
| 392 |
+
r = REGS[int(torch.randint(len(REGS), (1,), generator=g))]
|
| 393 |
+
x, y = _batch(blob["train"][r], BATCH, BLOCK, device, g)
|
| 394 |
+
logits = model(input_ids=x).logits
|
| 395 |
+
loss = F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 396 |
+
y.reshape(-1))
|
| 397 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 398 |
+
if step == 50 or step % 1000 == 0:
|
| 399 |
+
mem = torch.cuda.max_memory_allocated() / 2**30 \
|
| 400 |
+
if device == "cuda" else 0.0
|
| 401 |
+
print(f"[q6 {arm} s{seed} step {step}] "
|
| 402 |
+
f"loss={loss.item():.3f} peak_mem={mem:.1f}GB",
|
| 403 |
+
flush=True)
|
| 404 |
+
rec = {"exp": "q6", "arm": arm, "seed": seed,
|
| 405 |
+
"steps": steps if arm != "frozen" else 0,
|
| 406 |
+
"ppl": ppl_per_reg(model, blob, device=device),
|
| 407 |
+
"validity": story_validity(model, blob, device=device),
|
| 408 |
+
"register_sep": register_probe6(model, adapters, blob,
|
| 409 |
+
device=device)
|
| 410 |
+
if adapters is not None else None,
|
| 411 |
+
"cache_stats": blob["stats"]}
|
| 412 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.close()
|
| 413 |
+
v = {r: rec["validity"][r]["valid"] for r in REGS}
|
| 414 |
+
print(f"[q6 {arm} s{seed}] FINAL ppl={rec['ppl']} valid={v} "
|
| 415 |
+
f"sep={rec['register_sep']}", flush=True)
|
| 416 |
+
if adapters is not None:
|
| 417 |
+
torch.save({"arm": arm, "seed": seed,
|
| 418 |
+
"adapters": {k: t.cpu() for k, t in
|
| 419 |
+
adapters.state_dict().items()}},
|
| 420 |
+
os.path.join(EXP6_DIR, f"q6_{arm}_s{seed}.pt"))
|
| 421 |
+
del model, adapters
|
| 422 |
+
torch.cuda.empty_cache()
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def run_exp006(device="cuda"):
|
| 426 |
+
run_arm6("frozen", seed=0, device=device)
|
| 427 |
+
run_arm6("relay_pw_story", seed=0, device=device)
|
| 428 |
+
print("=== qwen exp006 COMPLETE ===", flush=True)
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def smoke():
|
| 432 |
+
st = ("Once upon a time there was a little dog named Rex. Rex loved to "
|
| 433 |
+
"play in the park every day. One day Rex found a shiny red ball "
|
| 434 |
+
"under a big tree. He picked it up and ran to show his friend "
|
| 435 |
+
"Anna. Anna smiled and they played with the ball until the sun "
|
| 436 |
+
"went down. Rex was very happy that day.")
|
| 437 |
+
d = derive_registers(st)
|
| 438 |
+
assert d is not None
|
| 439 |
+
assert len(d["kw"]["kws"]) == 3 and len(set(d["kw"]["kws"])) == 3
|
| 440 |
+
assert d["cont"]["user"].startswith("Once upon")
|
| 441 |
+
assert d["json"]["target"]["title"].endswith("...")
|
| 442 |
+
ok, why = check_gen("kw", {"kws": d["kw"]["kws"]},
|
| 443 |
+
st + "<|im_end|>", True)
|
| 444 |
+
assert ok, why
|
| 445 |
+
ok, why = check_gen("kw", {"kws": ["zebra", "quantum", "xylophone"]},
|
| 446 |
+
st + "<|im_end|>", True)
|
| 447 |
+
assert not ok and why == "missing_keyword"
|
| 448 |
+
ok, why = check_gen("cont", {"opening": st}, st + "<|im_end|>", True)
|
| 449 |
+
assert not ok and why == "parrot"
|
| 450 |
+
ok, why = check_gen("json", {}, "<tool_call>\n" + json.dumps(
|
| 451 |
+
{"name": "emit_story", "arguments":
|
| 452 |
+
{"title": "T...", "story": st}}) + "\n</tool_call><|im_end|>", True)
|
| 453 |
+
assert ok, why
|
| 454 |
+
assert check_gen("inst", {}, "Hi.<|im_end|>", True)[1] == "too_short"
|
| 455 |
+
assert check_gen("inst", {}, st, False)[1] == "no_eos"
|
| 456 |
+
print("qwen exp006 smoke passed (register derivation + validators "
|
| 457 |
+
"both directions)")
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def _in_notebook():
|
| 461 |
+
try:
|
| 462 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 463 |
+
return True
|
| 464 |
+
except NameError:
|
| 465 |
+
return False
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
if __name__ == "__main__":
|
| 469 |
+
smoke() if not _in_notebook() else (smoke(),
|
| 470 |
+
print("Notebook: run_exp006() on GPU."))
|
exp007_math/qwen_exp007_math.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""qwen_exp007_math.py — geolip-aleph-qwen EXPERIMENT 7: math anchors —
|
| 2 |
+
one relay_pw stack, FOUR methods of asking for a computation (Phil's
|
| 3 |
+
overnight directive 2026-07-12: "math functions with multiple methods of
|
| 4 |
+
asking for functions"). The judge splits FORMAT from CORRECTNESS: does the
|
| 5 |
+
anchor paradigm carry COMPUTATION on held-out problems, or only the shape
|
| 6 |
+
of answers? Fully synthetic — no downloads; ground truth computable.
|
| 7 |
+
|
| 8 |
+
REGISTERS (each problem rendered one way, deterministically assigned):
|
| 9 |
+
nl — "What is 37 + 45?" -> "37 + 45 = 82"
|
| 10 |
+
json — emit_answer tool call -> {"expression": "37 + 45",
|
| 11 |
+
"result": 82}
|
| 12 |
+
code — "def f(x): return 3*x + 2 ... What is f(4)?" -> "f(4) = 14"
|
| 13 |
+
word — templated word problem -> "Sam has 82 apples."
|
| 14 |
+
|
| 15 |
+
PROBLEM SPACE: a+b (1-99), a-b (result>=0), a*b (2-12), f(x)=a*x+b with
|
| 16 |
+
a 2-9, b 0-20, x 2-9. 4,000 problems, problem-hash holdout 10% — held-out
|
| 17 |
+
CORRECTNESS is unseen-instance computation, not recall.
|
| 18 |
+
|
| 19 |
+
ARMS: frozen | relay_pw_math (all four registers interleaved). 3000 steps,
|
| 20 |
+
block 512, batch 4, pure Adam lr 1e-3 wd 0.
|
| 21 |
+
|
| 22 |
+
JUDGES: per-register held-out ppl; per-register FORMAT validity and
|
| 23 |
+
CORRECTNESS (n=16/register, both arms — the frozen baseline separates
|
| 24 |
+
"the trunk can already add" from what the anchor adds); the register probe
|
| 25 |
+
(sign-code separation across the 4 ask-methods). Riders: trunk frozen;
|
| 26 |
+
GPU-only verdicts; Colab-safe; step-50 peak_mem telemetry.
|
| 27 |
+
Paste order: geolip_vitals -> ar_differentiation_bed ->
|
| 28 |
+
exp013_augmentation_bed -> qwen_exp001_relay -> qwen_exp002_refine ->
|
| 29 |
+
qwen_exp003_instruct -> qwen_exp006_story -> this file.
|
| 30 |
+
"""
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
import hashlib
|
| 33 |
+
import json
|
| 34 |
+
import math
|
| 35 |
+
import os
|
| 36 |
+
import re
|
| 37 |
+
import torch
|
| 38 |
+
import torch.nn as nn
|
| 39 |
+
import torch.nn.functional as F
|
| 40 |
+
|
| 41 |
+
if "parse_tool_json" not in globals():
|
| 42 |
+
try:
|
| 43 |
+
from qwen_exp001_relay import _QwenBlockWithAdapter, _batch
|
| 44 |
+
from qwen_exp002_refine import RelayPatchwork
|
| 45 |
+
from qwen_exp003_instruct import parse_tool_json, INSTRUCT_MODEL
|
| 46 |
+
from qwen_exp006_story import load_arm6, register_probe6
|
| 47 |
+
except ImportError:
|
| 48 |
+
_here = globals().get("__file__")
|
| 49 |
+
if _here is None:
|
| 50 |
+
raise ImportError("paste the qwen stack first")
|
| 51 |
+
import sys, pathlib
|
| 52 |
+
sys.path.insert(0, str(pathlib.Path(_here).parent))
|
| 53 |
+
from qwen_exp001_relay import _QwenBlockWithAdapter, _batch
|
| 54 |
+
from qwen_exp002_refine import RelayPatchwork
|
| 55 |
+
from qwen_exp003_instruct import parse_tool_json, INSTRUCT_MODEL
|
| 56 |
+
from qwen_exp006_story import load_arm6, register_probe6
|
| 57 |
+
|
| 58 |
+
DATA_ROOT = os.environ.get("GEOLIP_DATA", "./data")
|
| 59 |
+
EXP7_DIR = os.path.join(DATA_ROOT, "qwen_exp007")
|
| 60 |
+
BLOCK, BATCH, STEPS, LR = 512, 4, 3000, 1e-3
|
| 61 |
+
MAX_NEW, N_EVAL = 96, 16
|
| 62 |
+
REGS = ("nl", "json", "code", "word")
|
| 63 |
+
|
| 64 |
+
TOOL = [{
|
| 65 |
+
"type": "function",
|
| 66 |
+
"function": {
|
| 67 |
+
"name": "emit_answer",
|
| 68 |
+
"description": "Return the result of a computation.",
|
| 69 |
+
"parameters": {
|
| 70 |
+
"type": "object",
|
| 71 |
+
"properties": {
|
| 72 |
+
"expression": {"type": "string"},
|
| 73 |
+
"result": {"type": "integer"},
|
| 74 |
+
},
|
| 75 |
+
"required": ["expression", "result"],
|
| 76 |
+
},
|
| 77 |
+
},
|
| 78 |
+
}]
|
| 79 |
+
|
| 80 |
+
SYS = {
|
| 81 |
+
"nl": "Answer the arithmetic question. Reply with the equation and its "
|
| 82 |
+
"result, like: 12 + 5 = 17",
|
| 83 |
+
"json": "Compute the expression and return it by calling the "
|
| 84 |
+
"emit_answer tool.",
|
| 85 |
+
"code": "The user defines a Python function and asks for its value. "
|
| 86 |
+
"Reply like: f(3) = 11",
|
| 87 |
+
"word": "Solve the word problem. Reply with one short sentence "
|
| 88 |
+
"containing the answer.",
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
_NAMES = ("Sam", "Anna", "Ben", "Mia", "Tom", "Lily", "Max", "Zoe")
|
| 92 |
+
_THINGS = ("apples", "pens", "books", "coins", "shells", "cards",
|
| 93 |
+
"marbles", "stickers")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _h(s: str) -> int:
|
| 97 |
+
return int(hashlib.md5(s.encode()).hexdigest(), 16)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def gen_problems(n=4000, seed=0):
|
| 101 |
+
"""Deterministic synthetic problems: (register, user, target, truth)."""
|
| 102 |
+
g = torch.Generator().manual_seed(seed)
|
| 103 |
+
|
| 104 |
+
def ri(lo, hi):
|
| 105 |
+
return int(torch.randint(lo, hi + 1, (1,), generator=g))
|
| 106 |
+
|
| 107 |
+
out, seen = [], set()
|
| 108 |
+
while len(out) < n:
|
| 109 |
+
kind = ri(0, 3)
|
| 110 |
+
if kind == 0: # a + b
|
| 111 |
+
a, b = ri(1, 99), ri(1, 99)
|
| 112 |
+
expr, truth = f"{a} + {b}", a + b
|
| 113 |
+
elif kind == 1: # a - b >= 0
|
| 114 |
+
a, b = ri(1, 99), ri(1, 99)
|
| 115 |
+
a, b = max(a, b), min(a, b)
|
| 116 |
+
expr, truth = f"{a} - {b}", a - b
|
| 117 |
+
elif kind == 2: # a * b
|
| 118 |
+
a, b = ri(2, 12), ri(2, 12)
|
| 119 |
+
expr, truth = f"{a} * {b}", a * b
|
| 120 |
+
else: # f(x)=a*x+b
|
| 121 |
+
a, b, x = ri(2, 9), ri(0, 20), ri(2, 9)
|
| 122 |
+
expr, truth = f"f{a}x{b}@{x}", a * x + b
|
| 123 |
+
reg = REGS[len(out) % 4] if kind != 3 else "code"
|
| 124 |
+
if kind == 3:
|
| 125 |
+
user = (f"def f(x):\n return {a} * x + {b}\n\n"
|
| 126 |
+
f"What is f({x})?")
|
| 127 |
+
target = f"f({x}) = {truth}"
|
| 128 |
+
elif reg == "nl":
|
| 129 |
+
user = f"What is {expr}?"
|
| 130 |
+
target = f"{expr} = {truth}"
|
| 131 |
+
elif reg == "json":
|
| 132 |
+
user = f"Compute {expr}."
|
| 133 |
+
target = {"expression": expr, "result": truth}
|
| 134 |
+
elif reg == "code": # non-f(x) problem in code reg
|
| 135 |
+
user = (f"def f(x):\n return x {expr.split(' ', 1)[1]}\n\n"
|
| 136 |
+
f"What is f({expr.split(' ')[0]})?")
|
| 137 |
+
target = f"f({expr.split(' ')[0]}) = {truth}"
|
| 138 |
+
else: # word
|
| 139 |
+
nm = _NAMES[_h(expr) % len(_NAMES)]
|
| 140 |
+
th = _THINGS[_h(expr + "t") % len(_THINGS)]
|
| 141 |
+
a_, op, b_ = expr.split(" ")
|
| 142 |
+
if op == "+":
|
| 143 |
+
user = (f"{nm} has {a_} {th}. {nm} gets {b_} more. "
|
| 144 |
+
f"How many {th} does {nm} have now?")
|
| 145 |
+
elif op == "-":
|
| 146 |
+
user = (f"{nm} has {a_} {th}. {nm} gives away {b_}. "
|
| 147 |
+
f"How many {th} does {nm} have left?")
|
| 148 |
+
else:
|
| 149 |
+
user = (f"Each box holds {b_} {th}. There are {a_} boxes. "
|
| 150 |
+
f"How many {th} in all?")
|
| 151 |
+
target = f"{nm} has {truth} {th}." if op != "*" else \
|
| 152 |
+
f"There are {truth} {th}."
|
| 153 |
+
key = (reg, user)
|
| 154 |
+
if key in seen:
|
| 155 |
+
continue
|
| 156 |
+
seen.add(key)
|
| 157 |
+
out.append({"reg": reg, "user": user, "target": target,
|
| 158 |
+
"truth": truth})
|
| 159 |
+
return out
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _render(tok, reg, user, target=None, gen_prompt=False):
|
| 163 |
+
msgs = [{"role": "system", "content": SYS[reg]},
|
| 164 |
+
{"role": "user", "content": user}]
|
| 165 |
+
tools = TOOL if reg == "json" else None
|
| 166 |
+
if not gen_prompt:
|
| 167 |
+
if reg == "json":
|
| 168 |
+
msgs.append({"role": "assistant", "tool_calls": [{
|
| 169 |
+
"type": "function",
|
| 170 |
+
"function": {"name": "emit_answer", "arguments": target}}]})
|
| 171 |
+
else:
|
| 172 |
+
msgs.append({"role": "assistant", "content": target})
|
| 173 |
+
try:
|
| 174 |
+
return tok.apply_chat_template(msgs, tools=tools, tokenize=False,
|
| 175 |
+
add_generation_prompt=gen_prompt)
|
| 176 |
+
except Exception:
|
| 177 |
+
msgs2 = json.loads(json.dumps(msgs))
|
| 178 |
+
for m in msgs2:
|
| 179 |
+
for tc in m.get("tool_calls", []) or []:
|
| 180 |
+
if isinstance(tc["function"]["arguments"], dict):
|
| 181 |
+
tc["function"]["arguments"] = json.dumps(
|
| 182 |
+
tc["function"]["arguments"])
|
| 183 |
+
return tok.apply_chat_template(msgs2, tools=tools, tokenize=False,
|
| 184 |
+
add_generation_prompt=gen_prompt)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def build_caches7():
|
| 188 |
+
os.makedirs(EXP7_DIR, exist_ok=True)
|
| 189 |
+
path = os.path.join(EXP7_DIR, "math_cache.pt")
|
| 190 |
+
if os.path.exists(path):
|
| 191 |
+
return torch.load(path, map_location="cpu", weights_only=False)
|
| 192 |
+
from transformers import AutoTokenizer
|
| 193 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 194 |
+
probs = gen_problems()
|
| 195 |
+
train_txt = {r: [] for r in REGS}
|
| 196 |
+
hold_txt = {r: [] for r in REGS}
|
| 197 |
+
eval_rows = {r: [] for r in REGS}
|
| 198 |
+
for p in probs:
|
| 199 |
+
r = p["reg"]
|
| 200 |
+
txt = _render(tok, r, p["user"], p["target"])
|
| 201 |
+
if _h(p["user"]) % 10 == 0:
|
| 202 |
+
hold_txt[r].append(txt)
|
| 203 |
+
if len(eval_rows[r]) < 48:
|
| 204 |
+
eval_rows[r].append({
|
| 205 |
+
"user": p["user"], "truth": p["truth"],
|
| 206 |
+
"gen_prompt": _render(tok, r, p["user"],
|
| 207 |
+
gen_prompt=True)})
|
| 208 |
+
else:
|
| 209 |
+
train_txt[r].append(txt)
|
| 210 |
+
blob = {
|
| 211 |
+
"train": {r: tok("\n".join(train_txt[r]),
|
| 212 |
+
return_tensors="pt").input_ids[0] for r in REGS},
|
| 213 |
+
"hold": {r: tok("\n".join(hold_txt[r]),
|
| 214 |
+
return_tensors="pt").input_ids[0] for r in REGS},
|
| 215 |
+
"eval_rows": eval_rows,
|
| 216 |
+
"stats": {r: len(train_txt[r]) for r in REGS},
|
| 217 |
+
}
|
| 218 |
+
torch.save(blob, path)
|
| 219 |
+
print(f"[cache q7] {blob['stats']}", flush=True)
|
| 220 |
+
return blob
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# ================================ judges ==========================================
|
| 224 |
+
_NUM_RE = re.compile(r"-?\d+")
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def check_math(reg: str, row: dict, txt: str, closed: bool):
|
| 228 |
+
"""Returns (format_ok, correct, reason)."""
|
| 229 |
+
if not closed:
|
| 230 |
+
return False, False, "no_eos"
|
| 231 |
+
body = txt.split("<|im_end|>")[0].strip()
|
| 232 |
+
if reg == "json":
|
| 233 |
+
args = parse_tool_json(body)
|
| 234 |
+
if args is None:
|
| 235 |
+
return False, False, "json_error"
|
| 236 |
+
if "expression" not in args or "result" not in args:
|
| 237 |
+
return False, False, "missing_keys"
|
| 238 |
+
try:
|
| 239 |
+
return True, int(args["result"]) == row["truth"], "ok"
|
| 240 |
+
except (TypeError, ValueError):
|
| 241 |
+
return True, False, "non_int_result"
|
| 242 |
+
nums = _NUM_RE.findall(body)
|
| 243 |
+
if not nums:
|
| 244 |
+
return False, False, "no_number"
|
| 245 |
+
fmt = ("=" in body) if reg in ("nl", "code") else \
|
| 246 |
+
(len(body.split()) <= 24)
|
| 247 |
+
return fmt, int(nums[-1]) == row["truth"], "ok"
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@torch.no_grad()
|
| 251 |
+
def math_validity(model, blob, device="cuda", n_eval=N_EVAL):
|
| 252 |
+
from transformers import AutoTokenizer
|
| 253 |
+
tok = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
|
| 254 |
+
model.eval()
|
| 255 |
+
model.config.use_cache = True
|
| 256 |
+
out = {}
|
| 257 |
+
for r in REGS:
|
| 258 |
+
fmts, cors, samples = 0, 0, []
|
| 259 |
+
rows = blob["eval_rows"][r][:n_eval]
|
| 260 |
+
for i, row in enumerate(rows):
|
| 261 |
+
ids = tok(row["gen_prompt"],
|
| 262 |
+
return_tensors="pt").input_ids.to(device)
|
| 263 |
+
gen = model.generate(ids, max_new_tokens=MAX_NEW, do_sample=False,
|
| 264 |
+
pad_token_id=tok.eos_token_id)
|
| 265 |
+
txt = tok.decode(gen[0][ids.shape[1]:], skip_special_tokens=False)
|
| 266 |
+
closed = len(gen[0]) - ids.shape[1] < MAX_NEW or \
|
| 267 |
+
"<|im_end|>" in txt
|
| 268 |
+
fmt, cor, _ = check_math(r, row, txt, closed)
|
| 269 |
+
fmts += int(fmt)
|
| 270 |
+
cors += int(cor)
|
| 271 |
+
if i < 2:
|
| 272 |
+
samples.append(txt[:160])
|
| 273 |
+
out[r] = {"format": round(fmts / len(rows), 3),
|
| 274 |
+
"correct": round(cors / len(rows), 3),
|
| 275 |
+
"samples": samples}
|
| 276 |
+
model.config.use_cache = False
|
| 277 |
+
return out
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
@torch.no_grad()
|
| 281 |
+
def ppl_per_reg7(model, blob, device="cuda", n=15):
|
| 282 |
+
g = torch.Generator().manual_seed(0)
|
| 283 |
+
model.eval()
|
| 284 |
+
out = {}
|
| 285 |
+
for r in REGS:
|
| 286 |
+
ls = []
|
| 287 |
+
for _ in range(n):
|
| 288 |
+
x, y = _batch(blob["hold"][r], BATCH, BLOCK, device, g)
|
| 289 |
+
logits = model(input_ids=x).logits
|
| 290 |
+
ls.append(F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 291 |
+
y.reshape(-1)).item())
|
| 292 |
+
out[r] = round(math.exp(sum(ls) / len(ls)), 3)
|
| 293 |
+
return out
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ================================ runner ==========================================
|
| 297 |
+
def run_arm7(arm: str, seed: int = 0, steps=STEPS, device="cuda"):
|
| 298 |
+
if not torch.cuda.is_available():
|
| 299 |
+
raise RuntimeError("verdict runs are GPU-only")
|
| 300 |
+
os.makedirs(EXP7_DIR, exist_ok=True)
|
| 301 |
+
blob = build_caches7()
|
| 302 |
+
model, adapters = load_arm6(arm, seed=seed, device=device)
|
| 303 |
+
ledger = open(os.path.join(EXP7_DIR, "ledger.jsonl"), "a",
|
| 304 |
+
encoding="utf-8")
|
| 305 |
+
if arm != "frozen":
|
| 306 |
+
g = torch.Generator().manual_seed(seed)
|
| 307 |
+
opt = torch.optim.Adam(adapters.parameters(), lr=LR, weight_decay=0.0)
|
| 308 |
+
model.train()
|
| 309 |
+
for step in range(1, steps + 1):
|
| 310 |
+
r = REGS[int(torch.randint(len(REGS), (1,), generator=g))]
|
| 311 |
+
x, y = _batch(blob["train"][r], BATCH, BLOCK, device, g)
|
| 312 |
+
logits = model(input_ids=x).logits
|
| 313 |
+
loss = F.cross_entropy(logits.reshape(-1, logits.shape[-1]),
|
| 314 |
+
y.reshape(-1))
|
| 315 |
+
opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
|
| 316 |
+
if step == 50 or step % 1000 == 0:
|
| 317 |
+
mem = torch.cuda.max_memory_allocated() / 2**30 \
|
| 318 |
+
if device == "cuda" else 0.0
|
| 319 |
+
print(f"[q7 {arm} s{seed} step {step}] "
|
| 320 |
+
f"loss={loss.item():.3f} peak_mem={mem:.1f}GB",
|
| 321 |
+
flush=True)
|
| 322 |
+
# the probe reuses exp006's gauge (identical adapter anatomy)
|
| 323 |
+
import qwen_exp006_story as q6mod
|
| 324 |
+
q6_regs_save = q6mod.REGS
|
| 325 |
+
q6mod.REGS = REGS
|
| 326 |
+
try:
|
| 327 |
+
sep = register_probe6(model, adapters, blob, device=device) \
|
| 328 |
+
if adapters is not None else None
|
| 329 |
+
finally:
|
| 330 |
+
q6mod.REGS = q6_regs_save
|
| 331 |
+
rec = {"exp": "q7", "arm": arm, "seed": seed,
|
| 332 |
+
"steps": steps if arm != "frozen" else 0,
|
| 333 |
+
"ppl": ppl_per_reg7(model, blob, device=device),
|
| 334 |
+
"validity": math_validity(model, blob, device=device),
|
| 335 |
+
"register_sep": sep,
|
| 336 |
+
"cache_stats": blob["stats"]}
|
| 337 |
+
ledger.write(json.dumps(rec) + "\n"); ledger.close()
|
| 338 |
+
v = {r: (rec["validity"][r]["format"], rec["validity"][r]["correct"])
|
| 339 |
+
for r in REGS}
|
| 340 |
+
print(f"[q7 {arm} s{seed}] FINAL ppl={rec['ppl']} fmt/cor={v} "
|
| 341 |
+
f"sep={rec['register_sep']}", flush=True)
|
| 342 |
+
if adapters is not None:
|
| 343 |
+
torch.save({"arm": arm, "seed": seed,
|
| 344 |
+
"adapters": {k: t.cpu() for k, t in
|
| 345 |
+
adapters.state_dict().items()}},
|
| 346 |
+
os.path.join(EXP7_DIR, f"q7_{arm}_s{seed}.pt"))
|
| 347 |
+
del model, adapters
|
| 348 |
+
torch.cuda.empty_cache()
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def run_exp007(device="cuda"):
|
| 352 |
+
run_arm7("frozen", seed=0, device=device)
|
| 353 |
+
run_arm7("relay_pw_math", seed=0, device=device)
|
| 354 |
+
print("=== qwen exp007 COMPLETE ===", flush=True)
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def smoke():
|
| 358 |
+
probs = gen_problems(n=400)
|
| 359 |
+
assert len(probs) == 400
|
| 360 |
+
regs = {p["reg"] for p in probs}
|
| 361 |
+
assert regs == set(REGS), regs
|
| 362 |
+
for p in probs[:50]:
|
| 363 |
+
if p["reg"] == "json":
|
| 364 |
+
assert p["target"]["result"] == p["truth"]
|
| 365 |
+
elif p["reg"] in ("nl", "code"):
|
| 366 |
+
assert str(p["truth"]) in p["target"]
|
| 367 |
+
f, c, _ = check_math("nl", {"truth": 82}, "37 + 45 = 82<|im_end|>", True)
|
| 368 |
+
assert f and c
|
| 369 |
+
f, c, _ = check_math("nl", {"truth": 82}, "37 + 45 = 83<|im_end|>", True)
|
| 370 |
+
assert f and not c
|
| 371 |
+
f, c, _ = check_math("json", {"truth": 14}, "<tool_call>\n" + json.dumps(
|
| 372 |
+
{"name": "emit_answer",
|
| 373 |
+
"arguments": {"expression": "3*4+2", "result": 14}}) +
|
| 374 |
+
"\n</tool_call><|im_end|>", True)
|
| 375 |
+
assert f and c
|
| 376 |
+
f, c, _ = check_math("word", {"truth": 42},
|
| 377 |
+
"There are 42 pens.<|im_end|>", True)
|
| 378 |
+
assert f and c
|
| 379 |
+
f, c, why = check_math("nl", {"truth": 5}, "37 + 45 = 82", False)
|
| 380 |
+
assert not f and why == "no_eos"
|
| 381 |
+
print("qwen exp007 smoke passed (problem gen + format/correctness "
|
| 382 |
+
"judges both directions)")
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def _in_notebook():
|
| 386 |
+
try:
|
| 387 |
+
get_ipython() # type: ignore[name-defined] # noqa: F821
|
| 388 |
+
return True
|
| 389 |
+
except NameError:
|
| 390 |
+
return False
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
if __name__ == "__main__":
|
| 394 |
+
smoke() if not _in_notebook() else (smoke(),
|
| 395 |
+
print("Notebook: run_exp007() on GPU."))
|
exp007_math/repro.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""repro.py — standalone loader/runner for exp007. Code dependencies live in
|
| 2 |
+
THIS folder. python repro.py = CPU smoke; python repro.py --run = full
|
| 3 |
+
campaign (frozen baseline + trained arm; GPU ~1.2h). Data + caches land in
|
| 4 |
+
./data (override with GEOLIP_DATA)."""
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
sys.path.insert(0, HERE)
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
import geolip_vitals # noqa: F401 (paste order)
|
| 13 |
+
import ar_differentiation_bed # noqa: F401
|
| 14 |
+
import exp013_augmentation_bed # noqa: F401
|
| 15 |
+
import qwen_exp001_relay # noqa: F401
|
| 16 |
+
import qwen_exp002_refine # noqa: F401
|
| 17 |
+
import qwen_exp003_instruct # noqa: F401
|
| 18 |
+
import qwen_exp007_math as m
|
| 19 |
+
if "--run" in sys.argv[1:]:
|
| 20 |
+
m.run_exp007()
|
| 21 |
+
else:
|
| 22 |
+
m.smoke()
|
| 23 |
+
print("repro smoke passed — --run for the campaign (GPU)")
|
exp007_math/results/ledger.jsonl
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"exp": "q7", "arm": "frozen", "seed": 0, "steps": 0, "ppl": {"nl": 2.763, "json": 4.393, "code": 2.591, "word": 3.145}, "validity": {"nl": {"format": 0.312, "correct": 1.0, "samples": ["45<|im_end|>", "The equation for 94 - 27 is 67.<|im_end|>"]}, "json": {"format": 1.0, "correct": 1.0, "samples": ["<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"2 * 12\", \"result\": 24}}\n</tool_call><|im_end|>", "<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"84 + 73\", \"result\": 157}}\n</tool_call><|im_end|>"]}, "code": {"format": 1.0, "correct": 0.688, "samples": ["f(77) = 60<|im_end|>", "f(5) = 15<|im_end|>"]}, "word": {"format": 1.0, "correct": 0.938, "samples": ["Max now has 20 marbles.<|im_end|>", "Zoe has 41 shells left.<|im_end|>"]}}, "register_sep": null, "cache_stats": {"nl": 696, "json": 702, "code": 1496, "word": 718}}
|
| 2 |
+
{"exp": "q7", "arm": "relay_pw_math", "seed": 0, "steps": 3000, "ppl": {"nl": 1.342, "json": 1.061, "code": 1.168, "word": 1.525}, "validity": {"nl": {"format": 1.0, "correct": 1.0, "samples": ["5 * 9 = 45<|im_end|>", "94 - 27 = 67<|im_end|>"]}, "json": {"format": 1.0, "correct": 1.0, "samples": ["<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"2 * 12\", \"result\": 24}}\n</tool_call><|im_end|>", "<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"84 + 73\", \"result\": 157}}\n</tool_call><|im_end|>"]}, "code": {"format": 1.0, "correct": 0.938, "samples": ["f(77) = 6<|im_end|>", "f(5) = 15<|im_end|>"]}, "word": {"format": 1.0, "correct": 1.0, "samples": ["Max has 72 marbles.<|im_end|>", "Zoe has 41 shells.<|im_end|>"]}}, "register_sep": {"L0": {"inter": 0.2563, "intra": 0.0418, "sep": 0.2145}, "L8": {"inter": 0.3293, "intra": 0.0485, "sep": 0.2808}, "L16": {"inter": 0.8737, "intra": 0.4026, "sep": 0.4711}, "L23": {"inter": 0.9304, "intra": 0.4347, "sep": 0.4957}}, "cache_stats": {"nl": 696, "json": 702, "code": 1496, "word": 718}}
|
exp007_math/results/results.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"frozen": {
|
| 3 |
+
"ppl": {
|
| 4 |
+
"nl": 2.763,
|
| 5 |
+
"json": 4.393,
|
| 6 |
+
"code": 2.591,
|
| 7 |
+
"word": 3.145
|
| 8 |
+
},
|
| 9 |
+
"validity": {
|
| 10 |
+
"nl": {
|
| 11 |
+
"format": 0.312,
|
| 12 |
+
"correct": 1.0,
|
| 13 |
+
"samples": [
|
| 14 |
+
"45<|im_end|>",
|
| 15 |
+
"The equation for 94 - 27 is 67.<|im_end|>"
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"json": {
|
| 19 |
+
"format": 1.0,
|
| 20 |
+
"correct": 1.0,
|
| 21 |
+
"samples": [
|
| 22 |
+
"<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"2 * 12\", \"result\": 24}}\n</tool_call><|im_end|>",
|
| 23 |
+
"<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"84 + 73\", \"result\": 157}}\n</tool_call><|im_end|>"
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
"code": {
|
| 27 |
+
"format": 1.0,
|
| 28 |
+
"correct": 0.688,
|
| 29 |
+
"samples": [
|
| 30 |
+
"f(77) = 60<|im_end|>",
|
| 31 |
+
"f(5) = 15<|im_end|>"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"word": {
|
| 35 |
+
"format": 1.0,
|
| 36 |
+
"correct": 0.938,
|
| 37 |
+
"samples": [
|
| 38 |
+
"Max now has 20 marbles.<|im_end|>",
|
| 39 |
+
"Zoe has 41 shells left.<|im_end|>"
|
| 40 |
+
]
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"relay_pw_math": {
|
| 45 |
+
"steps": 3000,
|
| 46 |
+
"ppl": {
|
| 47 |
+
"nl": 1.342,
|
| 48 |
+
"json": 1.061,
|
| 49 |
+
"code": 1.168,
|
| 50 |
+
"word": 1.525
|
| 51 |
+
},
|
| 52 |
+
"validity": {
|
| 53 |
+
"nl": {
|
| 54 |
+
"format": 1.0,
|
| 55 |
+
"correct": 1.0,
|
| 56 |
+
"samples": [
|
| 57 |
+
"5 * 9 = 45<|im_end|>",
|
| 58 |
+
"94 - 27 = 67<|im_end|>"
|
| 59 |
+
]
|
| 60 |
+
},
|
| 61 |
+
"json": {
|
| 62 |
+
"format": 1.0,
|
| 63 |
+
"correct": 1.0,
|
| 64 |
+
"samples": [
|
| 65 |
+
"<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"2 * 12\", \"result\": 24}}\n</tool_call><|im_end|>",
|
| 66 |
+
"<tool_call>\n{\"name\": \"emit_answer\", \"arguments\": {\"expression\": \"84 + 73\", \"result\": 157}}\n</tool_call><|im_end|>"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
"code": {
|
| 70 |
+
"format": 1.0,
|
| 71 |
+
"correct": 0.938,
|
| 72 |
+
"samples": [
|
| 73 |
+
"f(77) = 6<|im_end|>",
|
| 74 |
+
"f(5) = 15<|im_end|>"
|
| 75 |
+
]
|
| 76 |
+
},
|
| 77 |
+
"word": {
|
| 78 |
+
"format": 1.0,
|
| 79 |
+
"correct": 1.0,
|
| 80 |
+
"samples": [
|
| 81 |
+
"Max has 72 marbles.<|im_end|>",
|
| 82 |
+
"Zoe has 41 shells.<|im_end|>"
|
| 83 |
+
]
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"register_sep": {
|
| 87 |
+
"L0": {
|
| 88 |
+
"inter": 0.2563,
|
| 89 |
+
"intra": 0.0418,
|
| 90 |
+
"sep": 0.2145
|
| 91 |
+
},
|
| 92 |
+
"L8": {
|
| 93 |
+
"inter": 0.3293,
|
| 94 |
+
"intra": 0.0485,
|
| 95 |
+
"sep": 0.2808
|
| 96 |
+
},
|
| 97 |
+
"L16": {
|
| 98 |
+
"inter": 0.8737,
|
| 99 |
+
"intra": 0.4026,
|
| 100 |
+
"sep": 0.4711
|
| 101 |
+
},
|
| 102 |
+
"L23": {
|
| 103 |
+
"inter": 0.9304,
|
| 104 |
+
"intra": 0.4347,
|
| 105 |
+
"sep": 0.4957
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"cache_stats": {
|
| 110 |
+
"nl": 696,
|
| 111 |
+
"json": 702,
|
| 112 |
+
"code": 1496,
|
| 113 |
+
"word": 718
|
| 114 |
+
},
|
| 115 |
+
"n_rows": 2
|
| 116 |
+
}
|