Download demo_collapse.py from JakCwi/tethergate: direct link, hf CLI and curl.
- Browser
- Download file 3.65 kB
-
https://huggingface.co/JakCwi/tethergate/resolve/main/demo_collapse.py
- Command line
-
hf download hf://JakCwi/tethergate/demo_collapse.py
-
curl -L -o demo_collapse.py https://huggingface.co/JakCwi/tethergate/resolve/main/demo_collapse.py
3.65 kB
| # SPDX-License-Identifier: CC-BY-NC-4.0 | |
| # Copyright (c) 2026 Jakub Ćwirlej | |
| """Toy collapse benchmark (CPU, seconds). NOT evidence about large models — a reproducible | |
| illustration of the failure mode and of what the spring does about it. | |
| Task: h = x + g * branch(x), target y = x + A x. The branch is needed, but its random init | |
| is harmful, so early on the loss pushes a free gate towards zero; with plain SGD a closed | |
| gate also starves the branch of gradient. Variants: | |
| open : g = 1 (no gate) — reference | |
| free : g = sigmoid(beta * (s - theta)), theta learned, no floor, no spring | |
| tether : TetherGate (floor 1.0 for `ramp_start` steps, then decay; spring towards g_target) | |
| s = mean |branch(x)| per token (detached), the same for both gated variants. | |
| python demo_collapse.py [--steps 3000] [--seeds 0 1 2] | |
| """ | |
| import argparse | |
| import torch | |
| import torch.nn as nn | |
| from tethergate import TetherGate, tethergate_step | |
| def run(variant, seed, steps, d=32, lr=0.05, beta=6.0, init_scale=3.0, ramp_start=300, tau=300.0, | |
| g_target=0.5, wd=0.5): | |
| torch.manual_seed(seed) | |
| A = torch.randn(d, d) * 0.5 / d ** 0.5 | |
| branch = nn.Linear(d, d, bias=False) | |
| with torch.no_grad(): | |
| branch.weight.mul_(init_scale) | |
| theta = nn.Parameter(torch.tensor(0.0)) | |
| gate = TetherGate(beta=beta, g_target=g_target) if variant == 'tether' else None | |
| params = list(branch.parameters()) + ([theta] if variant == 'free' else []) + \ | |
| (list(gate.parameters()) if gate is not None else []) | |
| opt = torch.optim.SGD(params, lr=lr) | |
| gen = torch.Generator().manual_seed(1000 + seed) | |
| log = [] | |
| for step in range(steps): | |
| x = torch.randn(256, d, generator=gen) | |
| u = branch(x) | |
| s = u.detach().abs().mean(-1, keepdim=True) | |
| if variant == 'open': | |
| g = torch.ones_like(s) | |
| elif variant == 'free': | |
| g = torch.sigmoid((beta * (s - theta)).clamp(-8.0, 8.0)) | |
| else: | |
| g = gate(s) | |
| loss = ((x + g * u) - (x + x @ A.t())).pow(2).mean() | |
| opt.zero_grad(); loss.backward(); opt.step() | |
| if gate is not None: | |
| tethergate_step([gate], step, lr=lr, wd=wd, kappa_sleep=0.05, ramp_start=ramp_start, tau=tau) | |
| if step % max(1, steps // 6) == 0 or step == steps - 1: | |
| log.append((step, loss.item(), g.mean().item())) | |
| with torch.no_grad(): # held-out loss on fresh data | |
| x = torch.randn(4096, d, generator=torch.Generator().manual_seed(77)) | |
| u = branch(x) | |
| s = u.abs().mean(-1, keepdim=True) | |
| if variant == 'open': | |
| g = torch.ones_like(s) | |
| elif variant == 'free': | |
| g = torch.sigmoid((beta * (s - theta)).clamp(-8.0, 8.0)) | |
| else: | |
| gate.eval(); g = gate(s) | |
| held = float(((x + g * u) - (x + x @ A.t())).pow(2).mean()) | |
| return held, float(g.mean()), log | |
| if __name__ == '__main__': | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument('--steps', type=int, default=3000) | |
| ap.add_argument('--seeds', type=int, nargs='+', default=[0, 1, 2]) | |
| ap.add_argument('--verbose', action='store_true') | |
| a = ap.parse_args() | |
| print(f'steps={a.steps} seeds={a.seeds} (held-out MSE; mean gate on held-out data)') | |
| for variant in ('open', 'free', 'tether'): | |
| for seed in a.seeds: | |
| held, gm, log = run(variant, seed, a.steps) | |
| print(f' {variant:6s} seed {seed}: held-out MSE {held:.5f} | mean gate {gm:.3f}') | |
| if a.verbose: | |
| for st, l, g in log: | |
| print(f' step {st:5d} loss {l:.5f} gate {g:.3f}') | |