summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--EMAIL_BEN_DRAFT2.md19
-rw-r--r--docs/campaign/CASCADE_ABLATION_PLAN.md24
-rw-r--r--docs/campaign/COST_MODEL.md47
-rw-r--r--docs/hardware/COMPONENT_HW_MAP.md10
-rw-r--r--ep_run/etier_probe.py227
5 files changed, 314 insertions, 13 deletions
diff --git a/EMAIL_BEN_DRAFT2.md b/EMAIL_BEN_DRAFT2.md
index f367b12..0b6fc4a 100644
--- a/EMAIL_BEN_DRAFT2.md
+++ b/EMAIL_BEN_DRAFT2.md
@@ -16,17 +16,17 @@ The results promised yesterday, concretely:
- **The gap ledger.** At matched hyperparameters, 4k steps: EP statistically indistinguishable from its tuned backprop control (three seeds each). Full epoch: 0.050 nats behind, with the mechanism identified — the late-training decline of estimator signal-to-noise as the true gradient shrinks against a fixed precision floor (the quantified version of your β remark) — and a β-schedule that removes most of it. Muon's improvement over AdamW transfers to EP gradients at full magnitude.
- **Solver cost.** Our nudged phase runs the full EP step at 3.2× the FLOPs of a backprop step (measured; single-sided, K=3 full-depth sweeps per nudge). For calibration against your Appendix B: that ratio is what turns an 18-day run into a ~4-day one at equal hardware.
-On costs, anchored on measured throughput (TF32 verified bit-exact):
+On costs — calibrated against a measured datapoint rather than list pricing (a 1B × 10B-token backprop run we executed on rented H200s cost ≈$1k all-in), combined with the measured 3.2× EP/BP FLOP ratio:
-| run | cost, incl. 1.4× failure allowance |
+| run | cost at market H100/H200 rates, incl. 1.3× failure allowance |
|---|---|
-| 300M × 6B tokens — recipe validation on a standard corpus | ~$1k |
-| **1B × 20B tokens, Chinchilla-optimal** | **~$12k** |
-| 1B backprop control | ~$4k |
+| 300M × 6B tokens — recipe validation on a standard corpus | ~$400 |
+| **1B × 20B tokens, Chinchilla-optimal** | **~$4k** (≤$8k even at the uncalibrated worst case) |
+| 1B backprop control, same tokens | ~$1.5k |
-Within $50k this leaves room for a second seed or a first 1.5B segment. The allowance is modest because the failure modes are characterized rather than assumed, and training checkpoints every 5k steps — a failure loses only the segment since the last checkpoint. A one-day calibration on your instances (~$500) would reduce the uncertainty from ±30% to ±10–15%; we would be glad to run that first.
+Three qualifications. The numbers assume the TF32 path we have verified bit-exact against fp32; a standard mixed-precision variant (bf16 matrix products, fp32 states and energy accumulation) is not yet validated for EP and would roughly halve them if it passes our gradient-fidelity gate. AWS list pricing runs 1.5–2.5× market rental per GPU-hour, with spot or capacity-block reservations closing most of that gap. And checkpoints every 5k steps bound any failure to the segment since the last one. A one-day calibration on your instances (~$500) would replace these estimates with measurements; we would be glad to run that first.
-The question we are weighing beyond that: **whether 3B at full Chinchilla, or 7–8B at reduced token count, is feasible** (~$25–30k and ~$50k respectively at current measured throughput). We are not requesting this now — the intention is to complete the runs above and revisit the larger scale on the basis of those results.
+This pricing changes the scale question. Within $50k, the 1B milestone and its control leave room for one of: **3B at full Chinchilla scaling (~$30–40k at market rates), or 7–8B at a GPT-3-style token budget (~$25–35k)** — AWS on-demand pricing adds roughly half on top, and the mixed-precision variant, if it validates, subtracts it back. Our intention is unchanged — complete the 1B result first and let it argue for the larger run — but the envelope does reach one of the two.
Separately, and possibly of direct interest to Rain: your paper leaves open whether F_PCN can be realized efficiently in hardware — **that question is the one our recipe was designed around.** Every trained operation was chosen to have a known analog implementation: divisive (current-mode) normalization, fixed rotations for the position encoding, translinear multipliers for the gated MLP, the subthreshold-exponential/KCL circuit for attention's softmax, and EP's two relaxation phases for the learning rule. A component-by-component hardware mapping is written up; we can share it.
@@ -39,5 +39,8 @@ Yuren
## 发送前 checklist(内部,不进邮件)
1. [LINK] 换成真实 Overleaf 链接(zip 已备好: overleaf_dynamics_v3.zip,含两对象改写+KHS26引用+floss重调数字)
2. 附 a1_demo.ipynb(token 已内置)
-3. gap 数字核对: f3e3cont 终值若 <1.2808 → "0.050 nats" 段落改为新 gap + "a β-schedule removes most of it" 升级为具体数字
+3. ~~gap 数字核对~~ RESOLVED: f3e3cont 终值 1.2883 > 1.2808 → "0.050 nats" 保持不变
4. 3.2× 的对照句("18-day → ~4-day")口吻是否合适你再定——它来自他们自己的 Appendix B,准确但直接
+5. 成本表 2026-07-12 已按用户实测锚点重校准(1B×10B BP H200 租赁 ≈$1k → $16.7k/EF 全包上限;
+ 市场价 H100/H200 TF32 路径 $6-9k/EF)。旧表锚 AWS-A100-savings,每 FLOP 贵 2.5-3×,已废。
+ 完整推导见 docs/campaign/COST_MODEL.md
diff --git a/docs/campaign/CASCADE_ABLATION_PLAN.md b/docs/campaign/CASCADE_ABLATION_PLAN.md
index 61860f6..8b52369 100644
--- a/docs/campaign/CASCADE_ABLATION_PLAN.md
+++ b/docs/campaign/CASCADE_ABLATION_PLAN.md
@@ -538,3 +538,27 @@ s1 alone beat the BP mean). Ledger of the residual-0.05 epoch gap after three pr
Carry `--bsign_rand` and a future centered mode as Stage-2 A/B flags; the gap question re-opens at
300M/real-corpus where it means something. Effort pivots to: (1) Stage-2 data pipeline (FineWeb-Edu +
32k tokenizer), (2) E-tier tolerance suite on the idle farm (hardware track / UIUC outreach feed).
+
+### RESULT 14 (2026-07-12): E-TIER WAVE-1 — full analog-fault tolerance ledger at stage1b s55000.
+`etier_probe.py`, farm GPUs 2/3/7 (shards A/B/C), stage1b_ep_muon_s55000.pt (clean valCE 1.2678),
+B=8 eval batches; metrics = faulted valCE (Δ vs clean), cos(EP_faulted, BP_faulted) [self-consistency
+of the learning signal under fault], cos(EP_faulted, BP_clean) [direction vs the ideal update].
+
+| fault (component) | mild | medium | severe | verdict |
+|---|---|---|---|---|
+| wq — weight quant (crossbar #3) | 8b: +0.004 / 0.956 | 6b: +0.051 / 0.812 | 4b: +2.23 / 0.05 | **8b FREE, 6b marginal, 4b dead → ≥7b effective is the binding spec** |
+| fnoise — fwd additive state noise (softmax/relax #5) | 1e-3: 0.000 / 0.975 | 3e-3: 0.000 / 0.974 | 1e-2: +0.001 / 0.970 | **FREE at 1% — looped-era 1e-3 cliff does NOT transfer to cascade** |
+| divmis — divisive-norm mismatch (#4/#7) | 1%: 0.000 / 0.971 | 3%: +0.003 / 0.948 | 10%: +0.042 / 0.785 | 3% (routine matching) FREE; 10% marginal |
+| rope — phase error rad (#2) | 0.01: 0.000 / 0.973 | 0.03: +0.001 / 0.967 | 0.1: +0.012 / 0.923 | 0.03 rad FREE; ~2° I/Q accuracy suffices |
+| gilbert — gate gain error (#6) | 1%: 0.000 / 0.974 | 3%: 0.000 / 0.971 | 10%: +0.006 / 0.945 | **FREE at 10%** — translinear practice is comfortably inside |
+| fbnoise — nudge/error-channel noise | 1e-2: 0.969 / 0.975 | **1e-1: 0.951 / 0.957** | 3e-1: 0.764 / 0.768 | **10% relative noise on the ERROR CHANNEL is FREE** (cos 0.95) — the r-indifference/large-nudge gift, now measured on cascade |
+
+Reading: (a) the only hard constraint is crossbar weight precision (≥7b effective — inside standard
+SRAM-CIM capability; 6b rescue = wave-2 quant-aware co-training); (b) everything dynamic — forward
+noise 1%, error-channel noise 10%, gate/divider/phase mismatch at routine device tolerances — is
+FREE at this scale. cos(EP,BP_faulted) stays ~0.97 under every non-fatal fault: the EP estimator
+tracks whatever network the faults define, i.e. learning co-adapts to the fault (the analog-training
+thesis in one number). CAVEAT: static probes at a trained checkpoint (eval CE + one-step gradient
+direction), not training-under-fault; wave-2 = co-training with faults injected from step 0
+(expectation from the literature and from (c): tolerances IMPROVE). Feeds COMPONENT_HW_MAP.md
+(per-row status updated) + UIUC outreach dossier.
diff --git a/docs/campaign/COST_MODEL.md b/docs/campaign/COST_MODEL.md
new file mode 100644
index 0000000..2b9d8b6
--- /dev/null
+++ b/docs/campaign/COST_MODEL.md
@@ -0,0 +1,47 @@
+# Cost model v2 (2026-07-12) — recalibrated on a measured rental datapoint
+
+Supersedes the v1 table in EMAIL_BEN_DRAFT2.md (drafts ≤5) and the numbers quoted in the
+2026-07-11 scale discussion. **v1's error: anchored $/FLOP on AWS-A100-savings TF32
+(~$19.7k/EF) — the wrong reference class for 2026.** Market H100/H200 rentals are 2.5–3×
+cheaper per FLOP.
+
+## Anchors
+
+| anchor | value | source |
+|---|---|---|
+| **Empirical all-in ceiling** | **$16.7k/EF** | user's real run: 1B × 10B tok BP on rented H200s ≈ $1k all-in; 6ND = 0.06 EF. Implied throughput only 42–58 TF/s eff (4–6% of bf16 peak) — an UN-optimized run, so this is a ceiling, not a target |
+| Market rental rate | $2–3.5/GPU·h | H100/H200 marketplace (Vast/RunPod class), 2026 |
+| Our stack's utilization | ~19% of TF32 peak | measured on A6000 (14 TF eff / 75 TF TF32 peak), eager TF32 |
+| → projected H200 throughput | 75–125 TF/s eff | 15–25% × 494 TF TF32 peak |
+| → **working rate (TF32 path)** | **$6–9k/EF** (mid $7.5k) | $2.5–3/h ÷ 75–125 TF/s |
+| EP/BP FLOP ratio | **3.2×** (measured) | EP ≈ 19.2N FLOPs/tok vs BP 6N; single-sided, K=3 |
+| Failure allowance | 1.3× | failure modes characterized; ckpt every 5k steps |
+| AWS multiplier | 1.5–2.5× on-demand; 1.2–1.5× spot/capacity-block | vs marketplace $/GPU·h |
+| Mixed-precision upside | ÷~2 | proper autocast (bf16 matmul, fp32 states + E-accum) — UNVALIDATED for EP; naive-cast is dead (cos ≤0.67) |
+
+Cross-check: v1's per-EF rate ($19.7k) ≈ the empirical ceiling ($16.7k) — v1 rows were not
+order-of-magnitude wrong, they were "un-optimized-run" priced. The row deltas people remember
+($1k vs $12k) decompose as: ×2 tokens (20B vs 10B) × 3.2 EP × 1.4 buffer ≈ ×9.
+
+## Table (market rates, TF32 path, ×1.3 buffer; AWS on-demand ≈ +50–150%)
+
+| run | EP FLOPs (19.2ND) | cost | note |
+|---|---|---|---|
+| 300M × 6B (recipe validation) | 0.035 EF | **~$400** | |
+| **1B × 20B Chinchilla EP** | 0.384 EF | **~$4k** | ≤$8k at the empirical-ceiling rate |
+| 1B × 20B BP control | 0.12 EF (6ND) | **~$1.5k** | user's datapoint directly: 10B = $1k |
+| **3B × 60B full-Chinchilla EP** | 3.46 EF | **~$30–40k** | inside $50k alone (market); AWS on-demand $45–60k → needs spot or bf16 |
+| **7B × 20B (GPT-3-style) EP** | 2.69 EF | **~$25–35k** | inside envelope |
+| 7B × 140B full-Chinchilla EP | 18.8 EF | $110–170k | out of scope; dedicated funding |
+
+## Consequences
+
+1. **$50k envelope reaches ONE of {3B full-Chinchilla, 7–8B reduced-token} after the 1B milestone** —
+ the scale conversation upgrades from speculative to budgetable.
+2. The **calibration day (~$500 on Ben's AWS instances)** now resolves a 2× spread
+ (working rate vs empirical ceiling, AWS vs market) — worth more than before.
+3. **bf16 mixed-precision validation is the highest-leverage engineering item**: ÷2 across the
+ table brings BOTH large options comfortably inside AWS on-demand pricing. Distinct from the
+ dead naive-cast: fp32 states + fp32 energy accumulation, bf16 matmuls only, gate = cos(EP,BPTT).
+4. Throughput assumptions are conservative (eager, no compile/flash in the nudged phase);
+ the speed-profile levers (--holofast, --sdpa ≈ 1.5×) are not priced in.
diff --git a/docs/hardware/COMPONENT_HW_MAP.md b/docs/hardware/COMPONENT_HW_MAP.md
index 95c5689..ff47123 100644
--- a/docs/hardware/COMPONENT_HW_MAP.md
+++ b/docs/hardware/COMPONENT_HW_MAP.md
@@ -11,11 +11,11 @@ Companion docs: `HW_RESEARCH_FINDINGS.md` (softmax dossier, OLMo2 analog audit,
| # | Component | Computes | Analog primitive | Reuse source (no tapeout) | Feedback path (nudged phase) | Tolerance status |
|---|---|---|---|---|---|---|
| 1 | Token embedding | table lookup | **digital** (SRAM lookup + DAC line drivers) | any MCU/FPGA + DAC array | none (input boundary) | n/a |
-| 2 | RoPE | fixed per-position 2×2 rotations on q,k | **I/Q quadrature mixer**: cos/sin DDS + multiplying DACs | century-old RF practice; COTS mixer/DDS chips | transpose = rotation by −θ — **same mixer, negated sin** | phase error ≈ position blur — expect very tolerant; **E-tier item (queued)** |
-| 3 | qkv / attn-proj / SwiGLU w1,w3,w2 / head | matrix–vector products | **crossbar arrays** (the bulk of all compute) | Demo-0/1: **SRAM-CIM (Shanbhag DIMA class)**; alternatives: Mythic flash CIM, IBM HERMES PCM eval | J^T reads = **bidirectional crossbar access** (the known PAR price; standard in every analog-EP proposal) | wq8 safe / wq6 marginal (looped-era static-tolerance); **re-run on cascade in E-tier** |
-| 4 | QK-norm (full-width RMS) | q,k ← q/rms(q)·g | **divisive normalization**: square-law devices + KCL current sum + divider/AGC | same primitive class as softmax normalization periphery | Jacobian symmetric — self-transpose, no extra hardware | divider mismatch/offset — **E-tier item (queued)** |
-| 5 | Softmax (causal) | exp + row-normalize | **Elfadel–Wyatt "entropic resistor"**: subthreshold-exp devices + KCL normalization (log-sum-exp co-content) | classic analog-VLSI circuit family; small arrays demonstrated | softmax Jacobian diag(p)−pp^T is symmetric; the QKV coupling around it is the non-reciprocal part | ±1% device mismatch → ±1% softmax error (linear); QK-norm bounds its input range. Dynamic-noise cliff (fnoise ≥1e-3, looped-era) **re-test on cascade = E-tier** |
-| 6 | SwiGLU gate | silu(w1x) ⊙ (w3x) | sigmoid = differential pair (native); ⊙ = **Gilbert/translinear multiplier** per unit | textbook standard cells | transpose of ⊙ = multiply by the partner signal — cheap | per-multiply mismatch/noise, 1408 wide — **E-tier item (queued)** |
+| 2 | RoPE | fixed per-position 2×2 rotations on q,k | **I/Q quadrature mixer**: cos/sin DDS + multiplying DACs | century-old RF practice; COTS mixer/DDS chips | transpose = rotation by −θ — **same mixer, negated sin** | **E-tier w1: 0.03 rad FREE; 0.1 rad ΔCE +0.012, cos_clean 0.92** — ~2° I/Q phase accuracy suffices (easy) |
+| 3 | qkv / attn-proj / SwiGLU w1,w3,w2 / head | matrix–vector products | **crossbar arrays** (the bulk of all compute) | Demo-0/1: **SRAM-CIM (Shanbhag DIMA class)**; alternatives: Mythic flash CIM, IBM HERMES PCM eval | J^T reads = **bidirectional crossbar access** (the known PAR price; standard in every analog-EP proposal) | **E-tier w1 (cascade): wq8 FREE (ΔCE +0.004, cos_clean 0.96); wq6 MARGINAL (+0.051, 0.81); wq4 DEAD (+2.23)** — ≥7b effective weight precision is the binding spec; wave-2 lever = quant-aware co-training |
+| 4 | QK-norm (full-width RMS) | q,k ← q/rms(q)·g | **divisive normalization**: square-law devices + KCL current sum + divider/AGC | same primitive class as softmax normalization periphery | Jacobian symmetric — self-transpose, no extra hardware | **E-tier w1: 3% mismatch FREE (ΔCE +0.003, cos_clean 0.95); 10% MARGINAL (+0.042, 0.78)** — 3% device matching is routine |
+| 5 | Softmax (causal) | exp + row-normalize | **Elfadel–Wyatt "entropic resistor"**: subthreshold-exp devices + KCL normalization (log-sum-exp co-content) | classic analog-VLSI circuit family; small arrays demonstrated | softmax Jacobian diag(p)−pp^T is symmetric; the QKV coupling around it is the non-reciprocal part | ±1% device mismatch → ±1% softmax error (linear); QK-norm bounds its input range. **E-tier w1: forward additive state noise σ=1e-2 FREE (ΔCE +0.001, cos 0.97) — the looped-era 1e-3 cliff does NOT transfer to cascade** |
+| 6 | SwiGLU gate | silu(w1x) ⊙ (w3x) | sigmoid = differential pair (native); ⊙ = **Gilbert/translinear multiplier** per unit | textbook standard cells | transpose of ⊙ = multiply by the partner signal — cheap | **E-tier w1: 10% gain error ΔCE +0.006, cos_clean 0.945 — FREE** at translinear-practice tolerances |
| 7 | RMSNorm ×2 per block (norm-after-sublayer) | bound each sublayer output | divisive normalization (as #4), **no mean-subtraction path** (cheaper than LayerNorm) | same as #4 | symmetric Jacobian | as #4; ~4L+1 normalizers total = the largest NEW periphery count |
| 8 | Residual stream | z + branch outputs | **current-summing bus (KCL node)** | wires | pass-through | norm-after bounds every injection — the arch change is itself the tolerance fix |
| 9 | Final RMSNorm | bound pre-readout state | as #4 | as #4 | symmetric | bounds the **ADC** dynamic range at the digital boundary |
diff --git a/ep_run/etier_probe.py b/ep_run/etier_probe.py
new file mode 100644
index 0000000..343b77a
--- /dev/null
+++ b/ep_run/etier_probe.py
@@ -0,0 +1,227 @@
+"""E-tier wave-1: device-fault tolerance probes on the trained OLMo2 cascade (stage1b ckpt).
+For each fault x severity: (a) faulted free-forward val CE (inference survival),
+(b) cos(EP_faulted, BP_faulted) — estimator robustness on the faulted system,
+(c) cos(EP_faulted, BP_clean) — direction vs the clean-system gradient.
+Faults: wq (weight quant bits) | fnoise (dynamic block-output noise, mult) |
+divmis (RMSNorm divider mismatch, fixed per-channel) | gilbert (SwiGLU gate gain mismatch)
+| rope (phase error) | fbnoise (additive error-channel noise in the nudged feedback).
+Usage: etier_probe.py --shard {A,B,C}
+"""
+import argparse, math, pickle, copy
+import numpy as np, torch, torch.nn as nn, torch.nn.functional as F
+from pathlib import Path
+
+ap = argparse.ArgumentParser()
+ap.add_argument('--shard', required=True, choices=['A', 'B', 'C'])
+ap.add_argument('--ckpt', default='runs/stage1b_ep_muon_s55000.pt')
+ap.add_argument('--beta', type=float, default=1e-3)
+ap.add_argument('--K', type=int, default=3)
+ap.add_argument('--nb', type=int, default=4)
+a = ap.parse_args()
+dev = 'cuda' if torch.cuda.is_available() else 'cpu'
+torch.manual_seed(7)
+
+DD = Path('/home/yurenh2/ept/ep_run/data/tinystories_bpe')
+vocab = pickle.load(open(DD / 'meta.pkl', 'rb'))['vocab_size']
+B, T = 8, 256
+
+def get_batch():
+ data = np.memmap(DD / 'val.bin', dtype=np.uint16, mode='r')
+ ix = torch.randint(len(data) - T - 1, (B,))
+ x = torch.stack([torch.from_numpy(data[i:i + T].astype(np.int64)) for i in ix])
+ y = torch.stack([torch.from_numpy(data[i + 1:i + 1 + T].astype(np.int64)) for i in ix])
+ return x.to(dev), y.to(dev)
+
+# ---- model (OLMo2 cascade, matches trainer) with fault hooks ----
+FAULT = {'fnoise': 0.0, 'fbnoise': 0.0}
+
+class RMSNorm(nn.Module):
+ def __init__(self, C, eps=1e-6):
+ super().__init__(); self.g = nn.Parameter(torch.ones(C)); self.eps = eps
+ def forward(self, x):
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.g
+
+class SwiGLU(nn.Module):
+ def __init__(self, C):
+ super().__init__()
+ h = ((8 * C // 3) + 63) // 64 * 64
+ self.w1 = nn.Linear(C, h, bias=False); self.w3 = nn.Linear(C, h, bias=False)
+ self.w2 = nn.Linear(h, C, bias=False)
+ self.register_buffer('ggain', torch.ones(h), persistent=False) # gilbert mismatch
+ def forward(self, x):
+ return self.w2(F.silu(self.w1(x)) * self.w3(x) * self.ggain)
+
+class Olmo2Attn(nn.Module):
+ def __init__(self, C, H, T):
+ super().__init__()
+ self.H, self.hd = H, C // H
+ self.qkv = nn.Linear(C, 3 * C, bias=False); self.proj = nn.Linear(C, C, bias=False)
+ self.qn, self.kn = RMSNorm(C), RMSNorm(C)
+ inv = 1.0 / (500000.0 ** (torch.arange(0, self.hd, 2).float() / self.hd))
+ fr = torch.outer(torch.arange(T).float(), inv)
+ self.register_buffer('rc', fr.cos(), persistent=False)
+ self.register_buffer('rs', fr.sin(), persistent=False)
+ self.register_buffer('fr', fr, persistent=False)
+ def rope(self, x):
+ Tn = x.shape[2]
+ x1, x2 = x[..., ::2], x[..., 1::2]
+ c, s = self.rc[None, None, :Tn], self.rs[None, None, :Tn]
+ return torch.stack((x1 * c - x2 * s, x1 * s + x2 * c), dim=-1).flatten(-2)
+ def forward(self, x):
+ Bn, Tn, C = x.shape
+ q, k, v = self.qkv(x).split(C, dim=2)
+ q, k = self.qn(q), self.kn(k)
+ q = self.rope(q.view(Bn, Tn, self.H, self.hd).transpose(1, 2))
+ k = self.rope(k.view(Bn, Tn, self.H, self.hd).transpose(1, 2))
+ v = v.view(Bn, Tn, self.H, self.hd).transpose(1, 2)
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
+ return self.proj(y.transpose(1, 2).contiguous().view(Bn, Tn, C))
+
+class Olmo2Block(nn.Module):
+ def __init__(self, C, H, T):
+ super().__init__()
+ self.attn = Olmo2Attn(C, H, T); self.ff = SwiGLU(C)
+ self.na, self.nf = RMSNorm(C), RMSNorm(C)
+ def forward(self, z):
+ z = z + self.na(self.attn(z))
+ out = z + self.nf(self.ff(z))
+ if FAULT['fnoise'] > 0:
+ out = out * (1.0 + FAULT['fnoise'] * torch.randn_like(out))
+ return out
+
+ck = torch.load(a.ckpt, map_location=dev, weights_only=False)
+cfg = ck['config']; C, H, L = cfg['C'], cfg['H'], cfg['L']
+tok = nn.Embedding(vocab, C).to(dev); tok.load_state_dict(ck['tok'])
+blocks0 = nn.ModuleList([Olmo2Block(C, H, T) for _ in range(L)]).to(dev)
+blocks0.load_state_dict(ck['blocks'], strict=False) # ggain buffers are non-persistent extras
+W_out0 = ck['wout'].to(dev)
+ln_f0 = RMSNorm(C).to(dev); ln_f0.load_state_dict(ck['lnf'])
+NBT = B * T
+
+def apply_fault(kind, sev):
+ """return (blocks, W_out, ln_f) with the fault applied; also sets FAULT dict."""
+ FAULT['fnoise'] = 0.0; FAULT['fbnoise'] = 0.0
+ bl = copy.deepcopy(blocks0); Wo = W_out0.clone(); lf = copy.deepcopy(ln_f0)
+ g = torch.Generator(device='cpu').manual_seed(11)
+ if kind == 'clean':
+ pass
+ elif kind == 'wq':
+ bits = sev
+ with torch.no_grad():
+ for m in bl.modules():
+ if isinstance(m, nn.Linear):
+ s = m.weight.abs().max() / (2 ** (bits - 1) - 1)
+ m.weight.copy_(torch.round(m.weight / s) * s)
+ s = Wo.abs().max() / (2 ** (bits - 1) - 1)
+ Wo = torch.round(Wo / s) * s
+ elif kind == 'divmis':
+ with torch.no_grad():
+ for m in bl.modules():
+ if isinstance(m, RMSNorm):
+ m.g.mul_(1.0 + sev * torch.randn(m.g.shape, generator=g).to(dev))
+ lf.g.mul_(1.0 + sev * torch.randn(lf.g.shape, generator=g).to(dev))
+ elif kind == 'gilbert':
+ with torch.no_grad():
+ for m in bl.modules():
+ if isinstance(m, SwiGLU):
+ m.ggain.copy_(1.0 + sev * torch.randn(m.ggain.shape, generator=g).to(dev))
+ elif kind == 'rope':
+ with torch.no_grad():
+ for m in bl.modules():
+ if isinstance(m, Olmo2Attn):
+ d = sev * torch.randn(m.fr.shape, generator=g).to(dev)
+ m.rc.copy_((m.fr + d).cos()); m.rs.copy_((m.fr + d).sin())
+ elif kind == 'fnoise':
+ FAULT['fnoise'] = sev
+ elif kind == 'fbnoise':
+ FAULT['fbnoise'] = sev
+ return bl, Wo, lf
+
+def readout(z, Wo, lf): return lf(z) @ Wo.t()
+
+def ep_grad(bl, Wo, lf, x, y):
+ """single-sided fb EP grad on blocks params (matches trainer structure, K rounds)."""
+ with torch.no_grad():
+ z = tok(x)
+ zs_free = []
+ for b in bl: z = b(z); zs_free.append(z.clone())
+ # graphed free pass
+ ins, outs, zs = [], [], []
+ prev = zs_free[0] * 0 + tok(x).detach()
+ prev = tok(x).detach()
+ for b in bl:
+ i = prev.detach().requires_grad_(True)
+ o = b(i)
+ ins.append(i); outs.append(o); zs.append(o.detach())
+ prev = zs[-1]
+ d = [None] * L
+ for k in range(a.K):
+ zc = zs[L - 1].detach().requires_grad_(True)
+ ce = F.cross_entropy(readout(zc, Wo, lf).reshape(-1, vocab), y.reshape(-1))
+ d[L - 1] = (-a.beta * NBT * torch.autograd.grad(ce, zc)[0]).detach()
+ for l in range(L - 2, -1, -1):
+ d[l] = torch.autograd.grad(outs[l + 1], ins[l + 1], grad_outputs=d[l + 1])[0].detach()
+ if FAULT['fbnoise'] > 0:
+ for l in range(L):
+ d[l] = d[l] + FAULT['fbnoise'] * d[l].norm() / math.sqrt(d[l].numel()) * torch.randn_like(d[l])
+ last = (k + 1 == a.K)
+ prev = tok(x).detach()
+ n_ins, n_outs = [], []
+ for l in range(L):
+ i = prev.detach().requires_grad_(True)
+ o = bl[l](i)
+ n_ins.append(i); n_outs.append(o)
+ zs[l] = (o + d[l]).detach()
+ prev = zs[l]
+ ins, outs = n_ins, n_outs
+ E = 0.0
+ for z, o in zip(zs, outs): E = E + 0.5 * ((z.detach().float() - o.float()) ** 2).sum()
+ obj = E / (NBT * a.beta)
+ params = [p for p in bl.parameters()]
+ gs = torch.autograd.grad(obj, params, allow_unused=True)
+ return [g if g is not None else torch.zeros(1, device=dev) for g in gs]
+
+def bp_grad(bl, Wo, lf, x, y):
+ z = tok(x)
+ for b in bl: z = b(z)
+ ce = F.cross_entropy(readout(z, Wo, lf).reshape(-1, vocab), y.reshape(-1))
+ params = [p for p in bl.parameters()]
+ gs = torch.autograd.grad(ce, params, allow_unused=True)
+ return [g if g is not None else torch.zeros(1, device=dev) for g in gs]
+
+def val_ce(bl, Wo, lf, x, y):
+ with torch.no_grad():
+ z = tok(x)
+ for b in bl: z = b(z)
+ return float(F.cross_entropy(readout(z, Wo, lf).reshape(-1, vocab), y.reshape(-1)))
+
+def cos(ga, gb):
+ va = torch.cat([g.reshape(-1) for g in ga]); vb = torch.cat([g.reshape(-1) for g in gb])
+ return float((va @ vb) / (va.norm() * vb.norm() + 1e-30))
+
+SHARDS = {
+ 'A': [('wq', 8), ('wq', 6), ('wq', 4), ('divmis', 0.01), ('divmis', 0.03), ('divmis', 0.10)],
+ 'B': [('fnoise', 1e-3), ('fnoise', 3e-3), ('fnoise', 1e-2), ('rope', 0.01), ('rope', 0.03), ('rope', 0.10)],
+ 'C': [('gilbert', 0.01), ('gilbert', 0.03), ('gilbert', 0.10), ('fbnoise', 1e-2), ('fbnoise', 1e-1), ('fbnoise', 3e-1)],
+}
+
+batches = [get_batch() for _ in range(a.nb)]
+blc, Woc, lfc = apply_fault('clean', 0)
+clean_ce = sum(val_ce(blc, Woc, lfc, x, y) for x, y in batches) / a.nb
+gclean = [[g.cpu() for g in bp_grad(blc, Woc, lfc, x, y)] for x, y in batches]
+torch.cuda.empty_cache()
+print(f"[clean] val CE {clean_ce:.4f}", flush=True)
+
+for kind, sev in SHARDS[a.shard]:
+ torch.cuda.empty_cache()
+ bl, Wo, lf = apply_fault(kind, sev)
+ ce = sum(val_ce(bl, Wo, lf, x, y) for x, y in batches) / a.nb
+ c_self, c_clean = [], []
+ for i, (x, y) in enumerate(batches):
+ ge = ep_grad(bl, Wo, lf, x, y)
+ gbf = bp_grad(bl, Wo, lf, x, y)
+ c_self.append(cos(ge, gbf))
+ c_clean.append(cos([g.cpu() for g in ge], gclean[i]))
+ del ge, gbf; torch.cuda.empty_cache()
+ print(f"[{kind}={sev}] valCE {ce:.4f} (Δ{ce-clean_ce:+.4f}) | cos(EP,BP_faulted) {sum(c_self)/a.nb:.4f} | cos(EP,BP_clean) {sum(c_clean)/a.nb:.4f}", flush=True)
+print(f"DONE_{a.shard}", flush=True)