Akahsizrr commited on
Commit
fe7c66d
·
verified ·
1 Parent(s): 7b8f78f

Upload folder using huggingface_hub

Browse files
Files changed (7) hide show
  1. README.md +16 -0
  2. config.json +38 -0
  3. gdn.py +273 -0
  4. model.safetensors +3 -0
  5. model4.py +597 -0
  6. segmm.py +393 -0
  7. tokenizer.json +0 -0
README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ ---
4
+ # Foundry-4
5
+
6
+ Kernel-specialized hybrid LM: 5.02B total / ~0.58B active params.
7
+ 18 GDN (gated DeltaNet, fla) + 6 RoPE attention layers, 384-expert
8
+ sigmoid-routed dropless MoE (top-8, packed Triton segmm), KernelGram
9
+ n-gram tables, 4-iteration shared-weight latent ponder loop, JEV-style
10
+ judge head.
11
+
12
+ Trained 2000 steps on a curated kernel-optimization corpus
13
+ (CUDA/Triton episodes with measured RESULT/BEST/HISTORY feedback) plus
14
+ GPU-verified synthetic optimization ladders (synth4.py).
15
+
16
+ Code: model4.py / gdn.py / segmm.py in this repo.
config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architecture": "Foundry4",
3
+ "config": {
4
+ "vocab_size": 65536,
5
+ "n_specials": 16,
6
+ "d_model": 1280,
7
+ "n_layers": 24,
8
+ "n_heads": 10,
9
+ "head_dim": 128,
10
+ "seq_len": 4096,
11
+ "dropout": 0.0,
12
+ "rope_theta": 10000.0,
13
+ "qk_norm": true,
14
+ "attn_every": 4,
15
+ "gdn_heads": 10,
16
+ "gdn_head_k": 128,
17
+ "gdn_head_v": 128,
18
+ "gdn_chunk": 32,
19
+ "n_experts": 384,
20
+ "expert_hidden": 128,
21
+ "moe_topk": 8,
22
+ "shared_hidden": 640,
23
+ "lb_weight": 0.01,
24
+ "kg_buckets": 262144,
25
+ "kg_dim": 80,
26
+ "kg_max_n": 4,
27
+ "kg_probes": 2,
28
+ "ponder_iters": 4,
29
+ "ponder_hidden": 2560,
30
+ "ponder_iter_weight_decay": 1.0,
31
+ "judge_hidden": 512,
32
+ "use_mtp": true,
33
+ "mtp_weight": 0.05,
34
+ "moe_ckpt": false
35
+ },
36
+ "trained_steps": 2000,
37
+ "dtype": "bfloat16"
38
+ }
gdn.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gated DeltaNet mixer for Foundry-4.
2
+
3
+ Recurrence (per head), GDN convention:
4
+
5
+ S_t = exp(g_t) * (I - beta_t k_t k_t^T) S_{t-1} + beta_t k_t v_t^T
6
+ o_t = q_t^T S_t
7
+
8
+ with S in R^{dk x dv} — keys address, values retrieved (o ~ v when
9
+ q ~ k). g <= 0 is log-decay per head per step, beta in (0,1) the write
10
+ strength.
11
+
12
+ Implementations:
13
+
14
+ - ``gdn_sequential`` — per-token recurrence. Obviously correct oracle
15
+ for the unit test; too slow for training.
16
+ - ``gdn_chunk`` — chunkwise WY form (Yang et al. 2024, "Gated Delta
17
+ Networks"). All within-chunk work is batched bmms + one triangular
18
+ solve; the only sequential loop is the inter-chunk state scan over
19
+ T/C chunks of cheap [dk x dv] ops.
20
+
21
+ Derivation (verified against the N=2 expansion by hand, and against
22
+ gdn_sequential in test_gdn.py):
23
+
24
+ within chunk, G_i = sum_{j<=i} g_j, Ghat_i = G_i - G_N (<= 0)
25
+ W = beta * K, U = beta * V
26
+ M[i,j] = exp(G_i - G_j) for i >= j, else 0
27
+ A_k[i,l] = beta_l <k_i, k_l> for i > l (strict lower)
28
+ A_v[i,j] = exp(G_i - G_j) beta_i <k_i, k_j> for i > j (strict lower)
29
+ Kt = (I + A_k)^{-1} K transformed keys (forward solve —
30
+ prefix property: leading submatrix
31
+ is the restricted solve)
32
+ Ut = (I + A_v)^{-1} U transformed values (decayed)
33
+ Kd = K * exp(G_N - G_j) decay-safe anti-decayed keys
34
+
35
+ removal product: prod_{j<=i}(I - w_j k_j^T) = I - W^T Kt (prefix
36
+ property lets the same Kt serve every intra-chunk position)
37
+
38
+ o_i = e^{G_i} q_i S_in
39
+ - e^{G_i} sum_{l<=i} <q_i, W_l> (Kt_l S_in)
40
+ + sum_{j<=i} M[i,j] <q_i, k_j> Ut_j
41
+
42
+ S_out = e^{G_N} ( S_in - W^T (Kt S_in) ) + Kd^T Ut
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import os
47
+
48
+ import torch
49
+ import torch.nn as nn
50
+ import torch.nn.functional as F
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # sequential oracle
55
+ # ---------------------------------------------------------------------------
56
+
57
+ def gdn_sequential(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
58
+ g: torch.Tensor, beta: torch.Tensor,
59
+ S0: torch.Tensor | None = None):
60
+ """q,k [B,T,H,dk]; v [B,T,H,dv]; g,beta [B,T,H] -> (o [B,T,H,dv], S).
61
+
62
+ fp32 oracle — the unit test compares gdn_chunk against this.
63
+ """
64
+ B, T, H, dk = q.shape
65
+ dv = v.shape[-1]
66
+ S = (S0 if S0 is not None else
67
+ q.new_zeros(B, H, dk, dv)).float()
68
+ o = q.new_zeros(B, T, H, dv, dtype=torch.float32)
69
+ qf, kf, vf = q.float(), k.float(), v.float()
70
+ gf, bf = g.float(), beta.float()
71
+ for t in range(T):
72
+ gt = gf[:, t].view(B, H, 1, 1).exp()
73
+ bt = bf[:, t].view(B, H, 1, 1)
74
+ kt, vt, qt = kf[:, t], vf[:, t], qf[:, t]
75
+ kTS = torch.einsum("bhd,bhdv->bhv", kt, S) # k^T S
76
+ S = gt * (S - bt * torch.einsum("bhd,bhv->bhdv", kt, kTS)) \
77
+ + bt * torch.einsum("bhd,bhv->bhdv", kt, vt)
78
+ o[:, t] = torch.einsum("bhd,bhdv->bhv", qt, S)
79
+ return o, S
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # chunked WY implementation
84
+ # ---------------------------------------------------------------------------
85
+
86
+ def gdn_chunk(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor,
87
+ g: torch.Tensor, beta: torch.Tensor,
88
+ S0: torch.Tensor | None = None, chunk_size: int = 32,
89
+ matmul_dtype: torch.dtype | None = None):
90
+ """Chunkwise gated delta rule, vectorized across chunks.
91
+
92
+ Only the [dk x dv] state scan is sequential (T/C cheap iterations).
93
+ Gram/output matmuls run in ``matmul_dtype`` (bf16 on GPU = tensor
94
+ cores); the triangular solves and the state recurrence stay fp32.
95
+ """
96
+ B, T, H, dk = q.shape
97
+ dv = v.shape[-1]
98
+ C = chunk_size
99
+ NC = (T + C - 1) // C
100
+ pad = NC * C - T
101
+ if pad: # pad tail chunk
102
+ zp = lambda t: F.pad(t, (0, 0, 0, 0, 0, pad)) # [B,T,H,*]
103
+ q, k, v = zp(q), zp(k), zp(v)
104
+ g = F.pad(g, (0, 0, 0, pad)) # g=0 no decay
105
+ beta = F.pad(beta, (0, 0, 0, pad))
106
+
107
+ mdt = matmul_dtype or torch.float32 # big matmuls
108
+ dt = torch.float32 # solves + state
109
+ dev = q.device
110
+ # -> [B,H,NC,C,*]
111
+ qc = q.float().view(B, NC, C, H, dk).permute(0, 3, 1, 2, 4)
112
+ kc = k.float().view(B, NC, C, H, dk).permute(0, 3, 1, 2, 4)
113
+ vc = v.float().view(B, NC, C, H, dv).permute(0, 3, 1, 2, 4)
114
+ gc = g.float().view(B, NC, C, H).permute(0, 3, 1, 2)
115
+ bc = beta.float().view(B, NC, C, H).permute(0, 3, 1, 2)
116
+
117
+ G = gc.cumsum(-1) # [B,H,NC,C]
118
+ Gh = G - G[..., -1:] # <=0
119
+ EG = Gh.exp() # e^{Ghat_i}
120
+ EGN = G[..., -1].exp() # e^{G_N} [B,H,NC]
121
+
122
+ tril = torch.tril(torch.ones(C, C, device=dev, dtype=torch.bool))
123
+ tril_m1 = torch.tril(torch.ones(C, C, device=dev, dtype=torch.bool),
124
+ diagonal=-1)
125
+
126
+ # M[i,j] = e^{G_i - G_j} on i >= j (<=1, stable)
127
+ M = torch.where(tril, (G.unsqueeze(-1) - G.unsqueeze(-2)).exp(),
128
+ G.new_zeros(()))
129
+ # ---- fp32 triangular solves (bounded by chunk size C) --------------
130
+ kbc32 = kc * bc.unsqueeze(-1) # W = beta k
131
+ vbc32 = vc * bc.unsqueeze(-1) # U = beta v
132
+ A_k = torch.where(tril_m1, kc @ kbc32.transpose(-1, -2),
133
+ kc.new_zeros(())) # beta_l<k_i,k_l>
134
+ A_v = torch.where(tril_m1, (kbc32 @ kc.transpose(-1, -2)) * M,
135
+ kc.new_zeros(())) # e^{Gi-Gj} b_i<k_i,k_j>
136
+ eye = torch.eye(C, device=dev, dtype=dt).view(
137
+ 1, 1, 1, C, C).expand(B, H, NC, C, C)
138
+ Kt = torch.linalg.solve_triangular(eye + A_k, kc, upper=False)
139
+ Ut = torch.linalg.solve_triangular(eye + A_v, vbc32, upper=False)
140
+
141
+ # ---- batch the matmul dtype: Gram rows, outputs, Kd----------------
142
+ kbc = kbc32.to(mdt)
143
+ Ut_m = Ut.to(mdt)
144
+ qc_m, kc_m = qc.to(mdt), kc.to(mdt)
145
+ M_m = M.to(mdt)
146
+ Kd = kc * (G[..., -1:] - G).exp().unsqueeze(-1) # e^{G_N-G_j} k
147
+ Kd_m = Kd.to(mdt)
148
+ EG_m, EGN_m = EG.to(mdt), EGN.to(mdt)
149
+ QW = torch.where(tril, qc_m @ kbc.transpose(-1, -2),
150
+ qc_m.new_zeros(())) # <q_i,W_l> l<=i
151
+ QWd = QW * EG_m.unsqueeze(-1) # e^{Ghat_i} rows
152
+ QK_m = (qc_m @ kc_m.transpose(-1, -2)) * M_m # tril by M
153
+ o_loc_all = QK_m @ Ut_m # batchable term
154
+
155
+ # scan states over chunks: o_inter needs S_in, so sequential here.
156
+ S = (S0.float() if S0 is not None else
157
+ q.new_zeros(B, H, dk, dv, dtype=dt))
158
+ o = q.new_zeros(B, H, NC, C, dv, dtype=dt)
159
+ for c in range(NC):
160
+ KS_c = Kt[:, :, c].to(mdt) @ S.to(mdt) # Kt_l S_in [C,dv]
161
+ o_in = (qc_m[:, :, c] * EG_m[:, :, c].unsqueeze(-1)) @ S.to(mdt) \
162
+ - QWd[:, :, c] @ KS_c
163
+ o_in = o_in * EGN_m[:, :, c].view(B, H, 1, 1)
164
+ o[:, :, c] = (o_in + o_loc_all[:, :, c]).float()
165
+ S = EGN[:, :, c].view(B, H, 1, 1) * (
166
+ S - kbc[:, :, c].transpose(-1, -2).float() @ KS_c.float()) \
167
+ + (Kd_m[:, :, c].transpose(-1, -2) @ Ut_m[:, :, c]).float()
168
+
169
+ o = o.permute(0, 2, 3, 1, 4).reshape(B, NC * C, H, dv)[:, :T]
170
+ return o.to(q.dtype), S
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # nn.Module: causal conv1d + projections + gated delta + output gate
175
+ # (the FLA/Qwen3-Next layer shape: short conv on qkv, per-head decay/beta,
176
+ # normalized output gate)
177
+ # ---------------------------------------------------------------------------
178
+
179
+ # fla provides the production Triton kernel (verified vs gdn_sequential
180
+ # in test_gdn: rel ~1e-6 on ROCm). Fall back to gdn_chunk elsewhere.
181
+ try:
182
+ from fla.ops.gated_delta_rule import chunk_gated_delta_rule as _fla_gdr
183
+ except Exception:
184
+ _fla_gdr = None
185
+
186
+
187
+ class GatedDeltaNet(nn.Module):
188
+ """Linear-attention mixer. h: [B,T,d_model] -> [B,T,d_model]."""
189
+
190
+ def __init__(self, d_model: int, n_heads: int, head_k: int = 128,
191
+ head_v: int = 128, conv_k: int = 4,
192
+ chunk_size: int = 32):
193
+ super().__init__()
194
+ self.h = n_heads
195
+ self.dk, self.dv = head_k, head_v
196
+ self.chunk_size = chunk_size
197
+ dk, dv = n_heads * head_k, n_heads * head_v
198
+ self.conv_k = conv_k
199
+ # q,k share dk side; v on dv side; conv covers all three
200
+ self.qkv = nn.Linear(d_model, 2 * dk + dv, bias=False)
201
+ self.conv = nn.Conv1d(2 * dk + dv, 2 * dk + dv, conv_k,
202
+ groups=2 * dk + dv, padding=conv_k - 1,
203
+ bias=True)
204
+ self.a_proj = nn.Linear(d_model, n_heads, bias=True) # decay pre-activ
205
+ self.b_proj = nn.Linear(d_model, n_heads, bias=True) # beta
206
+ self.g_out = nn.Linear(d_model, dv, bias=False) # output gate
207
+ self.out_norm = nn.GroupNorm(n_heads, dv) # per-head rms-ish
208
+ self.o_proj = nn.Linear(dv, d_model, bias=False)
209
+ nn.init.normal_(self.o_proj.weight, std=0.02)
210
+
211
+ def forward(self, x: torch.Tensor, S=None, use_cache=False,
212
+ conv_prev: torch.Tensor | None = None):
213
+ B, T, D = x.shape
214
+ qkv = self.qkv(x).transpose(1, 2) # [B,C,T]
215
+ if use_cache:
216
+ # decode path: the causal conv needs the previous k-1
217
+ # columns of qkv-projected input as its left context.
218
+ # conv_tail must be taken from the CONCATENATED context —
219
+ # a short chunk can carry fewer than k-1 new columns.
220
+ k1 = self.conv_k - 1
221
+ p_len = 0 if conv_prev is None else conv_prev.size(-1)
222
+ if conv_prev is not None:
223
+ qkv = torch.cat([conv_prev, qkv], -1)
224
+ conv_tail = qkv[..., -k1:]
225
+ # out[i] covers cat_in[i-k1 .. i]; new token t sits at cat
226
+ # index p_len+t -> want out[p_len : p_len+T]
227
+ qkv = self.conv(qkv)[..., p_len:p_len + T]
228
+ qkv = F.silu(qkv).transpose(1, 2) # [B,T,C]
229
+ else:
230
+ conv_tail = None
231
+ qkv = self.conv(qkv)[..., :T] # causal conv
232
+ qkv = F.silu(qkv).transpose(1, 2) # [B,T,C]
233
+ dk_t = self.h * self.dk
234
+ q = qkv[..., :dk_t].view(B, T, self.h, self.dk)
235
+ kk = qkv[..., dk_t:2 * dk_t].view(B, T, self.h, self.dk)
236
+ vv = qkv[..., 2 * dk_t:].view(B, T, self.h, self.dv)
237
+ # F.normalize upcasts to fp32 under autocast — fla's triton
238
+ # kernels need all main operands in ONE dtype, so normalize in
239
+ # fp32 for accuracy then cast back to the input dtype.
240
+ kk = F.normalize(kk.float(), p=2, dim=-1).to(vv.dtype)
241
+ a = self.a_proj(x) # [B,T,H]
242
+ b = self.b_proj(x)
243
+ g_log = -F.softplus(a).clamp(1e-4, 8.0) # log decay <=0
244
+ beta = torch.sigmoid(b).clamp(1e-4, 1 - 1e-4)
245
+ if _fla_gdr is not None and x.is_cuda:
246
+ # fla accepts bf16 operands + fp32 gates on ROCm (verified —
247
+ # the earlier fp32-only workaround was stale). GDN_FP32=1
248
+ # forces the whole call fp32 — fallback if bf16 fla
249
+ # instability is ever suspected in a training divergence.
250
+ if os.environ.get("GDN_FP32") == "1":
251
+ q, kk, vv, beta = (q.float(), kk.float(), vv.float(),
252
+ beta.float())
253
+ res = _fla_gdr(q, kk, vv,
254
+ g=g_log.float(), beta=beta, scale=1.0,
255
+ initial_state=(S.to(q.dtype) if S is not None
256
+ else None),
257
+ output_final_state=use_cache)
258
+ o = res[0].to(x.dtype)
259
+ S = res[1] if (use_cache and len(res) > 1) else None
260
+ else:
261
+ mdt = x.dtype if x.dtype in (torch.bfloat16,
262
+ torch.float16) else None
263
+ o, S = gdn_chunk(q, kk, vv, g_log, beta, S0=S,
264
+ chunk_size=self.chunk_size,
265
+ matmul_dtype=mdt)
266
+ # per-head GroupNorm over dv: reshape so channels = H*dv
267
+ o = self.out_norm(o.reshape(B * T, self.h * self.dv, 1))
268
+ o = o.reshape(B, T, -1)
269
+ o = o * torch.sigmoid(self.g_out(x))
270
+ out = self.o_proj(o)
271
+ if use_cache:
272
+ return out, S, conv_tail
273
+ return out
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e9798dedae530810fc606a77ad8cbb31763700e3927d20b660d69398af55049
3
+ size 10202500036
model4.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Foundry-4: sparse hybrid kernel-optimization model.
2
+
3
+ Architecture (see DESIGN notes at bottom):
4
+
5
+ - Embedding + KernelGram n-gram memory (hashed bigram..5-gram tables,
6
+ zero-init gates — proven on Foundry-2/3 to carry ~half the val loss).
7
+ - Hybrid trunk: GatedDeltaNet linear-attention blocks everywhere except
8
+ every ``attn_every``-th layer which is full causal attention (RoPE +
9
+ QK-norm + SDPA). Linear layers handle the stream; attention layers
10
+ give exact long-range recall for pointer names / block sizes.
11
+ - Sparse MoE FFN on every block: 1 shared expert + N fine-grained
12
+ routed experts, top-k sigmoid router (DeepSeekMoE-style), Switch-style
13
+ load-balance aux loss.
14
+ - PonderLoop: a recurrent block (shared weights, iteration embedding)
15
+ applied K times to the trunk output. Per-iteration deep supervision
16
+ (weighted CE on each iteration's logits) so every loop step is a
17
+ usable state — the substrate for latent "try many paths" reasoning
18
+ and later RL-driven non-lazy search.
19
+ - JudgmentHead (Jev-like): small MLP over the pooled latent state that
20
+ emits typed verdicts — p(compiles), p(correct), expected speedup,
21
+ halt. In pretrain it is self-supervised on a signal we already have:
22
+ per-iteration LM improvement. In RL it is re-pointed at real harness
23
+ outcomes and becomes the best-of-loop selector / idea filter.
24
+ - Tied 65k-token embedding + chunked CE scoring (never materialise
25
+ [B,T,V] logits at once), optional MTP head.
26
+
27
+ Invariants (same discipline as Foundry-1/2 — do not break):
28
+ 1. Trunk is causal. Attention uses the explicit -inf mask; GDN is
29
+ causal by construction; the ponder block uses the same mask.
30
+ 2. Rewards/judgments are measured or self-supervised, never faked.
31
+ 3. save_checkpoint stage->verify->copy->verify on every write.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import math
36
+ from dataclasses import dataclass
37
+
38
+ import torch
39
+ import torch.nn as nn
40
+ import torch.nn.functional as F
41
+ import torch.utils.checkpoint
42
+
43
+ from gdn import GatedDeltaNet
44
+ from segmm import seg_mm, wscatter
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # config
49
+ # ---------------------------------------------------------------------------
50
+
51
+ @dataclass
52
+ class Foundry4Config:
53
+ vocab_size: int = 65536 # bpe2_65k_v2 + specials 0..15
54
+ n_specials: int = 16
55
+ d_model: int = 1280
56
+ n_layers: int = 24
57
+ n_heads: int = 10 # attn heads, head_dim 128
58
+ head_dim: int = 128
59
+ seq_len: int = 4096
60
+ dropout: float = 0.0
61
+ rope_theta: float = 10000.0
62
+ qk_norm: bool = True
63
+ attn_every: int = 4 # layer i%4==3 -> full attention
64
+ # gated deltanet
65
+ gdn_heads: int = 10
66
+ gdn_head_k: int = 128
67
+ gdn_head_v: int = 128
68
+ gdn_chunk: int = 32 # C=64 can overflow the WY solve (2^C growth)
69
+ # sparse MoE
70
+ n_experts: int = 384
71
+ expert_hidden: int = 128
72
+ moe_topk: int = 8
73
+ shared_hidden: int = 640 # always-on shared expert
74
+ lb_weight: float = 0.01
75
+ # kernelgram
76
+ kg_buckets: int = 262144
77
+ kg_dim: int = 80
78
+ kg_max_n: int = 4 # n-gram orders 2..4
79
+ kg_probes: int = 2 # salted probes per order
80
+ # ponder loop (latent reasoning)
81
+ ponder_iters: int = 4
82
+ ponder_hidden: int = 2560 # dense SwiGLU inside the loop block
83
+ ponder_iter_weight_decay: float = 1.0 # loss weight ramp exponent
84
+ # judgment head
85
+ judge_hidden: int = 512
86
+ # mtp
87
+ use_mtp: bool = True
88
+ mtp_weight: float = 0.05
89
+ # memory
90
+ moe_ckpt: bool = False # recompute MoE dispatch in backward
91
+
92
+
93
+ def beast_config() -> Foundry4Config:
94
+ """~5.1B total / ~570M active. The target."""
95
+ return Foundry4Config()
96
+
97
+
98
+ def dev_config() -> Foundry4Config:
99
+ """~700M total / ~120M active — smoke + kernel-tuning config."""
100
+ return Foundry4Config(
101
+ d_model=768, n_layers=12, n_heads=6, head_dim=128,
102
+ gdn_heads=6, gdn_head_k=128, gdn_head_v=128,
103
+ n_experts=32, expert_hidden=192, moe_topk=4,
104
+ shared_hidden=512, seq_len=2048,
105
+ kg_buckets=65536, kg_dim=64, kg_max_n=4, kg_probes=2,
106
+ ponder_iters=3, ponder_hidden=1536,
107
+ judge_hidden=256)
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # KernelGram — identical semantics to model2 (proven); kept self-contained
112
+ # ---------------------------------------------------------------------------
113
+
114
+ class KernelGram(nn.Module):
115
+ _C1 = 0x9E3779B1
116
+ _C2 = 0x85EBCA77
117
+ _SEEDS = (0x0, 0x9E3779B1, 0x85EBCA77, 0xC2B2AE3D)
118
+
119
+ def __init__(self, cfg: Foundry4Config):
120
+ super().__init__()
121
+ self.cfg = cfg
122
+ self.n_grams = cfg.kg_max_n - 1
123
+ self.n_probes = cfg.kg_probes
124
+ self.n_slots = self.n_grams * self.n_probes
125
+ self.tables = nn.ParameterList([
126
+ nn.Parameter(torch.randn(cfg.kg_buckets, cfg.kg_dim)
127
+ / math.sqrt(cfg.kg_dim))
128
+ for _ in range(self.n_slots)])
129
+ self.proj = nn.ParameterList([
130
+ nn.Parameter(torch.empty(cfg.kg_dim, cfg.d_model))
131
+ for _ in range(self.n_slots)])
132
+ for p in self.proj:
133
+ nn.init.normal_(p, std=0.02)
134
+ self.gates = nn.Parameter(torch.zeros(self.n_slots))
135
+
136
+ def _hashes(self, ids: torch.Tensor) -> list[torch.Tensor]:
137
+ B, T = ids.shape
138
+ out = []
139
+ for k_ in range(self.n_grams):
140
+ n = k_ + 2
141
+ for p in range(self.n_probes):
142
+ h = torch.zeros(B, T, dtype=torch.long, device=ids.device)
143
+ if p:
144
+ h[:, n - 1:] = self._SEEDS[p % len(self._SEEDS)]
145
+ for j in range(n):
146
+ tok = ids[:, j:T - n + 1 + j]
147
+ h[:, n - 1:] = (h[:, n - 1:] * self._C1
148
+ + tok * self._C2)
149
+ out.append(h % self.cfg.kg_buckets)
150
+ return out
151
+
152
+ def forward(self, ids, x_emb):
153
+ for s, (h, tab, pr) in enumerate(
154
+ zip(self._hashes(ids), self.tables, self.proj)):
155
+ g = torch.tanh(self.gates[s])
156
+ x_emb = x_emb + g * (F.embedding(h, tab) @ pr)
157
+ return x_emb
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Sparse MoE — shared expert + fine-grained routed experts (sigmoid router,
162
+ # DeepSeek-style top-k with weight renormalisation; bucketed bmm dispatch)
163
+ # ---------------------------------------------------------------------------
164
+
165
+ class SparseMoE(nn.Module):
166
+ def __init__(self, cfg: Foundry4Config):
167
+ super().__init__()
168
+ d, h = cfg.d_model, cfg.expert_hidden
169
+ E = cfg.n_experts
170
+ self.cfg = cfg
171
+ self.E, self.k = E, cfg.moe_topk
172
+ self.router = nn.Linear(d, E, bias=False)
173
+ self.w_g = nn.Parameter(torch.empty(E, d, h))
174
+ self.w_w = nn.Parameter(torch.empty(E, d, h))
175
+ self.w_o = nn.Parameter(torch.empty(E, h, d))
176
+ for w in (self.w_g, self.w_w, self.w_o):
177
+ nn.init.normal_(w, std=0.02)
178
+ # shared expert (always on)
179
+ self.sh_g = nn.Linear(d, cfg.shared_hidden, bias=False)
180
+ self.sh_w = nn.Linear(d, cfg.shared_hidden, bias=False)
181
+ self.sh_o = nn.Linear(cfg.shared_hidden, d, bias=False)
182
+
183
+ def _swiglu(self, x, wg, ww, wo):
184
+ return (F.silu(x @ wg) * (x @ ww)) @ wo
185
+
186
+ def _dispatch(self, xf, top_i, w):
187
+ """Packed-segment expert compute (segmm.py Triton grouped GEMM).
188
+ DROPLESS (causality invariant): every routed token keeps all k
189
+ experts — a capacity mask would couple position i's output to
190
+ future tokens' routing and break KV-cache semantics. Packed
191
+ layout: memory stays at Nk rows regardless of routing skew, and
192
+ there is no host<->device sync at all."""
193
+ N, d = xf.shape
194
+ counts = torch.bincount(
195
+ top_i.reshape(-1), minlength=self.E)
196
+ order = torch.argsort(top_i.reshape(-1)) # stable sort
197
+ flat_t = (order // self.k) # token ids
198
+ flat_w = w.reshape(-1)[order]
199
+ offs = torch.zeros(self.E + 1, dtype=torch.int64,
200
+ device=xf.device)
201
+ offs[1:] = torch.cumsum(counts, 0)
202
+ xs = xf[flat_t] # [Nk,d] packed
203
+ dt = xs.dtype
204
+ wg = self.w_g.to(dt) # cast: same-dtype
205
+ ww = self.w_w.to(dt) # tl.dot operands
206
+ wo = self.w_o.to(dt)
207
+ h1 = seg_mm(xs, wg, offs)
208
+ h2 = seg_mm(xs, ww, offs)
209
+ yp = seg_mm(F.silu(h1) * h2, wo, offs) # [Nk,d]
210
+ out = xf.new_zeros(N, d)
211
+ out.index_add_(0, flat_t,
212
+ (yp * flat_w.unsqueeze(-1)).to(out.dtype))
213
+ return out
214
+
215
+ def forward(self, x: torch.Tensor):
216
+ """x [B,T,d] -> out [B,T,d], lb_loss scalar."""
217
+ B, T, d = x.shape
218
+ xf = x.reshape(-1, d)
219
+ N = xf.size(0)
220
+
221
+ logits = self.router(xf) # [N,E]
222
+ scores = torch.sigmoid(logits)
223
+ top_s, top_i = scores.topk(self.k, dim=-1) # [N,k]
224
+ w = top_s / top_s.sum(-1, keepdim=True).clamp_min(1e-6)
225
+
226
+ if self.cfg.moe_ckpt and self.training and xf.requires_grad:
227
+ out = torch.utils.checkpoint.checkpoint(
228
+ self._dispatch, xf, top_i, w,
229
+ use_reentrant=False)
230
+ else:
231
+ out = self._dispatch(xf, top_i, w)
232
+
233
+ out = out + self.sh_o(F.silu(self.sh_g(xf)) * self.sh_w(xf))
234
+ out = out.view(B, T, d)
235
+
236
+ # Switch-style load balance on the full softmax
237
+ p = F.softmax(logits, -1)
238
+ f = torch.zeros_like(p).scatter_(
239
+ 1, top_i, 1.0 / self.k).mean(0)
240
+ lb = self.E * (f * p.mean(0)).sum()
241
+ return out, lb
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # attention mixer + block
246
+ # ---------------------------------------------------------------------------
247
+
248
+ class RMSNorm(nn.Module):
249
+ def __init__(self, d: int, eps: float = 1e-6):
250
+ super().__init__()
251
+ self.w = nn.Parameter(torch.ones(d))
252
+ self.eps = eps
253
+
254
+ def forward(self, x):
255
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True)
256
+ + self.eps) * self.w
257
+
258
+
259
+ def _rope_cos_sin(T, hd, theta, device):
260
+ half = hd // 2
261
+ fr = 1.0 / (theta ** (torch.arange(0, half, device=device,
262
+ dtype=torch.float32) / half))
263
+ t = torch.arange(T, device=device, dtype=torch.float32)
264
+ ang = torch.outer(t, fr)
265
+ return ang.cos(), ang.sin()
266
+
267
+
268
+ def _rope(x, cos, sin):
269
+ half = x.size(-1) // 2
270
+ x1, x2 = x[..., :half], x[..., half:]
271
+ c = cos[None, None]
272
+ s = sin[None, None]
273
+ return torch.cat([x1 * c - x2 * s, x1 * s + x2 * c], -1)
274
+
275
+
276
+ class Attention(nn.Module):
277
+ def __init__(self, cfg: Foundry4Config):
278
+ super().__init__()
279
+ d = cfg.d_model
280
+ self.h, self.hd = cfg.n_heads, cfg.head_dim
281
+ self.wq = nn.Linear(d, self.h * self.hd, bias=False)
282
+ self.wk = nn.Linear(d, self.h * self.hd, bias=False)
283
+ self.wv = nn.Linear(d, self.h * self.hd, bias=False)
284
+ self.wo = nn.Linear(self.h * self.hd, d, bias=False)
285
+ self.qn = RMSNorm(self.hd) if cfg.qk_norm else None
286
+ self.kn = RMSNorm(self.hd) if cfg.qk_norm else None
287
+
288
+ def forward(self, x, cos, sin, causal):
289
+ B, T, _ = x.shape
290
+ q = self.wq(x).view(B, T, self.h, self.hd).transpose(1, 2)
291
+ k = self.wk(x).view(B, T, self.h, self.hd).transpose(1, 2)
292
+ v = self.wv(x).view(B, T, self.h, self.hd).transpose(1, 2)
293
+ if self.qn is not None:
294
+ q, k = self.qn(q), self.kn(k)
295
+ q, k = _rope(q, cos, sin), _rope(k, cos, sin)
296
+ a = F.scaled_dot_product_attention(q, k, v, is_causal=True)
297
+ return self.wo(a.transpose(1, 2).reshape(B, T, -1))
298
+
299
+ def forward_cached(self, x, cos, sin, past=None):
300
+ """Decode-time path. past = (k, v) post-rope from earlier calls.
301
+ Returns (out, (k_all, v_all)) — the returned tensors become the
302
+ next call's `past`. cos/sin must cover only the NEW positions."""
303
+ B, T, _ = x.shape
304
+ q = self.wq(x).view(B, T, self.h, self.hd).transpose(1, 2)
305
+ k = self.wk(x).view(B, T, self.h, self.hd).transpose(1, 2)
306
+ v = self.wv(x).view(B, T, self.h, self.hd).transpose(1, 2)
307
+ if self.qn is not None:
308
+ q, k = self.qn(q), self.kn(k)
309
+ q, k = _rope(q, cos, sin), _rope(k, cos, sin)
310
+ if past is not None:
311
+ k = torch.cat([past[0], k], 2)
312
+ v = torch.cat([past[1], v], 2)
313
+ if past is None or T == k.size(2):
314
+ a = F.scaled_dot_product_attention(q, k, v, is_causal=True)
315
+ else:
316
+ P = k.size(2) - T
317
+ m = torch.ones(T, k.size(2), dtype=torch.bool,
318
+ device=x.device)
319
+ m[:, P:] = torch.tril(torch.ones(T, T, dtype=torch.bool,
320
+ device=x.device))
321
+ a = F.scaled_dot_product_attention(q, k, v, attn_mask=m)
322
+ return self.wo(a.transpose(1, 2).reshape(B, T, -1)), (k, v)
323
+
324
+
325
+ class Block(nn.Module):
326
+ """mixer (GDN or attention) + sparse MoE, pre-norm."""
327
+
328
+ def __init__(self, cfg: Foundry4Config, use_attn: bool):
329
+ super().__init__()
330
+ self.use_attn = use_attn
331
+ d = cfg.d_model
332
+ self.norm1 = RMSNorm(d)
333
+ self.norm2 = RMSNorm(d)
334
+ if use_attn:
335
+ self.mixer = Attention(cfg)
336
+ else:
337
+ self.mixer = GatedDeltaNet(
338
+ d, cfg.gdn_heads, cfg.gdn_head_k, cfg.gdn_head_v,
339
+ chunk_size=cfg.gdn_chunk)
340
+ self.ffn = SparseMoE(cfg)
341
+
342
+ def forward(self, h, cos, sin, causal):
343
+ if self.use_attn:
344
+ h = h + self.mixer(self.norm1(h), cos, sin, causal)
345
+ else:
346
+ h = h + self.mixer(self.norm1(h))
347
+ ff, lb = self.ffn(self.norm2(h))
348
+ h = h + ff
349
+ return h, lb
350
+
351
+ def forward_cached(self, h, cos, sin, cache):
352
+ """cache: dict — {'kv': (k,v)} for attn, {'S': state} for GDN."""
353
+ if self.use_attn:
354
+ o, kv = self.mixer.forward_cached(self.norm1(h), cos, sin,
355
+ cache.get("kv"))
356
+ cache["kv"] = kv
357
+ h = h + o
358
+ else:
359
+ o, S, ct = self.mixer(self.norm1(h), S=cache.get("S"),
360
+ use_cache=True,
361
+ conv_prev=cache.get("conv"))
362
+ cache["S"] = S
363
+ cache["conv"] = ct
364
+ h = h + o
365
+ ff, _ = self.ffn(self.norm2(h))
366
+ return h + ff
367
+
368
+
369
+ # ---------------------------------------------------------------------------
370
+ # PonderLoop — the latent reasoning core
371
+ # ---------------------------------------------------------------------------
372
+
373
+ class PonderBlock(nn.Module):
374
+ """One shared-weight recurrent step: causal attn + dense SwiGLU."""
375
+
376
+ def __init__(self, cfg: Foundry4Config):
377
+ super().__init__()
378
+ d = cfg.d_model
379
+ self.norm1 = RMSNorm(d)
380
+ self.norm2 = RMSNorm(d)
381
+ self.attn = Attention(cfg)
382
+ self.ff_g = nn.Linear(d, cfg.ponder_hidden, bias=False)
383
+ self.ff_w = nn.Linear(d, cfg.ponder_hidden, bias=False)
384
+ self.ff_o = nn.Linear(cfg.ponder_hidden, d, bias=False)
385
+ self.iter_emb = nn.Parameter(torch.zeros(cfg.ponder_iters, d))
386
+
387
+ def forward(self, h, it, cos, sin, causal):
388
+ h = h + self.iter_emb[it]
389
+ h = h + self.attn(self.norm1(h), cos, sin, causal)
390
+ x = self.norm2(h)
391
+ h = h + self.ff_o(F.silu(self.ff_g(x)) * self.ff_w(x))
392
+ return h
393
+
394
+ def forward_cached(self, h, it, cos, sin, past):
395
+ o, kv = self.attn.forward_cached(
396
+ self.norm1(h + self.iter_emb[it]), cos, sin, past)
397
+ h = h + self.iter_emb[it] + o
398
+ x = self.norm2(h)
399
+ return h + self.ff_o(F.silu(self.ff_g(x)) * self.ff_w(x)), kv
400
+
401
+
402
+ class JudgmentHead(nn.Module):
403
+ """Typed verdicts over the pooled latent state.
404
+
405
+ Outputs: p_compile, p_correct logits, log-speedup regression,
406
+ halt logit. Trained self-supervised in pretrain (per-iter LM
407
+ improvement); re-pointed at harness rewards in SFT/RL.
408
+ """
409
+
410
+ def __init__(self, cfg: Foundry4Config):
411
+ super().__init__()
412
+ d = cfg.d_model
413
+ self.net = nn.Sequential(
414
+ nn.Linear(d, cfg.judge_hidden), nn.SiLU(),
415
+ nn.Linear(cfg.judge_hidden, 4))
416
+
417
+ def forward(self, h): # h [B,T,d] -> [B,4]
418
+ pooled = h.mean(1)
419
+ return self.net(pooled)
420
+
421
+
422
+ # ---------------------------------------------------------------------------
423
+ # model
424
+ # ---------------------------------------------------------------------------
425
+
426
+ class Foundry4Model(nn.Module):
427
+ def __init__(self, cfg: Foundry4Config | None = None):
428
+ super().__init__()
429
+ self.cfg = cfg = cfg or Foundry4Config()
430
+ d = cfg.d_model
431
+ self.tok_emb = nn.Embedding(cfg.vocab_size, d)
432
+ self.kg = KernelGram(cfg)
433
+ self.blocks = nn.ModuleList([
434
+ Block(cfg, use_attn=((i + 1) % cfg.attn_every == 0))
435
+ for i in range(cfg.n_layers)])
436
+ self.norm = RMSNorm(d)
437
+ self.ponder = PonderBlock(cfg)
438
+ self.judge = JudgmentHead(cfg)
439
+ self.head = nn.Linear(d, cfg.vocab_size, bias=False)
440
+ self.head.weight = self.tok_emb.weight # tied
441
+ self.mtp = (nn.Linear(d, d, bias=False)
442
+ if cfg.use_mtp else None)
443
+ self.apply(self._init)
444
+
445
+ @staticmethod
446
+ def _init(m):
447
+ if isinstance(m, nn.Linear):
448
+ nn.init.normal_(m.weight, std=0.02)
449
+ elif isinstance(m, nn.Embedding):
450
+ nn.init.normal_(m.weight, std=0.02)
451
+
452
+ # -- trunk -------------------------------------------------------------
453
+
454
+ def trunk(self, ids):
455
+ x = self.kg(ids, self.tok_emb(ids))
456
+ dev = ids.device
457
+ T = ids.size(1)
458
+ cos, sin = _rope_cos_sin(T, self.cfg.head_dim,
459
+ self.cfg.rope_theta, dev)
460
+ h, lb = x, x.new_zeros(())
461
+ for blk in self.blocks:
462
+ h, l = blk(h, cos, sin, None)
463
+ lb = lb + l
464
+ return self.norm(h), lb
465
+
466
+ # -- forward: trunk -> ponder loop -> per-iter hidden states -----------
467
+
468
+ def forward(self, ids, return_aux=False):
469
+ cfg = self.cfg
470
+ h, lb = self.trunk(ids)
471
+ T = ids.size(1)
472
+ cos, sin = _rope_cos_sin(T, cfg.head_dim, cfg.rope_theta,
473
+ ids.device)
474
+ h_iters, judge_per_iter = [h], []
475
+ for it in range(cfg.ponder_iters):
476
+ h = self.ponder(h, it, cos, sin, None)
477
+ h_iters.append(h)
478
+ if return_aux:
479
+ judge_per_iter.append(self.judge(h))
480
+ if return_aux:
481
+ # training path: h_iters drive chunked/fused CE — never
482
+ # materialise the [B,T,V] logits here (saves ~4GB + backward)
483
+ aux = {"lb_loss": lb * cfg.lb_weight,
484
+ "judge": torch.stack(judge_per_iter, 1),
485
+ "h_iters": h_iters}
486
+ if self.mtp is not None:
487
+ aux["mtp_h"] = self.mtp(h)
488
+ return None, aux
489
+ return self.head(h) # final iter
490
+
491
+ # -- cached decode ------------------------------------------------------
492
+
493
+ def new_cache(self):
494
+ return {"blocks": [{} for _ in self.blocks],
495
+ "ponder": [None] * self.cfg.ponder_iters,
496
+ "tail": None} # last kg_max_n-1 ids
497
+
498
+ def forward_cached(self, ids, pos0, cache):
499
+ """Decode/prefill for generation. ids [B,Tn] at absolute
500
+ positions pos0..pos0+Tn-1. Returns logits [B,Tn,V]; cache is
501
+ updated in place. Matches forward() token-for-token (causality
502
+ invariant), just incremental."""
503
+ cfg = self.cfg
504
+ B, Tn = ids.shape
505
+ dev = ids.device
506
+ # KernelGram needs the trailing n-1 ids for the n-gram window
507
+ tail = cache["tail"]
508
+ kg_in = ids if tail is None else torch.cat([tail, ids], 1)
509
+ x = self.kg(kg_in, self.tok_emb(kg_in))[:, -Tn:]
510
+ cache["tail"] = kg_in[:, -(cfg.kg_max_n - 1):]
511
+ cos, sin = _rope_cos_sin(pos0 + Tn, cfg.head_dim,
512
+ cfg.rope_theta, dev)
513
+ cos, sin = cos[pos0:], sin[pos0:]
514
+ h = x
515
+ for blk, bc in zip(self.blocks, cache["blocks"]):
516
+ h = blk.forward_cached(h, cos, sin, bc)
517
+ h = self.norm(h)
518
+ for it in range(cfg.ponder_iters):
519
+ h, cache["ponder"][it] = self.ponder.forward_cached(
520
+ h, it, cos, sin, cache["ponder"][it])
521
+ return self.head(h)
522
+
523
+
524
+ # ---------------------------------------------------------------------------
525
+ # parameter accounting + checkpointing (same discipline as model2)
526
+ # ---------------------------------------------------------------------------
527
+
528
+ def param_report(model: Foundry4Model) -> dict:
529
+ cfg = model.cfg
530
+ total = sum(p.numel() for p in model.parameters())
531
+
532
+ emb = cfg.vocab_size * cfg.d_model # tied
533
+ kg = sum(p.numel() for p in model.kg.parameters())
534
+ experts = cfg.n_layers * cfg.n_experts * 3 * cfg.d_model \
535
+ * cfg.expert_hidden
536
+
537
+ # active = everything except the cold (non-selected) routed-expert
538
+ # capacity. Embedding lookup counts once (tied head reuses it for
539
+ # logits, so it's active anyway).
540
+ routed = cfg.n_layers * cfg.moe_topk * 3 * cfg.d_model \
541
+ * cfg.expert_hidden
542
+ active = total - experts + routed
543
+ n_attn = cfg.n_layers // cfg.attn_every
544
+ n_gdn = cfg.n_layers - n_attn
545
+ return {"total": total, "active": active,
546
+ "experts": experts, "kg": kg, "emb": emb,
547
+ "n_attn": n_attn, "n_gdn": n_gdn}
548
+
549
+
550
+ def save_checkpoint(obj: dict, out_path) -> None:
551
+ """Stage-write-verify-copy-verify; survives flaky filesystems."""
552
+ import io
553
+ import shutil
554
+ import tempfile
555
+ from pathlib import Path
556
+
557
+ out_path = Path(out_path)
558
+ out_path.parent.mkdir(parents=True, exist_ok=True)
559
+ buf = io.BytesIO()
560
+ torch.save(obj, buf)
561
+ data = buf.getvalue()
562
+ with tempfile.NamedTemporaryFile(
563
+ dir=out_path.parent, delete=False) as f:
564
+ f.write(data)
565
+ tmp = Path(f.name)
566
+ # verify the staged file loads before it replaces anything
567
+ torch.load(tmp, map_location="cpu", weights_only=False)
568
+ shutil.copy2(tmp, out_path)
569
+ torch.load(out_path, map_location="cpu", weights_only=False)
570
+ tmp.unlink()
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # FLOP model for MFU accounting
575
+ # ---------------------------------------------------------------------------
576
+
577
+ def flops_per_token(cfg: Foundry4Config) -> float:
578
+ """Forward+backward FLOPs per token (~6N_active + attention terms)."""
579
+ d = cfg.d_model
580
+ n_attn = cfg.n_layers // cfg.attn_every
581
+ n_gdn = cfg.n_layers - n_attn
582
+ # dense per-token params: mixers + shared expert + topk experts +
583
+ # router + ponder(iters) + heads — times 6
584
+ mixer = n_attn * 4 * d * cfg.n_heads * cfg.head_dim \
585
+ + n_gdn * d * (4 * cfg.gdn_heads * cfg.gdn_head_k)
586
+ ffn = cfg.n_layers * 3 * d * (cfg.shared_hidden
587
+ + cfg.moe_topk * cfg.expert_hidden)
588
+ ponder = cfg.ponder_iters * (4 * d * cfg.n_heads * cfg.head_dim
589
+ + 3 * d * cfg.ponder_hidden)
590
+ heads = cfg.vocab_size * d # tied head proj
591
+ n_active = mixer + ffn + ponder + heads
592
+ # attention QK/AV term: ~4 * T * d * n_attn_heads-ish; and GDN chunk
593
+ # work ~= O(C * dk * dv) per token — lump as seq term:
594
+ seq_term = n_attn * 12 * cfg.seq_len * d \
595
+ + n_gdn * 2 * cfg.gdn_chunk * cfg.gdn_heads \
596
+ * cfg.gdn_head_v
597
+ return 6.0 * n_active + seq_term
segmm.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """segmm.py — segmented/grouped GEMM for dropless MoE dispatch.
2
+
3
+ Tokens are gathered into expert-sorted order once: xs [M, K] where the
4
+ rows for expert e occupy the contiguous slice [offs[e], offs[e+1]).
5
+ Each expert multiplies its slice by its own weight w[e] [K, N].
6
+
7
+ Why not torch.bmm + padding? Dropless padding materialises
8
+ [E, max_count, *] — on skewed routing max_count spikes (we measured
9
+ cap ~7600 on real data -> multi-GB buffers per layer, OOM). Packed
10
+ segments keep memory at exactly M rows regardless of skew, and the
11
+ same slices power a dropless (causality-safe) dispatch.
12
+
13
+ Why not torch._grouped_mm? On this ROCm build it measured ~10 TFLOP/s
14
+ (a scalar fallback path) vs ~300 TFLOP/s for bmm. This kernel is a
15
+ straight tile loop and hits bmm-class throughput.
16
+
17
+ Forward: c[rows_e] = a[rows_e] @ w[e]
18
+ Backward: da[rows_e] = dc[rows_e] @ w[e]^T (same kernel, w
19
+ loaded transposed via strides)
20
+ dw[e] = a[rows_e]^T @ dc[rows_e] (reduction kernel)
21
+ """
22
+ import torch
23
+ import triton
24
+ import triton.language as tl
25
+
26
+
27
+ @triton.autotune(
28
+ configs=[
29
+ # BM must stay 64 — _tile_meta partitions tiles with this size
30
+ triton.Config({"BM": 64, "BK": 64, "BN": 64}, num_warps=4,
31
+ num_stages=3),
32
+ triton.Config({"BM": 64, "BK": 128, "BN": 64}, num_warps=4,
33
+ num_stages=4),
34
+ triton.Config({"BM": 64, "BK": 64, "BN": 128}, num_warps=8,
35
+ num_stages=3),
36
+ triton.Config({"BM": 64, "BK": 128, "BN": 128}, num_warps=8,
37
+ num_stages=4),
38
+ triton.Config({"BM": 64, "BK": 256, "BN": 64}, num_warps=8,
39
+ num_stages=3),
40
+ ],
41
+ key=["K", "N", "E"],
42
+ )
43
+ @triton.jit
44
+ def _seg_mm_fwd(a_ptr, w_ptr, c_ptr, offs_ptr, mt_starts_ptr, n_tiles_ptr,
45
+ gidx_ptr,
46
+ K, N,
47
+ sa_m, sa_k, sw_e, sw_k, sw_n, sc_m, sc_n,
48
+ E: tl.constexpr, BE: tl.constexpr,
49
+ HAS_IDX: tl.constexpr,
50
+ BM: tl.constexpr, BK: tl.constexpr, BN: tl.constexpr):
51
+ pid_m = tl.program_id(0) # global m-tile across all experts
52
+ pid_n = tl.program_id(1)
53
+ total = tl.load(n_tiles_ptr) # scalar: total m-tiles
54
+ if pid_m >= total:
55
+ return
56
+ # expert = last e with mt_starts[e] <= pid_m (linear scan, E small)
57
+ e_off = tl.arange(0, BE)
58
+ starts = tl.load(mt_starts_ptr + e_off, mask=e_off < E, other=1 << 30)
59
+ e = tl.sum((starts <= pid_m).to(tl.int32)) - 1
60
+ mt_local = pid_m - tl.load(mt_starts_ptr + e)
61
+ row0 = tl.load(offs_ptr + e)
62
+ row1 = tl.load(offs_ptr + e + 1)
63
+ rows = row0 + mt_local * BM + tl.arange(0, BM)
64
+ rmask = rows < row1
65
+ if HAS_IDX:
66
+ a_rows = tl.load(gidx_ptr + rows, mask=rmask, other=0)
67
+ else:
68
+ a_rows = rows
69
+ cols = pid_n * BN + tl.arange(0, BN)
70
+ cmask = cols < N
71
+ acc = tl.zeros((BM, BN), dtype=tl.float32)
72
+ for k0 in range(0, K, BK):
73
+ kk = k0 + tl.arange(0, BK)
74
+ a = tl.load(a_ptr + a_rows[:, None] * sa_m + kk[None, :] * sa_k,
75
+ mask=rmask[:, None] & (kk < K)[None, :], other=0.0)
76
+ w = tl.load(w_ptr + e * sw_e + kk[:, None] * sw_k
77
+ + cols[None, :] * sw_n,
78
+ mask=(kk < K)[:, None] & cmask[None, :], other=0.0)
79
+ acc += tl.dot(a, w)
80
+ tl.store(c_ptr + rows[:, None] * sc_m + cols[None, :] * sc_n,
81
+ acc.to(c_ptr.dtype.element_ty),
82
+ mask=rmask[:, None] & cmask[None, :])
83
+
84
+
85
+ @triton.jit
86
+ def _seg_mm_dw(a_ptr, dy_ptr, dw_ptr, offs_ptr, gidx_ptr,
87
+ K, N,
88
+ sa_m, sa_k, sd_m, sd_n, sw_e, sw_k, sw_n,
89
+ HAS_IDX: tl.constexpr,
90
+ BR: tl.constexpr, BK: tl.constexpr, BN: tl.constexpr):
91
+ # grid: (E, ceil(K/BK), ceil(N/BN)) flattened into pid via program_id
92
+ e = tl.program_id(0)
93
+ pid_k = tl.program_id(1)
94
+ pid_n = tl.program_id(2)
95
+ row0 = tl.load(offs_ptr + e)
96
+ row1 = tl.load(offs_ptr + e + 1)
97
+ kk = pid_k * BK + tl.arange(0, BK)
98
+ nn = pid_n * BN + tl.arange(0, BN)
99
+ kmask = kk < K
100
+ nmask = nn < N
101
+ acc = tl.zeros((BK, BN), dtype=tl.float32)
102
+ for r0 in range(row0, row1, BR):
103
+ rows = r0 + tl.arange(0, BR)
104
+ rmask = rows < row1
105
+ if HAS_IDX:
106
+ a_rows = tl.load(gidx_ptr + rows, mask=rmask, other=0)
107
+ else:
108
+ a_rows = rows
109
+ a = tl.load(a_ptr + a_rows[:, None] * sa_m + kk[None, :] * sa_k,
110
+ mask=rmask[:, None] & kmask[None, :], other=0.0)
111
+ dy = tl.load(dy_ptr + rows[:, None] * sd_m + nn[None, :] * sd_n,
112
+ mask=rmask[:, None] & nmask[None, :], other=0.0)
113
+ acc += tl.dot(tl.trans(a), dy)
114
+ tl.store(dw_ptr + e * sw_e + kk[:, None] * sw_k + nn[None, :] * sw_n,
115
+ acc.to(dw_ptr.dtype.element_ty),
116
+ mask=kmask[:, None] & nmask[None, :])
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # fused weighted scatter: out[gidx[r]] += s[r] * yp[r] (atomics, fp32 out)
121
+ # backward: dyp[r] = s[r] * dout[gidx[r]]
122
+ # ds[r] = <dout[gidx[r]], yp[r]>
123
+ # ---------------------------------------------------------------------------
124
+
125
+ @triton.jit
126
+ def _wscatter_fwd(yp_ptr, gidx_ptr, s_ptr, out_ptr,
127
+ D, sm, sd_m,
128
+ BR: tl.constexpr, BD: tl.constexpr):
129
+ r = tl.program_id(0)
130
+ dn = tl.program_id(1)
131
+ t = tl.load(gidx_ptr + r)
132
+ s = tl.load(s_ptr + r).to(tl.float32)
133
+ cols = dn * BD + tl.arange(0, BD)
134
+ m = cols < D
135
+ v = tl.load(yp_ptr + r * sm + cols, mask=m, other=0.).to(tl.float32)
136
+ tl.atomic_add(out_ptr + t * sd_m + cols, s * v, mask=m)
137
+
138
+
139
+ @triton.jit
140
+ def _wscatter_bwd(dout_ptr, gidx_ptr, s_ptr, yp_ptr, dyp_ptr, ds_ptr,
141
+ D, sd_m, sm,
142
+ BR: tl.constexpr, BD: tl.constexpr):
143
+ r = tl.program_id(0)
144
+ dn = tl.program_id(1)
145
+ t = tl.load(gidx_ptr + r)
146
+ s = tl.load(s_ptr + r).to(tl.float32)
147
+ cols = dn * BD + tl.arange(0, BD)
148
+ m = cols < D
149
+ do = tl.load(dout_ptr + t * sd_m + cols, mask=m, other=0.).to(tl.float32)
150
+ yp = tl.load(yp_ptr + r * sm + cols, mask=m, other=0.).to(tl.float32)
151
+ tl.store(dyp_ptr + r * sm + cols, (s * do).to(dyp_ptr.dtype.element_ty),
152
+ mask=m)
153
+ tl.atomic_add(ds_ptr + r, tl.sum(do * yp, 0))
154
+
155
+
156
+ def _next_pow2(x):
157
+ return 1 << (x - 1).bit_length()
158
+
159
+
160
+ class SegMM(torch.autograd.Function):
161
+ """c[offs[e]:offs[e+1]] = a[gidx[offs[e]:offs[e+1]]] @ w[e] for each e.
162
+
163
+ a: [M,K] rows sorted by segment (or an arbitrarily-ordered dense
164
+ matrix when gidx is given — the kernel gathers rows itself);
165
+ w: [E,K,N]; offs: [E+1] int32; gidx (optional): [M] int32 row
166
+ indices into a (gather fused into the GEMM read).
167
+ Grid is a static upper bound — no host<->device sync anywhere.
168
+ """
169
+
170
+ BM, BK, BN, BR = 64, 64, 64, 32
171
+
172
+ @staticmethod
173
+ def _tile_meta(offs, E, M, dev):
174
+ counts = (offs[1:] - offs[:-1])
175
+ tiles = (counts + SegMM.BM - 1) // SegMM.BM
176
+ mt_starts = torch.cumsum(tiles, 0) - tiles
177
+ total = tiles.sum()
178
+ return mt_starts.to(torch.int32), total[None].to(torch.int32)
179
+
180
+ @staticmethod
181
+ def forward(ctx, a, w, offs, gidx=None):
182
+ if gidx is not None:
183
+ M = gidx.shape[0]
184
+ else:
185
+ M = a.shape[0]
186
+ _, K = a.shape
187
+ E, _, N = w.shape
188
+ dev = a.device
189
+ offs = offs.to(torch.int32)
190
+ has_idx = gidx is not None
191
+ if has_idx:
192
+ gidx = gidx.to(torch.int32).contiguous()
193
+ else:
194
+ gidx = offs # dummy ptr — kernel ignores when HAS_IDX=False
195
+ mt_starts, total = SegMM._tile_meta(offs, E, M, dev)
196
+ c = torch.empty(M, N, device=dev, dtype=a.dtype)
197
+ max_tiles = (M + SegMM.BM - 1) // SegMM.BM + E
198
+ grid_n = (N + SegMM.BN - 1) // SegMM.BN
199
+ _seg_mm_fwd[(max_tiles, grid_n)](
200
+ a, w, c, offs, mt_starts, total, gidx, K, N,
201
+ a.stride(0), a.stride(1), w.stride(0), w.stride(1),
202
+ w.stride(2), c.stride(0), c.stride(1),
203
+ E=E, BE=_next_pow2(E + 1), HAS_IDX=has_idx)
204
+ ctx.save_for_backward(a, w, offs, gidx if has_idx else None)
205
+ ctx.has_idx = has_idx
206
+ return c
207
+
208
+ @staticmethod
209
+ def backward(ctx, dc):
210
+ a, w, offs, gidx = ctx.saved_tensors
211
+ has_idx = ctx.has_idx
212
+ M = dc.shape[0]
213
+ _, K = a.shape
214
+ E, _, N = w.shape
215
+ dev = a.device
216
+ dc = dc.contiguous()
217
+ # da = seg_mm(dc, w^T) — packed-space grad (M rows either way)
218
+ mt_starts, total = SegMM._tile_meta(offs, E, M, dev)
219
+ da_packed = torch.empty(M, K, device=dev, dtype=a.dtype)
220
+ max_tiles = (M + SegMM.BM - 1) // SegMM.BM + E
221
+ grid_k = (K + SegMM.BN - 1) // SegMM.BN
222
+ dummy = gidx if has_idx else offs
223
+ _seg_mm_fwd[(max_tiles, grid_k)](
224
+ dc, w, da_packed, offs, mt_starts, total, dummy, N, K,
225
+ dc.stride(0), dc.stride(1), w.stride(0), w.stride(2),
226
+ w.stride(1), da_packed.stride(0), da_packed.stride(1),
227
+ E=E, BE=_next_pow2(E + 1), HAS_IDX=False)
228
+ if has_idx:
229
+ # scatter packed grads back to the dense a-rows
230
+ da = torch.zeros_like(a)
231
+ da.index_add_(0, gidx.long(), da_packed)
232
+ else:
233
+ da = da_packed
234
+ # dw[e] = a_e^T @ dc_e — reduction kernel (gathers via gidx)
235
+ dw = torch.empty_like(w)
236
+ _seg_mm_dw[(E, (K + SegMM.BK - 1) // SegMM.BK,
237
+ (N + SegMM.BN - 1) // SegMM.BN)](
238
+ a, dc, dw, offs, dummy, K, N,
239
+ a.stride(0), a.stride(1), dc.stride(0), dc.stride(1),
240
+ dw.stride(0), dw.stride(1), dw.stride(2),
241
+ HAS_IDX=has_idx,
242
+ BR=SegMM.BR, BK=SegMM.BK, BN=SegMM.BN)
243
+ return da, dw, None, None
244
+
245
+
246
+ def seg_mm(a, w, offs, gidx=None):
247
+ """Grouped matmul: a [M,K] sorted rows, w [E,K,N], offs [E+1].
248
+ With gidx [M] int32: a stays dense, packed row r reads a[gidx[r]]."""
249
+ return SegMM.apply(a, w, offs, gidx)
250
+
251
+
252
+ class WScatter(torch.autograd.Function):
253
+ """out[t[r]] += s[r] * yp[r] — fused weighted scatter-add.
254
+
255
+ out: [N,D] fp32 (must be pre-zeroed); yp: [M,D]; s: [M];
256
+ t: [M] int32 destination rows (duplicates allowed — atomics).
257
+ Backward: dyp = s * dout[t]; ds = <dout[t], yp>.
258
+ """
259
+
260
+ BD = 256
261
+
262
+ @staticmethod
263
+ def forward(ctx, out, yp, s, t):
264
+ M, D = yp.shape
265
+ t32 = t.to(torch.int32).contiguous()
266
+ grid_d = (D + WScatter.BD - 1) // WScatter.BD
267
+ _wscatter_fwd[(M, grid_d)](
268
+ yp, t32, s, out, D, yp.stride(0), out.stride(0),
269
+ BR=1, BD=WScatter.BD)
270
+ ctx.mark_dirty(out)
271
+ ctx.save_for_backward(yp, s, t32)
272
+ ctx.D, ctx.sm = D, yp.stride(0)
273
+ return out
274
+
275
+ @staticmethod
276
+ def backward(ctx, dout):
277
+ yp, s, t32 = ctx.saved_tensors
278
+ M, D = yp.shape
279
+ dout = dout.contiguous()
280
+ dyp = torch.empty_like(yp)
281
+ ds = torch.zeros(M, device=yp.device, dtype=torch.float32)
282
+ grid_d = (D + WScatter.BD - 1) // WScatter.BD
283
+ _wscatter_bwd[(M, grid_d)](
284
+ dout, t32, s, yp, dyp, ds, D, dout.stride(0),
285
+ yp.stride(0), BR=1, BD=WScatter.BD)
286
+ return dout, dyp, ds.to(s.dtype), None
287
+
288
+
289
+ def wscatter(out, yp, s, t):
290
+ """out [N,D] fp32 pre-zeroed; yp [M,D]; s [M]; t [M] int rows."""
291
+ return WScatter.apply(out, yp, s, t)
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ if __name__ == "__main__":
296
+ torch.manual_seed(0)
297
+ dev = "cuda"
298
+ E, M, K, N = 32, 9000, 256, 192
299
+ # ragged segment sizes
300
+ counts = torch.multinomial(torch.rand(E), E, replacement=True) * 7
301
+ counts = (counts.float() / counts.sum() * M).long()
302
+ counts[0] += M - counts.sum()
303
+ offs = torch.zeros(E + 1, dtype=torch.int64)
304
+ offs[1:] = torch.cumsum(counts, 0)
305
+ offs = offs.to(dev)
306
+ a = torch.randn(M, K, device=dev, dtype=torch.bfloat16,
307
+ requires_grad=True)
308
+ w = (torch.randn(E, K, N, device=dev, dtype=torch.bfloat16)
309
+ / (K ** 0.5)).requires_grad_(True)
310
+
311
+ a_ref = a.detach().clone().requires_grad_(True)
312
+ w_ref = w.detach().clone().requires_grad_(True)
313
+ c2 = torch.zeros(M, N, device=dev, dtype=torch.bfloat16)
314
+ for e in range(E):
315
+ s, t = int(offs[e]), int(offs[e + 1])
316
+ if t > s:
317
+ c2[s:t] = a_ref[s:t] @ w_ref[e]
318
+
319
+ c = seg_mm(a, w, offs)
320
+ err = (c.float() - c2.float()).abs().max().item()
321
+ rel = err / (c2.float().abs().max().item() + 1e-9)
322
+ print(f"fwd rel err {rel:.2e}")
323
+
324
+ g = torch.randn_like(c)
325
+ c.backward(g); c2.backward(g)
326
+ da_err = ((a.grad - a_ref.grad).abs().max()
327
+ / a_ref.grad.abs().max()).item()
328
+ dw_err = ((w.grad - w_ref.grad).abs().max()
329
+ / w_ref.grad.abs().max()).item()
330
+ print(f"bwd da rel err {da_err:.2e} dw rel err {dw_err:.2e}")
331
+
332
+ # --- gidx gather variant: dense a_src[N,K], gidx[M] picks rows ---
333
+ Nsrc = 4000
334
+ gidx = torch.randint(0, Nsrc, (M,), device=dev)
335
+ a_src = torch.randn(Nsrc, K, device=dev, dtype=torch.bfloat16,
336
+ requires_grad=True)
337
+ w3 = (torch.randn(E, K, N, device=dev, dtype=torch.bfloat16)
338
+ / (K ** 0.5)).requires_grad_(True)
339
+ a_src_r = a_src.detach().clone().requires_grad_(True)
340
+ w3_r = w3.detach().clone().requires_grad_(True)
341
+ c3 = torch.zeros(M, N, device=dev, dtype=torch.bfloat16)
342
+ for e in range(E):
343
+ s, t = int(offs[e]), int(offs[e + 1])
344
+ if t > s:
345
+ c3[s:t] = a_src_r[gidx[s:t]] @ w3_r[e]
346
+ c4 = seg_mm(a_src, w3, offs, gidx=gidx)
347
+ rel = ((c4.float() - c3.float()).abs().max()
348
+ / (c3.float().abs().max() + 1e-9)).item()
349
+ print(f"gidx fwd rel err {rel:.2e}")
350
+ g4 = torch.randn_like(c4)
351
+ c4.backward(g4); c3.backward(g4)
352
+ da_err = ((a_src.grad - a_src_r.grad).abs().max()
353
+ / a_src_r.grad.abs().max()).item()
354
+ dw_err = ((w3.grad - w3_r.grad).abs().max()
355
+ / w3_r.grad.abs().max()).item()
356
+ print(f"gidx bwd da rel {da_err:.2e} dw rel {dw_err:.2e}")
357
+
358
+ # --- wscatter: out[t[r]] += s[r]*yp[r] vs index_add ---
359
+ Nout = 3000
360
+ yp = torch.randn(M, N, device=dev, dtype=torch.bfloat16,
361
+ requires_grad=True)
362
+ tt = torch.randint(0, Nout, (M,), device=dev)
363
+ sw = torch.rand(M, device=dev, dtype=torch.float32,
364
+ requires_grad=True)
365
+ yp_r = yp.detach().clone().requires_grad_(True)
366
+ sw_r = sw.detach().clone().requires_grad_(True)
367
+ ref = torch.zeros(Nout, N, device=dev, dtype=torch.float32)
368
+ ref.index_add_(0, tt, (yp_r * sw_r[:, None]).float())
369
+ out5 = wscatter(torch.zeros(Nout, N, device=dev, dtype=torch.float32),
370
+ yp, sw, tt)
371
+ rel = ((out5 - ref).abs().max()
372
+ / (ref.abs().max() + 1e-9)).item()
373
+ print(f"wscatter fwd rel err {rel:.2e}")
374
+ g5 = torch.randn(Nout, N, device=dev, dtype=torch.float32)
375
+ out5.backward(g5); ref.backward(g5)
376
+ dyp_err = ((yp.grad.float() - yp_r.grad.float()).abs().max()
377
+ / yp_r.grad.float().abs().max()).item()
378
+ ds_err = ((sw.grad - sw_r.grad).abs().max()
379
+ / (sw_r.grad.abs().max() + 1e-9)).item()
380
+ print(f"wscatter bwd dyp rel {dyp_err:.2e} ds rel {ds_err:.2e}")
381
+
382
+ # speed vs padded bmm
383
+ import time
384
+ def bench(fn, n=50):
385
+ for _ in range(5): fn()
386
+ torch.cuda.synchronize(); t0 = time.time()
387
+ for _ in range(n): fn()
388
+ torch.cuda.synchronize(); return (time.time() - t0) / n * 1e3
389
+ print(f"seg_mm fwd {bench(lambda: seg_mm(a, w, offs)):.3f}ms")
390
+ cap = int(counts.max())
391
+ xp = torch.zeros(E, cap, K, device=dev, dtype=torch.bfloat16)
392
+ print(f"padded bmm {bench(lambda: torch.bmm(xp, w)):.3f}ms")
393
+ print("segmm self-test done")
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff