Download TECHNICAL.md from JakCwi/tethergate: direct link, hf CLI and curl.
- Browser
- Download file 4.04 kB
-
https://huggingface.co/JakCwi/tethergate/resolve/main/TECHNICAL.md
- Command line
-
hf download hf://JakCwi/tethergate/TECHNICAL.md
-
curl -L -o TECHNICAL.md https://huggingface.co/JakCwi/tethergate/resolve/main/TECHNICAL.md
TetherGate — how it works
The problem
A learned gate scales a residual branch: out = x + g · branch(x). When the loss does not pay for keeping the branch open — it hurts at initialisation, or its benefit comes later — the optimizer slowly pushes g to zero and the branch stops contributing. Two drifts cause it:
- early in training, the control signal
sdrifts below a thresholdthetathat has not moved yet; - later, the threshold itself drifts upwards, because the loss-only equilibrium moves towards "closed".
The mechanism
Forward (TetherGate.forward, differentiable):
g_raw = sigmoid( clamp( beta · (s − theta), arg_min, +8 ) )
g = F + (1 − F) · g_raw
betais fixed;thetais the only learned parameter.F(bufferfloor) is an affine floor: withF = 1the gate is fully open ("asleep") and theta receives no loss gradient; asFdecays,g_rawtakes over. Forward and backward use the same formula — the floor is not a backward-only trick.- In
train()mode the module updatesema_s(buffer) with the mean ofs(no_grad, no Python branching on tensors, sotorch.compilecan trace the forward). The first observation initialises the EMA (it starts as NaN). - With
detach_signal=True(default)sis detached: the branch cannot learn to steer its own gate.
Gradient and clamp. d g / d theta = −(1 − F) · beta · σ'(a) for a = beta·(s − theta) inside [arg_min, +8], and exactly 0 outside it (the clamp passes no gradient). Inside the range the gradient stays alive even close to the floor; outside it the only force on theta is the spring.
After each optimizer step (tethergate_step, no_grad, outside the graph):
F = floor_schedule(step) # 1 until ramp_start, then exp or cos decay to f_min
theta* = EMA(s) − logit(g_target) / beta # relative anchor (per gate, or the mean over gates)
k = kappa_sleep if F == 1 else lr · wd
theta ← theta − k · (theta − theta*)
- The anchor is relative: it follows the signal's own drift, so the spring removes only the common one-sided drift; the gate's mean opening settles near
g_targetregardless of the learning rate (the LR sets the time constant), while the per-token selectivity driven bysremains. anchor='global': onetheta*= mean over all gates (gates may differentiate around it).anchor='local': each gate to its owntheta*.- Gates whose EMA is still NaN are skipped (also for the floor update).
Choosing wd
Under a steady closing push the optimizer moves theta by about lr per step (Adam), while the spring pulls it back by lr · wd · (theta − theta*). Theta then settles about 1/wd from its anchor, whatever the LR, and the gate argument shifts by about beta / wd. With the default wd = 20 and beta = 6 that is 0.3: the gate stays near g_target. With wd = 0.5 a sustained push could shift it by 12, i.e. towards the floor; our long production run used wd = 0.5, which gives the loss much more freedom — with a small wd, monitor the mean gate opening. Stability requires lr · wd ≪ 1.
Floor schedule
mode='exp':F = f_min + (1 − f_min) · exp(−(step − ramp_start)/tau)— approachesf_minasymptotically.mode='cos': cosine from 1 atramp_starttof_minatramp_end.- Driven by the training step only, never by the loss.
State, determinism, cost
- State:
theta(parameter),floor,ema_s(buffers, saved instate_dict). - Deterministic given the same inputs (no randomness in the module).
- Cost: one sigmoid per gate element;
tethergate_stepreads each EMA as a Python float (one device sync per gate per step).
Tests
test_tethergate.py checks: the forward floor; a live gradient below the floor; zero gradient beyond the clamp and recovery through the spring; detach_signal; EMA updates only in train(); the spring reaching its target; an equilibrium independent of the LR; global vs local anchor.