Skip to content

Commit 1f08c22

Browse files
committed
transformerless_lm: quantization fixes — reciprocal tiers + per-row scale
The v1 Fibonacci-tier quantizer saturated at +1 nat loss regardless of bit depth because: - tier values {1, 2, 3, 5, 8, 13, ...} have no resolution between 0 and 1, but Gaussian weight distributions are concentrated near 0 - global per-tensor scale wastes the tail tier values (89, 144, 233, ...) on a single max-magnitude weight Two fixes implemented: 1. reciprocal Fibonacci tier values (reciprocals=True) {0, +-1/F_max, ..., +-1/3, +-1/2, +-1, +-2, +-3, +-5, ...} geometric spacing crossing zero with ratio approaching phi between adjacent values, giving fine resolution near 0 where weights live. 2. per-row scale (scale="per_row") each output row of W gets its own scale factor matched to its own magnitude range, instead of one global scale for the entire tensor. Standard per-channel quantization trick, applied to the Fibonacci-tier setting. train_weight_substrate.py now sweeps the cross product of: n_tiers in {4, 8, 16, 32} x reciprocals in {False, True} x scale in {"per_tensor", "per_row"} = 16 quantizer configurations per arch, plus the 2 fp32 baselines. The bench tells us which of the four combinations (rec * scale) actually clears the <=0.1 nat validation threshold, and at what tier depth, separately for dense_crt and tied_substrate. User's framing: faster inference for larger models. Each successful fix is a step toward that. The dense_crt validating at <=4 bits would already mean 4-8x parameter-storage compression vs fp32 / fp16.
1 parent 60d0b43 commit 1f08c22

2 files changed

Lines changed: 132 additions & 89 deletions

File tree

experiments/transformerless_lm/models_substrate.py

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -52,71 +52,91 @@
5252
FIB_POS_UNIQUE = sorted(set(f for f in FIBONACCI if f > 0))
5353

5454

55-
def fibonacci_tier_values(n_tiers: int) -> list[float]:
56-
"""First n_tiers signed Fibonacci tier values, smallest magnitude first.
57-
58-
Tier 0 = 0 (the "fold-to-zero" attractor).
59-
Tier 1 = ±1
60-
Tier 2 = ±2
61-
Tier 3 = ±3
62-
Tier k = ±F(k) for F(k) the k-th unique positive Fibonacci number.
63-
64-
Returns sorted list [-F_max, ..., -1, 0, +1, ..., +F_max] suitable
65-
for nearest-tier-value snapping.
55+
def fibonacci_tier_values(n_tiers: int, reciprocals: bool = False) -> list[float]:
56+
"""Signed Fibonacci tier values.
57+
58+
Without reciprocals (the original v1):
59+
{0, ±1, ±2, ±3, ±5, ±8, ±13, ±21, ...}
60+
log spacing toward infinity, no resolution between 0 and 1.
61+
62+
With reciprocals (v2 — fixes the "no resolution near zero" failure
63+
from the v1 bench):
64+
{0, ±1/F_max, ..., ±1/5, ±1/3, ±1/2, ±1, ±2, ±3, ±5, ±8, ..., ±F_max}
65+
log spacing crossing zero — fine resolution near 0 where most
66+
Gaussian-distributed weights actually live.
67+
68+
Adjacent ratios approach φ (since F(k+1)/F(k) → φ), so this is
69+
the natural phi-Fibonacci tier system the substrate already uses
70+
elsewhere in OMC.
6671
"""
6772
fibs = FIB_POS_UNIQUE[: max(0, n_tiers - 1)]
68-
return sorted([-f for f in fibs] + [0.0] + [float(f) for f in fibs])
73+
pos = [float(f) for f in fibs]
74+
if reciprocals:
75+
pos = sorted(set(pos + [1.0 / f for f in fibs if f > 1]))
76+
return sorted([-v for v in pos] + [0.0] + pos)
6977

7078

7179
def fibonacci_tier_snap(W: torch.Tensor, n_tiers: int = 8,
72-
scale: str = "per_tensor") -> tuple[torch.Tensor, int]:
80+
scale: str = "per_tensor",
81+
reciprocals: bool = False) -> tuple[torch.Tensor, int]:
7382
"""Snap each weight in W to its nearest signed-Fibonacci tier value.
7483
75-
The user's Principle B: every weight folds to a Fibonacci tier;
76-
high-frequency / large-magnitude weights occupy the small tiers
77-
(1, 2, 3); rare ones drift to higher tiers (5, 8, 13, ...) and
78-
very rare ones fold to 0 (pruned).
79-
8084
Args:
81-
W: tensor of arbitrary shape.
82-
n_tiers: number of tier levels per sign (including 0).
83-
e.g. n_tiers=4 → values in {-3, -2, -1, 0, 1, 2, 3}
84-
= 7 levels = log2(7) ≈ 2.8 bits per weight.
85-
scale: "per_tensor" → one scale factor over the full tensor.
85+
W: tensor (1-D or 2-D).
86+
n_tiers: resolution per sign (= number of distinct positive Fibonacci
87+
values, before any reciprocals).
88+
scale: "per_tensor" → one global scale set by max(|W|).
89+
"per_row" → one scale per output row of a 2-D matrix
90+
(matches each row's own dynamic range; the
91+
standard per-channel quantization trick).
92+
reciprocals: if True, include 1/F(k) values in the tier set —
93+
gives fine resolution near 0.
8694
8795
Returns:
88-
(W_quantized, n_unique_values_actually_used)
96+
(W_quantized, n_unique_values_actually_used_avg)
8997
"""
9098
tier_vals = torch.tensor(
91-
fibonacci_tier_values(n_tiers), dtype=W.dtype, device=W.device
92-
) # [n_levels]
93-
abs_max = W.abs().max().item()
94-
if abs_max == 0:
95-
return W.clone(), 1
96-
# Choose scale so the largest |w| maps to the largest tier value.
97-
s = abs_max / max(tier_vals.abs().max().item(), 1.0)
98-
# Nearest tier (in scaled units).
99-
target_vals = tier_vals * s # [n_levels]
100-
# For each element of W, find the closest target value.
101-
diffs = (W.unsqueeze(-1) - target_vals).abs() # [..., n_levels]
102-
nearest = diffs.argmin(dim=-1) # [...]
103-
W_q = target_vals[nearest]
104-
n_unique = nearest.unique().numel()
105-
return W_q, n_unique
99+
fibonacci_tier_values(n_tiers, reciprocals=reciprocals),
100+
dtype=W.dtype, device=W.device,
101+
) # [n_levels]
102+
max_tier = max(tier_vals.abs().max().item(), 1.0)
103+
104+
if scale == "per_tensor":
105+
abs_max = W.abs().max().item()
106+
if abs_max == 0:
107+
return W.clone(), 1
108+
s = abs_max / max_tier
109+
target_vals = tier_vals * s
110+
diffs = (W.unsqueeze(-1) - target_vals).abs()
111+
nearest = diffs.argmin(dim=-1)
112+
W_q = target_vals[nearest]
113+
n_unique = nearest.unique().numel()
114+
return W_q, n_unique
115+
116+
if scale == "per_row":
117+
if W.dim() != 2:
118+
# Fall back to per-tensor for 1-D / N-D parameters.
119+
return fibonacci_tier_snap(W, n_tiers, "per_tensor", reciprocals)
120+
abs_max_row = W.abs().max(dim=-1, keepdim=True).values.clamp(min=1e-12) # [out, 1]
121+
s_row = abs_max_row / max_tier # [out, 1]
122+
# For each row, scaled tier set is tier_vals * s_row. We need
123+
# per-row argmin over [out, in, n_levels].
124+
targets = tier_vals.view(1, 1, -1) * s_row.unsqueeze(-1) # [out, 1, n_levels]
125+
diffs = (W.unsqueeze(-1) - targets).abs() # [out, in, n_levels]
126+
nearest = diffs.argmin(dim=-1) # [out, in]
127+
W_q = torch.gather(targets.expand_as(diffs), -1,
128+
nearest.unsqueeze(-1)).squeeze(-1)
129+
n_unique = nearest.unique().numel()
130+
return W_q, n_unique
131+
132+
raise ValueError(scale)
106133

107134

108135
def fibonacci_quantize_model(model: torch.nn.Module, n_tiers: int = 8,
136+
scale: str = "per_tensor",
137+
reciprocals: bool = False,
109138
targets: list[str] = None) -> dict:
110-
"""In-place Fibonacci-tier-snap of model parameters matching `targets`.
111-
112-
Args:
113-
model: any nn.Module.
114-
n_tiers: tier resolution per sign.
115-
targets: substrings; a param is quantized iff any target is in
116-
its name. Default = quantize ALL params.
117-
118-
Returns: dict with summary stats (params_quantized, n_unique_per_tensor).
119-
"""
139+
"""In-place Fibonacci-tier-snap of model parameters matching `targets`."""
120140
if targets is None:
121141
targets = [""]
122142
stats = {"params_quantized": 0, "tensors_quantized": 0,
@@ -125,7 +145,9 @@ def fibonacci_quantize_model(model: torch.nn.Module, n_tiers: int = 8,
125145
if not any(t in name for t in targets):
126146
continue
127147
with torch.no_grad():
128-
W_q, n_unique = fibonacci_tier_snap(p.data, n_tiers=n_tiers)
148+
W_q, n_unique = fibonacci_tier_snap(
149+
p.data, n_tiers=n_tiers, scale=scale, reciprocals=reciprocals,
150+
)
129151
p.data.copy_(W_q)
130152
stats["params_quantized"] += p.numel()
131153
stats["tensors_quantized"] += 1

experiments/transformerless_lm/train_weight_substrate.py

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -156,73 +156,94 @@ def main():
156156
print(f"\n ✓ [arch={arch}] fp32 final_val = {final_val:.4f}")
157157

158158
# ---- Principle B: post-hoc Fibonacci-tier quantization sweep ----
159-
# Keep the trained weights in `state_dict_orig` so we can re-quantize
160-
# at each n_tiers from a clean baseline.
161159
state_dict_orig = {k: v.clone() for k, v in model.state_dict().items()}
162-
for n_tiers in tier_sweep:
160+
configs = []
161+
for reciprocals in [False, True]:
162+
for scale in ["per_tensor", "per_row"]:
163+
for n_tiers in tier_sweep:
164+
configs.append((n_tiers, reciprocals, scale))
165+
for n_tiers, reciprocals, scale in configs:
163166
model.load_state_dict(state_dict_orig)
164-
stats = fibonacci_quantize_model(model, n_tiers=n_tiers)
167+
stats = fibonacci_quantize_model(
168+
model, n_tiers=n_tiers,
169+
reciprocals=reciprocals, scale=scale,
170+
)
165171
eval_gen.manual_seed(args.seed + 1000)
166172
vq = evaluate(model, val_split, args.batch_size, args.seq_len,
167173
n_batches=32, generator=eval_gen)
168174
n_unique_total = sum(s["n_unique_tier_values"]
169175
for s in stats["per_tensor"].values())
170176
n_tensors = stats["tensors_quantized"]
171177
avg_unique = n_unique_total / max(n_tensors, 1)
172-
print(f" n_tiers={n_tiers:>3} → val={vq:.4f} Δ={vq - final_val:+.4f} "
173-
f"avg_unique_tier_vals={avg_unique:.1f}", flush=True)
174-
arch_record["quantized"][n_tiers] = {
178+
key = f"n{n_tiers}_{'rec' if reciprocals else 'nor'}_{scale}"
179+
print(f" {key:<22} → val={vq:.4f} Δ={vq - final_val:+.4f} "
180+
f"avg_unique={avg_unique:.1f}", flush=True)
181+
arch_record["quantized"][key] = {
182+
"n_tiers": n_tiers,
183+
"reciprocals": reciprocals,
184+
"scale": scale,
175185
"val": vq,
176186
"delta": vq - final_val,
177187
"params_quantized": stats["params_quantized"],
178188
"avg_unique_tier_values": avg_unique,
179189
}
180-
# Restore fp32 weights at the end so subsequent code can use the model.
181190
model.load_state_dict(state_dict_orig)
182191
results["archs"][arch] = arch_record
183192

184-
# ---- Summary table ----
193+
# ---- Summary tables ----
185194
print()
186-
print("=" * 96)
187-
schemes = ["fp32"] + [f"q{n}" for n in tier_sweep]
188-
header = f"{'arch':<18} {'attn_params':>12} {'total':>10} "
189-
header += " ".join(f"{s:>10}" for s in schemes)
190-
print(header)
191-
print("-" * 96)
195+
print("=" * 110)
196+
print("FP32 BASELINES")
197+
print("-" * 110)
198+
print(f"{'arch':<18} {'attn_params':>12} {'total':>10} {'fp32_val':>10}")
192199
for arch in ["dense_crt", "tied_substrate"]:
193200
r = results["archs"][arch]
194-
vals = [r["val_fp32"]]
195-
for n_tiers in tier_sweep:
196-
vals.append(r["quantized"][n_tiers]["val"])
197-
cells = " ".join(f"{v:>10.4f}" for v in vals)
198-
print(f"{arch:<18} {r['n_attn_params']:>12,} {r['n_params']:>10,} {cells}")
201+
print(f"{arch:<18} {r['n_attn_params']:>12,} {r['n_params']:>10,} "
202+
f"{r['val_fp32']:>10.4f}")
203+
204+
print()
205+
print("=" * 110)
206+
print("QUANTIZATION SWEEP — Δ vs fp32 for each arch")
207+
print("(rec=with reciprocal Fibonacci tiers; nor=Fibonacci only)")
208+
print("-" * 110)
209+
for arch in ["dense_crt", "tied_substrate"]:
210+
r = results["archs"][arch]
211+
print(f"\n {arch} (fp32 val = {r['val_fp32']:.4f}):")
212+
print(f" {'n_tiers':>8} {'tier_set':>10} {'scale':>12} "
213+
f"{'val':>10} {'Δ':>10} {'unique':>10}")
214+
for key, q in r["quantized"].items():
215+
print(f" {q['n_tiers']:>8} {('rec' if q['reciprocals'] else 'nor'):>10} "
216+
f"{q['scale']:>12} {q['val']:>10.4f} {q['delta']:>+10.4f} "
217+
f"{q['avg_unique_tier_values']:>10.1f}")
199218

200219
# ---- Interpretation ----
201220
print()
202-
print("INTERPRETATION:")
221+
print("=" * 110)
222+
print("INTERPRETATION")
223+
print("-" * 110)
203224
a_fp32 = results["archs"]["dense_crt"]["val_fp32"]
204225
t_fp32 = results["archs"]["tied_substrate"]["val_fp32"]
205226
a_attn = results["archs"]["dense_crt"]["n_attn_params"]
206227
t_attn = results["archs"]["tied_substrate"]["n_attn_params"]
207228
rel = (t_fp32 - a_fp32) / a_fp32 * 100
208-
print(f" Principle A (tied substrate vs dense_crt):")
209-
print(f" val_fp32 delta: {t_fp32 - a_fp32:+.4f} ({rel:+.1f}%)")
210-
print(f" attn param reduction: {a_attn / t_attn:.2f}x")
211-
if abs(rel) < 5:
212-
print(f" → PRINCIPLE A: tied QKV trains within 5% of dense — VALIDATED")
213-
else:
214-
print(f" → PRINCIPLE A: tied QKV {('lost' if rel > 0 else 'gained')} {abs(rel):.1f}% "
215-
f"— {'NOT VALIDATED (too tight)' if rel > 5 else 'BEAT BASELINE'}")
216-
217-
print(f"\n Principle B (Fibonacci-tier quantization on dense_crt):")
218-
for n_tiers in tier_sweep:
219-
delta = results["archs"]["dense_crt"]["quantized"][n_tiers]["delta"]
220-
# log2(2*n_tiers - 1) bits per weight (n_tiers includes 0, then mirror)
221-
# for n_tiers=8: levels = 2*8-1 = 15 → ~4 bits.
222-
levels = 2 * n_tiers - 1
223-
bits = (levels - 1).bit_length()
224-
verdict = "VALIDATED" if delta < 0.1 else ("DEGRADED" if delta < 0.5 else "BROKEN")
225-
print(f" n_tiers={n_tiers:>3} (~{bits} bits): val delta {delta:+.4f} {verdict}")
229+
print(f"\nPRINCIPLE A (tied substrate vs dense_crt):")
230+
print(f" val_fp32 delta: {t_fp32 - a_fp32:+.4f} ({rel:+.1f}%)")
231+
print(f" attn param reduction: {a_attn / t_attn:.2f}x")
232+
verdict_a = ("VALIDATED" if abs(rel) < 5 else
233+
("BEAT BASELINE" if rel < 0 else "NOT VALIDATED"))
234+
print(f" → PRINCIPLE A: {verdict_a}")
235+
236+
print(f"\nPRINCIPLE B (best quantizer per arch, vs ≤0.10 nat threshold):")
237+
for arch in ["dense_crt", "tied_substrate"]:
238+
r = results["archs"][arch]
239+
best_key = min(r["quantized"], key=lambda k: r["quantized"][k]["delta"])
240+
best = r["quantized"][best_key]
241+
verdict_b = "VALIDATED" if best["delta"] < 0.10 else (
242+
"USABLE" if best["delta"] < 0.30 else "BROKEN")
243+
print(f" {arch}:")
244+
print(f" best = {best_key} (Δ={best['delta']:+.4f}, "
245+
f"n_tiers={best['n_tiers']}, rec={best['reciprocals']}, "
246+
f"scale={best['scale']}) {verdict_b}")
226247

227248
out_path = Path(__file__).parent / args.out
228249
with open(out_path, "w") as f:

0 commit comments

Comments
 (0)