File size: 6,099 Bytes
e44ce27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env python3
"""mini-next: our mini implementation of the Flash-Next recipe on Whittle-16B.

Composition (each piece independently verifiable):
  1. MoE FFN     - carved 192-shared + 67x192 experts (see carve.py, CARVE_GATE)
  2. Hyper-connections - n residual streams with learned mixing, wrapped AROUND
     unmodified HF decoder layers via hooks. The layer computes out = h0 + T(h0)
     internally, so T(h0) = out - h0, and the HC update is
         H_i <- sum_j Ar[i,j] H_j + B_i * T(h0),   h0 = sum_i Am[k,i] H_i.
     Eq-14 identity init (HC paper, ICLR 2025): Am = e_{k mod n}, Ar = I, B = 1
     -> all streams stay equal to the standard residual, and the final row-sum's
     factor n cancels in the scale-invariant RMSNorm => logits IDENTICAL (HC_GATE).
  3. mHC constraint - Ar is parameterised through Sinkhorn-Knopp so residual
     mixing is doubly stochastic (can average, never amplify; the 3000x
     divergence fix from mHC).
  4. PLE - n-gram table with per-layer gated injection (train_ple.py checkpoint).
"""
import json, math
import torch
import torch.nn as nn
import torch.nn.functional as F


def sinkhorn(logits, iters=8):
    """Project exp(logits) onto (approx) doubly-stochastic via Sinkhorn-Knopp."""
    M = torch.exp(logits - logits.max())
    for _ in range(iters):
        M = M / (M.sum(-1, keepdim=True) + 1e-9)
        M = M / (M.sum(-2, keepdim=True) + 1e-9)
    return M


class HCState:
    def __init__(self): self.H = None
    def reset(self): self.H = None


class HyperConnections(nn.Module):
    """Static-matrix hyper-connections for L layers, expansion n (mHC-constrained)."""
    def __init__(self, n_layers, n=2, sinkhorn_iters=8):
        super().__init__()
        self.n, self.L, self.si = n, n_layers, sinkhorn_iters
        # Eq-14 identity init
        am = torch.zeros(n_layers, n)
        for k in range(n_layers): am[k, k % n] = 1.0
        self.Am_logit = nn.Parameter(torch.log(am + 1e-4))          # softmax -> ~e_{k mod n}
        eye = torch.eye(n).unsqueeze(0).repeat(n_layers, 1, 1)
        self.Ar_logit = nn.Parameter(torch.log(eye * 8.0 + 1.0))    # sinkhorn(exp) ~= I
        self.B = nn.Parameter(torch.ones(n_layers, n))               # write weights
        # Eq-14 init taken LITERALLY: while in identity mode, read/write use
        # exact index/add paths (zero arithmetic). The soft softmax/Sinkhorn
        # parameterisation is only engaged when training starts - in bf16 the
        # soft mix injects ~2^-8 error per layer and compounds to ~0.6 rel over
        # 44 layers, which is noise, not signal.
        self.identity_mode = True

    def release_identity(self):
        self.identity_mode = False

    def Am(self, k): return F.softmax(self.Am_logit[k], -1)          # non-neg, sums to 1
    def Ar(self, k): return sinkhorn(self.Ar_logit[k], self.si)      # doubly stochastic
    def read(self, k, H):
        if self.identity_mode:
            return H[k % self.n]
        # mixing coefficients are tiny (n, n^2); follow the streams' device -
        # layers span GPU boundaries under device_map.
        a = self.Am(k).to(dtype=H[0].dtype, device=H[0].device)
        return sum(a[i] * H[i] for i in range(self.n))
    def write(self, k, H, T_out):
        if self.identity_mode:
            return [H[i].to(T_out.device) + T_out for i in range(self.n)]
        R = self.Ar(k).to(dtype=H[0].dtype, device=H[0].device)
        b = self.B[k].to(dtype=H[0].dtype, device=H[0].device)
        return [sum(R[i, j] * H[j] for j in range(self.n)) + b[i] * T_out
                for i in range(self.n)]


def attach_hc(model, n=2):
    """Wrap every decoder layer of a HF qwen3_5(_moe) model in hyper-connections."""
    layers = model.model.layers
    hc = HyperConnections(len(layers), n=n)
    dev = next(layers[0].parameters()).device
    hc.to(dev).to(next(model.parameters()).dtype)
    st = HCState()
    inbuf = {}

    def mk_pre(k):
        def pre(mod, args, kwargs):
            h = kwargs.get("hidden_states", args[0] if args else None)
            if k == 0 or st.H is None:
                st.H = [h.clone() for _ in range(hc.n)]
            h0 = hc.read(k, [x.to(h.device) for x in st.H])
            inbuf[k] = h0
            if "hidden_states" in kwargs:
                kwargs["hidden_states"] = h0; return (args, kwargs)
            return ((h0,) + tuple(args[1:]), kwargs)
        return pre

    def mk_post(k, last):
        def post(mod, args, kwargs, out):
            o = out[0] if isinstance(out, tuple) else out
            if hc.identity_mode:
                # Eq-14 identity, taken to its bit-exact conclusion: with B=1,
                # Ar=I and equal streams, H_i <- H_i + (o - h0) == o. Assign
                # directly - zero extra arithmetic, so the wrapped model IS the
                # base model, bitwise. (T = o - h0 re-add costs one extra bf16
                # rounding per layer and flipped 5% of top-1s by layer 44.)
                inbuf.pop(k, None)
                st.H = [o for _ in range(hc.n)]
                new = o
                if last: st.reset()
                if isinstance(out, tuple): return (new,) + tuple(out[1:])
                return new
            T = o - inbuf.pop(k).to(o.device)   # layer may span a GPU boundary
            st.H = hc.write(k, [x.to(o.device) for x in st.H], T)
            new = sum(st.H) if last else st.H[0]
            # note: what we return only matters for the LAST layer (final norm
            # consumes it); intermediate layers are re-mixed by the next pre-hook.
            if last: st.reset()
            if isinstance(out, tuple): return (new,) + tuple(out[1:])
            return new
        return post

    hs = []
    for k, layer in enumerate(layers):
        hs.append(layer.register_forward_pre_hook(mk_pre(k), with_kwargs=True))
        hs.append(layer.register_forward_hook(mk_post(k, k == len(layers) - 1), with_kwargs=True))
    model._hc = hc
    model._hc_hooks = hs
    return hc


def detach_hc(model):
    for h in getattr(model, "_hc_hooks", []): h.remove()
    model._hc_hooks = []