Skip to content

Commit 9c4c158

Browse files
Phase 4: gate reformulation falsified across both follow-on archs
Two architectures proposed in distractor_mix_README.md as paths to make the HBit-tension attention gate earn its keep — both tested, both falsified at distractor_frac=0.20: arch mean std wins vs crt_only crt_only 2.4595 0.0257 — hybrid_score 2.5488 0.0239 0/3 (+3.6%) hybrid_learned 2.5607 0.0179 0/3 (+4.1%) Combined with the original falsified key-gate (0/3, +3.2%), this is THREE different gate formulations all losing to crt_only by the same ~3-4% magnitude. Interpretation (full writeup in GATE_REFORMULATION_RESULTS.md): substrate-distance signal on (q@k^T / sqrt(d)) just doesn't correlate with what the attention should focus on. The Fibonacci attractor structure of OMC's HInt doesn't transfer to attention score tensors which have totally different distributional properties (Gaussian-ish, scaled, drawn from learned projections). The learned-threshold variant is the stronger negative: the model could simply learn gate_w≈0 to disable the gate. It did not. It converged to a setting that costs ~4% on val loss. Architectural verdict consolidated: - Substrate as positional encoding (CRT-PE): WINS (-5.4%, -2.9%) - Substrate as OOD signal (HBit cross-cutting): WINS (AUROC 1.0) - Substrate as attention modulator: FALSIFIED 3× (key, score, learned) Recommended pivot: stop investing in attention-gate variants. The next-most-promising substrate role is at the tokenizer / embedding layer (using OMC's substrate-routed tokenizer as the LM's input tokenizer), which is architecturally unmeasured and has stronger theoretical grounding than another attention-layer attempt. Implementation: - models.py: Attention now takes gate_mode ∈ {none, key, score, learned} - hybrid_score: log_gate added to scores pre-mask, pre-softmax - hybrid_learned: per-block scalar W, b learnable; init W=-1, b=0 so initial gate ≈ original 1/(1+d), then trains from loss signal - train_gate_reformulation.py: focused 3-arch comparison vs crt_only Per-seed wall-clock ~10 min, total ~30 min on CPU. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5e054b2 commit 9c4c158

5 files changed

Lines changed: 455 additions & 38 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ Submit a package: PR an entry to [`registry/index.json`](registry/index.json).
350350
| Harmonic libraries on real data | shipped, mixed-honest results |
351351
| Hybrid LLM experiments (12 experiments) | shipped, 1 perfect AUROC, 1 architectural negative, 1 CRT-PE win |
352352
| End-to-end transformerless LM (PyTorch) | CRT-PE wins -19.9% (tiny), **-5.4% (TinyShakespeare, 3/3 seeds), -2.9% (distractor mix, 3/3)** |
353-
| Hybrid HBit-gate distractor-mix test | **falsified at current gate formulation (0/3 wins)**, score-level / learned-threshold reformulations documented |
353+
| Hybrid HBit-gate distractor-mix test | **falsified across THREE gate formulations** (0/3 wins each, +3–4% consistent loss): KEY-magnitude gate, SCORE-level gate, LEARNED-threshold gate. The architectural pivot per [`GATE_REFORMULATION_RESULTS.md`](experiments/transformerless_lm/GATE_REFORMULATION_RESULTS.md): substrate's home is positional + distributional, not as an attention-score shaper. |
354354
| Self-hosting compiler V.9b | shipped, gen2 == gen3 byte-identical |
355355
| **Self-healing pass (7 classes, substrate-routed typo)** | shipped, `OMC_HEAL=1`, **10× typo lookup**, 16 tests, per-class pragmas |
356356
| **Substrate-keyed code codec + compressed messaging** | **shipped**, `omc_codec_encode/decode_lookup` + `omc_msg_sign_compressed/recover`, alpha-rename invariant, token-count ~N× (wire-byte breaks even at ≥500 B + N≥8); always-on win is library-lookup recovery; 13 tests, lossless on in-library content |
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Gate reformulation — both follow-on architectures falsified
2+
3+
## Context
4+
5+
`distractor_mix_README.md` reported the original `hybrid` arch
6+
(CRT-PE + KEY-magnitude HBit gate) losing 0/3 to `crt_only` at
7+
distractor_frac=0.20. The writeup proposed two concrete
8+
reformulations that kept CRT-PE and changed only the gate:
9+
10+
1. **`hybrid_score`** — gate on raw attention SCORES additively in
11+
log-space pre-softmax, instead of post-softmax renormalization
12+
on key magnitudes.
13+
2. **`hybrid_learned`** — replace fixed `1/(1+d)` with
14+
`sigmoid(W*d + b)` where W, b are learned per-head. Lets the
15+
model discover its own threshold and slope.
16+
17+
Both kept CRT-PE intact. The hypothesis was that the original gate's
18+
formulation was too rigid, and a softer or learnable variant might
19+
earn its keep.
20+
21+
## Setup
22+
23+
Identical to `train_distractor_mix.py`:
24+
- TinyShakespeare, 90/10 split
25+
- 20% of training chunks char-shuffled (within-vocab distractors)
26+
- Validation on PURE shakespeare (the actual task we care about)
27+
- d_model=128, n_blocks=4, seq_len=128, ~801K params (+8 for learned gate)
28+
- 1500 steps, batch=32, AdamW lr=3e-4
29+
- 3 seeds: 42, 7, 123
30+
- CPU, ~30 min total wall-clock for 3 archs × 3 seeds
31+
32+
## Results
33+
34+
| arch | mean | std | wins vs crt_only | rel |
35+
|---|--:|--:|:-:|--:|
36+
| `crt_only` | **2.4595** | 0.0257 |||
37+
| `hybrid_score` | 2.5488 | 0.0239 | **0/3** | **+3.6%** |
38+
| `hybrid_learned` | 2.5607 | 0.0179 | **0/3** | **+4.1%** |
39+
40+
### Per-seed
41+
42+
| seed | crt_only | hybrid_score | hybrid_learned |
43+
|---|--:|--:|--:|
44+
| 42 | 2.489 | 2.562 | 2.567 |
45+
| 7 | 2.443 | 2.521 | 2.540 |
46+
| 123| 2.446 | 2.564 | 2.574 |
47+
48+
### Combined with the original
49+
50+
| arch | mean | wins vs crt_only |
51+
|---|--:|:-:|
52+
| `crt_only` | 2.4595 ||
53+
| `hybrid` (key-gate, original) | 2.5379 | 0/3 |
54+
| `hybrid_score` (score-gate) | 2.5488 | 0/3 |
55+
| `hybrid_learned` (learned-thresh) | 2.5607 | 0/3 |
56+
57+
**Three different gate formulations, three falsifications. Same
58+
+3-4% loss magnitude across all three.**
59+
60+
## Interpretation
61+
62+
The architectural read consolidates: **HBit tension is not a useful
63+
attention modulator at this scale and data regime**, regardless of
64+
where in the attention path the gate fires or whether its threshold
65+
is learnable.
66+
67+
Why this is a stronger negative than the original single failure:
68+
- We tested two DIFFERENT failure modes the README proposed
69+
- The learned-threshold variant had every chance to recover —
70+
the model could simply learn `gate_w ≈ 0, gate_b ≈ large` to
71+
disable the gate entirely. It did not converge there; it
72+
converged to a gate setting that costs ~4% on val loss.
73+
- The score-level variant operates at the correct layer of the
74+
computation (logits, not key-magnitudes), removing the
75+
"wrong-layer-of-abstraction" objection.
76+
77+
This suggests the failure isn't a formulation bug — the underlying
78+
substrate-distance signal on `q@k^T / sqrt(d)` values just doesn't
79+
correlate with what the model needs to focus on. The Fibonacci
80+
attractor structure of OMC's `HInt` doesn't transfer to attention
81+
score tensors which have totally different distributional
82+
properties (Gaussian-ish, scaled by `1/sqrt(d_head)`, drawn from
83+
learned projections of token embeddings).
84+
85+
## What this means for the transformerless LM
86+
87+
The substrate's role in a transformer replacement is now empirically:
88+
89+
| Component | Substrate variant | Status |
90+
|---|---|:-:|
91+
| Positional encoding | CRT-Fibonacci PE | **Wins** (−5.4% clean, −2.9% distractor mix; 3/3 + 4/5 seeds) |
92+
| OOD detection | HBit cross-cutting tension | **Wins** (AUROC 1.0 on scenario A) |
93+
| Attention modulation (key-mag gate) | `1/(1+d)` on `\|k\|.mean` | **Falsified** (0/3) |
94+
| Attention modulation (score-level gate) | `1/(1+d)` on logits pre-softmax | **Falsified** (0/3, this writeup) |
95+
| Attention modulation (learned threshold) | `sigmoid(W*d+b)` on `\|k\|.mean` | **Falsified** (0/3, this writeup) |
96+
97+
**The substrate's home in this architecture is positional and
98+
distributional, not as an attention-score shaper.** Three independent
99+
attempts to make it work there have all failed by similar margins.
100+
101+
## What's left to try (the new menu)
102+
103+
Since attention-gate variants are exhausted at this scale, the
104+
remaining places to introduce substrate signal are all
105+
out-of-attention:
106+
107+
### A. FFN substrate gate (vs attention gate)
108+
The FFN block doesn't have softmax — substrate signal there is
109+
unmediated. Apply `attractor_distance` to the post-GELU
110+
activations or to one of the linear projections. The FFN
111+
operates on per-position vectors with no cross-position coupling,
112+
so the substrate distance is computed in the same per-position
113+
basis OMC's HInt was designed for.
114+
115+
### B. Auxiliary substrate loss (regularizer, not forward signal)
116+
Add `lambda * attractor_distance(activations).mean()` as an
117+
auxiliary loss term. Gradients pull the network toward
118+
substrate-aligned representations without affecting the forward
119+
pass. Closest analog: weight decay, but in attractor-distance
120+
space instead of L2.
121+
122+
### C. Substrate-curriculum sampling (training order, not architecture)
123+
Sort training batches by attractor-distance of their token IDs:
124+
on-attractor samples first, off-attractor later. The substrate
125+
becomes a curriculum signal, not an architecture change. Cheap
126+
to test (no model change needed).
127+
128+
### D. Per-head selective gating
129+
Some heads get the substrate gate, some don't. Train one of the
130+
existing falsified variants but applied to only 1-of-4 heads.
131+
This is the weakest "maybe" — if the gate fails on all heads it
132+
will likely fail on one too — but worth ruling out cleanly.
133+
134+
### E. Honest pivot
135+
Accept that HBit-as-attention-gate is dead at the scales we can
136+
test. Ship the transformerless prototype with CRT-PE + standard
137+
softmax attention + substrate-aware tokenization (which is the
138+
biggest unexplored axis — the substrate at the EMBEDDING layer,
139+
not the attention layer). This is the path that respects what
140+
we've actually measured.
141+
142+
## Recommendation
143+
144+
E first, then A in parallel. Stop investing in attention-gate
145+
formulations — three failures with consistent magnitude is a
146+
saturation signal. The pivot toward substrate-aware tokenization
147+
hasn't been measured yet and has a stronger architectural basis
148+
(OMC's tokenizer is already substrate-routed; using it as the LM's
149+
input tokenizer is a small change with potentially large effect).
150+
151+
Numbers taken on 2026-05-16. Same hardware as the original
152+
distractor-mix experiment. Per-seed wall-clock ~10 min for 3 archs.

experiments/transformerless_lm/models.py

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -109,47 +109,73 @@ def hbit_tension_gate(keys: torch.Tensor, scale: float = 1.0) -> torch.Tensor:
109109

110110

111111
class Attention(nn.Module):
112-
"""Single-head attention. Optionally wraps softmax with an HBit
113-
tension gate computed from the (post-projection) key magnitudes.
114-
115-
The gate is computed on per-key SCALAR summary (mean over d_head).
116-
This is the architectural mapping from our 1-D HBit-tension
117-
experiment to a real attention layer. More sophisticated gates
118-
(per-channel, learned threshold) are possible but the simplest
119-
match to experiment 12 is per-key.
112+
"""Single-head attention. The gate_mode parameter selects how (if
113+
at all) the HBit-tension signal modulates attention:
114+
115+
"none" : pure softmax (standard / crt_only).
116+
"key" : fixed gate on per-key magnitude (the falsified
117+
distractor-mix formulation; kept for reference).
118+
"score" : ADDITIVE log-gate on the score tensor pre-softmax.
119+
Substrate-distance of the raw score values dampens
120+
off-attractor logits before softmax normalizes. No
121+
post-hoc renormalization needed.
122+
"learned" : per-head learnable scalar (W, b) gate on per-key
123+
magnitude. Initialized to approximate the fixed
124+
1/(1+d) formula; trains to discover whether
125+
substrate distance is a useful signal for the task.
120126
"""
121127

122-
def __init__(self, d_model: int, use_hbit_gate: bool, dropout: float = 0.0):
128+
def __init__(self, d_model: int, gate_mode: str = "none", dropout: float = 0.0):
123129
super().__init__()
130+
if gate_mode not in ("none", "key", "score", "learned"):
131+
raise ValueError(f"unknown gate_mode: {gate_mode}")
124132
self.d_model = d_model
125133
self.qkv = nn.Linear(d_model, 3 * d_model)
126134
self.out = nn.Linear(d_model, d_model)
127-
self.use_hbit_gate = use_hbit_gate
135+
self.gate_mode = gate_mode
128136
self.dropout = dropout
137+
if gate_mode == "learned":
138+
# Initialize so sigmoid(W*d + b) ≈ 1/(1 + d) near d ≈ 0:
139+
# picking W = -1, b = 0 gives sigmoid(-d) ∈ (0, 0.5], a
140+
# softer version of the falsified gate. Both are learnable.
141+
self.gate_w = nn.Parameter(torch.tensor(-1.0))
142+
self.gate_b = nn.Parameter(torch.tensor(0.0))
129143

130144
def forward(self, x: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
131-
# x: [B, T, D]
132145
B, T, D = x.shape
133146
qkv = self.qkv(x)
134-
q, k, v = qkv.chunk(3, dim=-1) # each [B, T, D]
147+
q, k, v = qkv.chunk(3, dim=-1)
135148
scale = 1.0 / math.sqrt(D)
136149
scores = (q @ k.transpose(-2, -1)) * scale # [B, T, T]
150+
151+
if self.gate_mode == "score":
152+
# Gate on score VALUES (pre-mask). attractor_distance of
153+
# raw scores tells us whether the (q·k) magnitude lands
154+
# on a substrate attractor. Off-attractor scores get
155+
# additively penalized in log-space, so softmax handles
156+
# normalization natively.
157+
d = attractor_distance(scores * 10.0) # [B, T, T]
158+
log_gate = -torch.log1p(d)
159+
scores = scores + log_gate
160+
137161
scores = scores.masked_fill(mask == 0, float('-inf'))
138162
attn = F.softmax(scores, dim=-1) # [B, T, T]
139163

140-
if self.use_hbit_gate:
141-
# gate per key: 1 scalar per key position (mean of |k|).
142-
# Shape [B, T]. Apply along the key axis.
143-
key_mag = k.abs().mean(dim=-1) # [B, T]
144-
gate = hbit_tension_gate(key_mag * 100.0) # scale up so attractor_distance is meaningful
145-
# broadcast gate over the query axis: [B, 1, T]
164+
if self.gate_mode == "key":
165+
key_mag = k.abs().mean(dim=-1)
166+
gate = hbit_tension_gate(key_mag * 100.0)
167+
attn = attn * gate.unsqueeze(1)
168+
attn = attn / (attn.sum(dim=-1, keepdim=True) + 1e-9)
169+
elif self.gate_mode == "learned":
170+
key_mag = k.abs().mean(dim=-1)
171+
d = attractor_distance(key_mag * 100.0) # [B, T]
172+
gate = torch.sigmoid(self.gate_w * d + self.gate_b)
146173
attn = attn * gate.unsqueeze(1)
147-
# renormalize so attn rows still sum to ~1
148174
attn = attn / (attn.sum(dim=-1, keepdim=True) + 1e-9)
149175

150176
if self.dropout > 0 and self.training:
151177
attn = F.dropout(attn, p=self.dropout)
152-
out = attn @ v # [B, T, D]
178+
out = attn @ v
153179
return self.out(out)
154180

155181

@@ -172,9 +198,9 @@ def forward(self, x):
172198

173199

174200
class Block(nn.Module):
175-
def __init__(self, d_model: int, use_hbit_gate: bool):
201+
def __init__(self, d_model: int, gate_mode: str = "none"):
176202
super().__init__()
177-
self.attn = Attention(d_model, use_hbit_gate=use_hbit_gate)
203+
self.attn = Attention(d_model, gate_mode=gate_mode)
178204
self.ff = FeedForward(d_model)
179205
self.ln1 = nn.LayerNorm(d_model)
180206
self.ln2 = nn.LayerNorm(d_model)
@@ -186,8 +212,7 @@ def forward(self, x, mask):
186212

187213

188214
class TinyLM(nn.Module):
189-
"""Tiny char-level LM. Three architectures behind one class via
190-
constructor flags."""
215+
"""Tiny char-level LM. Architecture selected via constructor flags."""
191216

192217
def __init__(
193218
self,
@@ -196,7 +221,7 @@ def __init__(
196221
n_blocks: int,
197222
seq_len: int,
198223
pe_kind: str, # "sinusoidal" or "crt"
199-
use_hbit_gate: bool,
224+
gate_mode: str, # "none" | "key" | "score" | "learned"
200225
):
201226
super().__init__()
202227
self.seq_len = seq_len
@@ -207,27 +232,24 @@ def __init__(
207232
pe = crt_pe(seq_len, d_model)
208233
else:
209234
raise ValueError(f"unknown pe_kind: {pe_kind}")
210-
self.register_buffer("pe", pe) # [seq_len, d_model]
235+
self.register_buffer("pe", pe)
211236
self.blocks = nn.ModuleList([
212-
Block(d_model, use_hbit_gate=use_hbit_gate) for _ in range(n_blocks)
237+
Block(d_model, gate_mode=gate_mode) for _ in range(n_blocks)
213238
])
214239
self.ln_f = nn.LayerNorm(d_model)
215240
self.head = nn.Linear(d_model, vocab_size, bias=False)
216-
# tie head weights to embedding
217241
self.head.weight = self.embed.weight
218-
# causal mask
219242
mask = torch.tril(torch.ones(seq_len, seq_len))
220243
self.register_buffer("mask", mask)
221244

222245
def forward(self, x):
223-
# x: [B, T]
224246
B, T = x.shape
225247
h = self.embed(x) + self.pe[:T]
226248
mask = self.mask[:T, :T]
227249
for block in self.blocks:
228250
h = block(h, mask)
229251
h = self.ln_f(h)
230-
return self.head(h) # [B, T, vocab]
252+
return self.head(h)
231253

232254

233255
def make_model(
@@ -237,19 +259,27 @@ def make_model(
237259
d_model: int = 64,
238260
n_blocks: int = 2,
239261
) -> TinyLM:
240-
"""Convenience: build one of the three benchmarked architectures.
241-
Defaults match the original tiny-bench (d_model=64, n_blocks=2).
242-
The scale experiment uses d_model=128, n_blocks=4."""
262+
"""Five architectures:
263+
standard : sinusoidal PE + pure softmax
264+
crt_only : CRT-Fib PE + pure softmax
265+
hybrid : CRT-Fib PE + KEY-magnitude gate (falsified)
266+
hybrid_score : CRT-Fib PE + SCORE-level pre-softmax gate
267+
hybrid_learned : CRT-Fib PE + LEARNED per-head gate on key magnitude
268+
"""
243269
common = dict(
244270
vocab_size=vocab_size,
245271
d_model=d_model,
246272
n_blocks=n_blocks,
247273
seq_len=seq_len,
248274
)
249275
if arch == "standard":
250-
return TinyLM(**common, pe_kind="sinusoidal", use_hbit_gate=False)
276+
return TinyLM(**common, pe_kind="sinusoidal", gate_mode="none")
251277
if arch == "crt_only":
252-
return TinyLM(**common, pe_kind="crt", use_hbit_gate=False)
278+
return TinyLM(**common, pe_kind="crt", gate_mode="none")
253279
if arch == "hybrid":
254-
return TinyLM(**common, pe_kind="crt", use_hbit_gate=True)
280+
return TinyLM(**common, pe_kind="crt", gate_mode="key")
281+
if arch == "hybrid_score":
282+
return TinyLM(**common, pe_kind="crt", gate_mode="score")
283+
if arch == "hybrid_learned":
284+
return TinyLM(**common, pe_kind="crt", gate_mode="learned")
255285
raise ValueError(f"unknown arch: {arch}")

0 commit comments

Comments
 (0)