>:
92 match a b:
93 case OneMom{x} OneMom{y}: OneMom{Two{x, y}}
94 case TwoMom{x, u} TwoMom{y, v}: TwoMom{Two{x, y}, Two{u, v}}
95 case Packed{x} Packed{y}: Packed{PNode{x, y}}
96 case _ _: NoMom{}
97
98 # what an optimizer step returns: the new parameters, the zeroed gradient, the optimizer state
99 type Step<-P: Type> is Type:
100 Step{p: P, g: P, st: OptState}
101
102 # The optimizer, with the scalars of the current step. Adam's `step` is lr / (1 - b1^t) and `bc2` is
103 # sqrt(1 - b2^t), as PyTorch computes them; `Opt.at` fills them from an `OptCfg` and the step count.
104 # `dec` picks AdamW's decoupled weight decay (w *= 1 - lr*wd) over Adam's (g += wd*w).
105 type Opt is Data:
106 SGD{lr: F32}
107 Momentum{lr: F32, mu: F32}
108 Adam{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, dec: Bool, step: F32, bc2: F32, c1: F32, c2: F32}
109
110 # Parameters and their flat dump in a fixed order. Loading has a separate array cursor (PA).
111 type PL<-P: Type> is Type:
112 PL{p: P, xs: List<&2, F32>}
113
114 # Loading cursor: the shared source array and next element offset, handed back without a list or a clone.
115 # The IO boundary checks the source length against Layer.count before calling the pure loaders.
116 type PA<-P: Type> is Type:
117 PA{p: P, xs: Array, off: U32}
118
119 type Layer<-P: Type, -X: Type, -Y: Type, -S: Type> is Type:
120 Layer{fwd: P -> X -> Fwd, bwd: P -> S -> Y -> P -> Bwd
, step: Opt -> P -> P -> OptState
-> Step
, zeros: Unit -> P, dump: P -> PL
, load: P -> Array -> U32 -> PA, store: P -> Array -> U32 -> PA, count: Unit -> U32}
121
122 def Layer.fwd(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, x: X) -> Fwd
:
123 match l:
124 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
125 fwd(p, x)
126
127 def Layer.bwd(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, s: S, dy: Y, g: P) -> Bwd
:
128 match l:
129 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
130 bwd(p, s, dy, g)
131
132 def Layer.step(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, +o: Opt, p: P, g: P, st: OptState
) -> Step
:
133 match l:
134 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
135 step(o, p, g, st)
136
137 def Layer.zeros(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
) -> P:
138 match l:
139 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
140 zeros(Unit{})
141
142 # every number of the parameters, in the layer's fixed order (the parameters handed back)
143 def Layer.dump(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P) -> PL
:
144 match l:
145 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
146 dump(p)
147
148 # Parameters overwritten from xs at an element offset, in dump order; xs and the next offset come back.
149 def Layer.load(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, xs: Array, off: U32) -> PA:
150 match l:
151 case Layer{fwd, bwd, step, zeros, dump, load, store, count}: load(p, xs, off)
152
153 # The parameters copied into xs from an element offset, in dump order (load's inverse); xs and the next offset come back.
154 def Layer.store(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, xs: Array, off: U32) -> PA:
155 match l:
156 case Layer{fwd, bwd, step, zeros, dump, load, store, count}: store(p, xs, off)
157
158 def Layer.count(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
) -> U32:
159 match l:
160 case Layer{fwd, bwd, step, zeros, dump, load, store, count}: count(Unit{})
161
162 # =====================================================================================================
163 # Element-wise kernels (templates: one copy per function, no closure at run time)
164 # =====================================================================================================
165
166 # a[t] = f(a[t]) for the n numbers from t; ra holds the array and a[t], already read
167 def ew1_l(~f: @+a: F32 -> F32, nleft: Nat, +t: U32, ra: Array & F32) -> Array:
168 match nleft ra:
169 case 0n Tuple{a, v}:
170 a
171 case 1n+p Tuple{a, +v}:
172 ew1_l(~f, p, (t + 1 : U32), Array.get(F32, Array.set(F32, a, t, f(v)), (t + 1 : U32)))
173
174 # a[t] = f(a[t], b[t]): the result replaces a; b comes back unchanged
175 def ew2_l(~f: @+a: F32 -> @+b: F32 -> F32, nleft: Nat, +t: U32, ra: Array & F32, rb: Array & F32) -> TA.P2:
176 match nleft ra rb:
177 case 0n Tuple{a, v} Tuple{b, w}:
178 TA.P2{a, b}
179 case 1n+p Tuple{a, +v} Tuple{b, +w}:
180 ew2_l(~f, p, (t + 1 : U32), Array.get(F32, Array.set(F32, a, t, f(v, w)), (t + 1 : U32)), Array.get(F32, b, (t + 1 : U32)))
181
182 def Ew.map(~f: @+a: F32 -> F32, +r: Nat, +c: Nat, m: TA.Mat) -> TA.Mat:
183 match m:
184 case TA.Mat{d}:
185 TA.Mat{ew1_l(~f, Nat.mul(r, c), 0, Array.get(F32, d, 0))}
186
187 def ew2_fin(-r: Nat, -c: Nat, p: TA.P2) -> TA.MPair:
188 match p:
189 case TA.P2{a, b}:
190 TA.MPair{TA.Mat{a}, TA.Mat{b}}
191
192 # (f(a, b), b), element by element
193 def Ew.zip(~f: @+a: F32 -> @+b: F32 -> F32, +r: Nat, +c: Nat, a: TA.Mat, b: TA.Mat) -> TA.MPair:
194 match a b:
195 case TA.Mat{x} TA.Mat{y}:
196 ew2_fin(r, c, ew2_l(~f, Nat.mul(r, c), 0, Array.get(F32, x, 0), Array.get(F32, y, 0)))
197
198 def f32_add(+a: F32, +b: F32) -> F32:
199 (a + b : F32)
200
201 def f32_mul(+a: F32, +b: F32) -> F32:
202 (a * b : F32)
203
204 # (a + b, b)
205 def Mat.add(+r: Nat, +c: Nat, a: TA.Mat, b: TA.Mat) -> TA.MPair:
206 Ew.zip(~f32_add, r, c, a, b)
207
208 def zero_of(+x: F32) -> F32:
209 0.0
210
211 # the smallest d with 2^d >= n (TA.cap_depth's answer), by doubling a U32: log2(n) cheap steps, where cap_depth
212 # compares Nats one unit at a time at each doubling; f is fuel (32 doublings cover every U32)
213 def depth_go(f: Nat, ok: Bool, +n: U32, +p: U32, d: Nat) -> Nat:
214 match f ok:
215 case _ True{}:
216 d
217 case 0n False{}:
218 d
219 case 1n+g False{}:
220 depth_go(g, U32.is_le(n, (p + p : U32)), n, (p + p : U32), 1n+d)
221
222 def depth(+n: Nat) -> Nat:
223 depth_go(32n, U32.is_le(U32.from_nat(n), 1), U32.from_nat(n), 1, 0n)
224
225 # =====================================================================================================
226 # Optimizers: one pass over a parameter, its gradient and the two moment buffers; the gradient is zeroed
227 # =====================================================================================================
228
229 type Q3 is Type:
230 Q3{w: Array, g: Array, m: Array}
231
232 type Q4 is Type:
233 Q4{w: Array, g: Array, m: Array, v: Array}
234
235 type MatStep<-r: Nat, -c: Nat> is Type:
236 MatStep{w: TA.Mat, g: TA.Mat, st: OptState>}
237
238 # w <- w + (-lr)*g, as PyTorch's p.add_(g, alpha=-lr)
239 def sgd_l(nleft: Nat, +t: U32, +lr: F32, rw: Array & F32, rg: Array & F32) -> TA.P2:
240 match nleft rw rg:
241 case 0n Tuple{w, x} Tuple{g, y}:
242 TA.P2{w, g}
243 case 1n+p Tuple{w, +x} Tuple{g, +y}:
244 sgd_l(p, (t + 1 : U32), lr, Array.get(F32, Array.set(F32, w, t, (x + (F32.neg(lr) * y : F32) : F32)), (t + 1 : U32)), Array.get(F32, Array.set(F32, g, t, 0.0), (t + 1 : U32)))
245
246 # buf <- buf*mu + g; w <- w + (-lr)*buf, as PyTorch's SGD with momentum (no dampening, not Nesterov)
247 def mom_l(nleft: Nat, +t: U32, +lr: F32, +mu: F32, rw: Array & F32, rg: Array & F32, rm: Array & F32) -> Q3:
248 match nleft rw rg rm:
249 case 0n Tuple{w, x} Tuple{g, y} Tuple{m, b}:
250 Q3{w, g, m}
251 case 1n+p Tuple{w, +x} Tuple{g, +y} Tuple{m, +b}:
252 +nb = ((b * mu : F32) + y : F32)
253 +u = (t + 1 : U32)
254 mom_l(p, u, lr, mu, Array.get(F32, Array.set(F32, w, t, (x + (F32.neg(lr) * nb : F32) : F32)), u), Array.get(F32, Array.set(F32, g, t, 0.0), u), Array.get(F32, Array.set(F32, m, t, nb), u))
255
256 # the Adam family, in PyTorch's order of operations (torch.optim.adam._single_tensor_adam):
257 # g' = g + wd*w (Adam) or w' = w*(1 - lr*wd) (AdamW); m = m + (1-b1)*(g' - m) (lerp); v = v*b2 + (1-b2)*g'*g';
258 # w = w' + (-step*m) / (sqrt(v)/bc2 + eps)
259 def adam_grad(dec: Bool, +wd: F32, +w: F32, +g: F32) -> F32:
260 match dec:
261 case True{}:
262 g
263 case False{}:
264 (g + (w * wd : F32) : F32)
265
266 def adam_decay(dec: Bool, +lr: F32, +wd: F32, +w: F32) -> F32:
267 match dec:
268 case True{}:
269 (w * (1.0 - (lr * wd : F32) : F32) : F32)
270 case False{}:
271 w
272
273 # c1 = 1 - b1 and c2 = 1 - b2 are given as their own constants (0.1, 0.001), as PyTorch computes 1 - beta in float64
274 # before rounding: 1 - b2 taken in F32 from b2 = 0.999 (0.99900001287 in F32) is 1.3e-5 too small, and over 100 steps
275 # that alone put the tiny Llama 21x further from float64 than PyTorch's float32 (NOTES, 2026-10-18)
276 def adam_m(+c1: F32, +m: F32, +g: F32) -> F32:
277 (m + (c1 * (g - m : F32) : F32) : F32)
278
279 def adam_v(+b2: F32, +c2: F32, +v: F32, +g: F32) -> F32:
280 ((v * b2 : F32) + ((c2 * g : F32) * g : F32) : F32)
281
282 def adam_w(+step: F32, +bc2: F32, +eps: F32, +w: F32, +m: F32, +v: F32) -> F32:
283 (w + ((F32.neg(step) * m : F32) / ((F32.sqrt(v) / bc2 : F32) + eps : F32) : F32) : F32)
284
285 def adam_l(nleft: Nat, +t: U32, +lr: F32, +c1: F32, +b2: F32, +c2: F32, +eps: F32, +wd: F32, +dec: Bool, +step: F32, +bc2: F32, rw: Array & F32, rg: Array & F32, rm: Array & F32, rv: Array & F32) -> Q4:
286 match nleft rw rg rm rv:
287 case 0n Tuple{w, x} Tuple{g, y} Tuple{m, a} Tuple{v, b}:
288 Q4{w, g, m, v}
289 case 1n+p Tuple{w, +x} Tuple{g, +y} Tuple{m, +a} Tuple{v, +b}:
290 +gg = adam_grad(dec, wd, x, y)
291 +nm = adam_m(c1, a, gg)
292 +nv = adam_v(b2, c2, b, gg)
293 +nw = adam_w(step, bc2, eps, adam_decay(dec, lr, wd, x), nm, nv)
294 +u = (t + 1 : U32)
295 adam_l(p, u, lr, c1, b2, c2, eps, wd, dec, step, bc2, Array.get(F32, Array.set(F32, w, t, nw), u), Array.get(F32, Array.set(F32, g, t, 0.0), u), Array.get(F32, Array.set(F32, m, t, nm), u), Array.get(F32, Array.set(F32, v, t, nv), u))
296
297 # ---- AdamW over packed BF16 moments (CAdamWBF16) ----
298 # The same F32 arithmetic as adam_l, element by element: each moment is read from its half of a word (BF16 -> F32 is
299 # exact), updated in F32, rounded to BF16 (to nearest, ties to even; a NaN becomes 0xFFFF, as torch/dtype.bend and
300 # PyTorch's CPU kernel do) and written back into its half; the weight update reads the stored, rounded moments, as
301 # PyTorch's AdamW does with BF16 state. The weights and the gradient stay F32.
302
303 def ph_f2u(x: F32) -> U32:
304 match x:
305 case F32{w}:
306 U32{w}
307
308 def ph_u2f(u: U32) -> F32:
309 match u:
310 case U32{w}:
311 F32{w}
312
313 def ph_pick(b: Bool, +x: U32, +y: U32) -> U32:
314 match b:
315 case True{}:
316 x
317 case False{}:
318 y
319
320 # F32 -> BF16 bits (dtype.bend's BF16.of_f32): add 0x7FFF plus the lowest kept bit, keep the top half; NaN -> 0xFFFF
321 def ph_of_bits(+u: U32) -> U32:
322 ph_pick(U32.is_gt((u .&. 2147483647 : U32), 2139095040), 65535, U32.shrn(((u + 32767 : U32) + (U32.shrn(u, 16n) .&. 1 : U32) : U32), 16n))
323
324 def ph_of(x: F32) -> U32:
325 ph_of_bits(ph_f2u(x))
326
327 # BF16 bits -> F32, exact
328 def ph_to(+h: U32) -> F32:
329 ph_u2f(U32.shln(h, 16n))
330
331 # the half of word w that element t uses (odd t: the high half), and w with that half replaced by h
332 def ph_half(odd: Bool, +w: U32) -> U32:
333 match odd:
334 case True{}:
335 U32.shrn(w, 16n)
336 case False{}:
337 (w .&. 65535 : U32)
338
339 def ph_put(odd: Bool, +w: U32, +h: U32) -> U32:
340 match odd:
341 case True{}:
342 ((w .&. 65535 : U32) .|. U32.shln(h, 16n) : U32)
343 case False{}:
344 ((w .&. 4294901760 : U32) .|. h : U32)
345
346 type PQ is Type:
347 PQ{w: Array, g: Array, m: Array, v: Array}
348
349 type PQV is Type:
350 PQV{w: Array, g: Array, m: Array, v: Array}
351
352 # m and v packed. rm and rv hold the word of element t (index t >> 1), read after the previous element's write.
353 def adamh_l(nleft: Nat, +t: U32, +lr: F32, +c1: F32, +b2: F32, +c2: F32, +eps: F32, +wd: F32, +dec: Bool, +step: F32, +bc2: F32, rw: Array & F32, rg: Array & F32, rm: Array & U32, rv: Array & U32) -> PQ:
354 match nleft rw rg rm rv:
355 case 0n Tuple{w, x} Tuple{g, y} Tuple{m, a} Tuple{v, b}:
356 PQ{w, g, m, v}
357 case 1n+p Tuple{w, +x} Tuple{g, +y} Tuple{m, +a} Tuple{v, +b}:
358 +odd = U32.is_eq((t .&. 1 : U32), 1)
359 +gg = adam_grad(dec, wd, x, y)
360 +hm = ph_of(adam_m(c1, ph_to(ph_half(odd, a)), gg))
361 +hv = ph_of(adam_v(b2, c2, ph_to(ph_half(odd, b)), gg))
362 +nw = adam_w(step, bc2, eps, adam_decay(dec, lr, wd, x), ph_to(hm), ph_to(hv))
363 +u = (t + 1 : U32)
364 +k = U32.shrn(t, 1n)
365 +ku = U32.shrn(u, 1n)
366 adamh_l(p, u, lr, c1, b2, c2, eps, wd, dec, step, bc2, Array.get(F32, Array.set(F32, w, t, nw), u), Array.get(F32, Array.set(F32, g, t, 0.0), u), Array.get(U32, Array.set(U32, m, k, ph_put(odd, a, hm)), ku), Array.get(U32, Array.set(U32, v, k, ph_put(odd, b, hv)), ku))
367
368 # m packed, v in F32 (one cell an element, as adam_l's)
369 def adamhv_l(nleft: Nat, +t: U32, +lr: F32, +c1: F32, +b2: F32, +c2: F32, +eps: F32, +wd: F32, +dec: Bool, +step: F32, +bc2: F32, rw: Array & F32, rg: Array & F32, rm: Array & U32, rv: Array & F32) -> PQV:
370 match nleft rw rg rm rv:
371 case 0n Tuple{w, x} Tuple{g, y} Tuple{m, a} Tuple{v, b}:
372 PQV{w, g, m, v}
373 case 1n+p Tuple{w, +x} Tuple{g, +y} Tuple{m, +a} Tuple{v, +b}:
374 +odd = U32.is_eq((t .&. 1 : U32), 1)
375 +gg = adam_grad(dec, wd, x, y)
376 +hm = ph_of(adam_m(c1, ph_to(ph_half(odd, a)), gg))
377 +nv = adam_v(b2, c2, b, gg)
378 +nw = adam_w(step, bc2, eps, adam_decay(dec, lr, wd, x), ph_to(hm), nv)
379 +u = (t + 1 : U32)
380 adamhv_l(p, u, lr, c1, b2, c2, eps, wd, dec, step, bc2, Array.get(F32, Array.set(F32, w, t, nw), u), Array.get(F32, Array.set(F32, g, t, 0.0), u), Array.get(U32, Array.set(U32, m, U32.shrn(t, 1n), ph_put(odd, a, hm)), U32.shrn(u, 1n)), Array.get(F32, Array.set(F32, v, t, nv), u))
381
382 # the words of n packed values: ceil(n / 2), in a block of the next power of two
383 def ph_words(+n: Nat) -> Array:
384 Array.new(U32, depth(Nat.div(Nat.add(n, 1n), 2n)), 0)
385
386 def ph_fin(-r: Nat, -c: Nat, +n: U32, q: PQ) -> MatStep:
387 match q:
388 case PQ{w, g, m, v}: MatStep{TA.Mat{w}, TA.Mat{g}, Packed{PLeaf{n, m, v}}}
389
390 def phv_fin(-r: Nat, -c: Nat, +n: U32, q: PQV) -> MatStep:
391 match q:
392 case PQV{w, g, m, v}: MatStep{TA.Mat{w}, TA.Mat{g}, Packed{PLeafV{n, m, v}}}
393
394 # one matrix's step on its leaf; anything else leaves the values untouched, as Opt.mat's fallback
395 def adamh_leaf(+r: Nat, +c: Nat, +o: Opt, wa: Array, ga: Array, t: PTree) -> MatStep:
396 match o t:
397 case Adam{lr, b1, b2, eps, wd, dec, step, bc2, c1, c2} PLeaf{+n, ma, va}:
398 ph_fin(r, c, n, adamh_l(Nat.mul(r, c), 0, lr, c1, b2, c2, eps, wd, dec, step, bc2, Array.get(F32, wa, 0), Array.get(F32, ga, 0), Array.get(U32, ma, 0), Array.get(U32, va, 0)))
399 case Adam{lr, b1, b2, eps, wd, dec, step, bc2, c1, c2} PLeafV{+n, ma, va}:
400 phv_fin(r, c, n, adamhv_l(Nat.mul(r, c), 0, lr, c1, b2, c2, eps, wd, dec, step, bc2, Array.get(F32, wa, 0), Array.get(F32, ga, 0), Array.get(U32, ma, 0), Array.get(F32, va, 0)))
401 case _ t:
402 MatStep{TA.Mat{wa}, TA.Mat{ga}, Packed{t}}
403
404 # the leaf of a matrix on its first step: zeros (BF16 0x0000 is +0.0)
405 def ph_new(+n: Nat, v16: Bool) -> PTree:
406 match v16:
407 case True{}:
408 PLeaf{U32.from_nat(n), ph_words(n), ph_words(n)}
409 case False{}:
410 PLeafV{U32.from_nat(n), ph_words(n), Array.new(F32, depth(n), 0.0)}
411
412 def adamh_mat(+r: Nat, +c: Nat, +o: Opt, wa: Array, ga: Array, t: PTree) -> MatStep:
413 match t:
414 case PNew{v16}:
415 adamh_leaf(r, c, o, wa, ga, ph_new(Nat.mul(r, c), v16))
416 case t:
417 adamh_leaf(r, c, o, wa, ga, t)
418
419 # One matrix step, with only the buffers this optimizer reads.
420 def Opt.sgd_fin(-r: Nat, -c: Nat, q: TA.P2) -> MatStep:
421 match q:
422 case TA.P2{w, g}: MatStep{TA.Mat{w}, TA.Mat{g}, NoMom{}}
423
424 def Opt.mom_fin(-r: Nat, -c: Nat, q: Q3) -> MatStep:
425 match q:
426 case Q3{w, g, m}: MatStep{TA.Mat{w}, TA.Mat{g}, OneMom{TA.Mat{m}}}
427
428 def Opt.fin(-r: Nat, -c: Nat, q: Q4) -> MatStep:
429 match q:
430 case Q4{w, g, m, v}: MatStep{TA.Mat{w}, TA.Mat{g}, TwoMom{TA.Mat{m}, TA.Mat{v}}}
431
432 # A mismatched state is initialized only at the training boundary; Opt.mat requires the matching variant.
433 # The fallback leaves the values untouched, so it cannot fabricate a moment buffer.
434 def Opt.mat(+r: Nat, +c: Nat, +o: Opt, w: TA.Mat, g: TA.Mat, st: OptState>) -> MatStep:
435 match o w g st:
436 case SGD{lr} TA.Mat{wd} TA.Mat{gd} _:
437 Opt.sgd_fin(r, c, sgd_l(Nat.mul(r, c), 0, lr, Array.get(F32, wd, 0), Array.get(F32, gd, 0)))
438 case Momentum{lr, mu} TA.Mat{wd} TA.Mat{gd} OneMom{TA.Mat{md}}:
439 Opt.mom_fin(r, c, mom_l(Nat.mul(r, c), 0, lr, mu, Array.get(F32, wd, 0), Array.get(F32, gd, 0), Array.get(F32, md, 0)))
440 case Adam{lr, b1, b2, eps, wd2, dec, step, bc2, c1, c2} TA.Mat{wd} TA.Mat{gd} TwoMom{TA.Mat{md}, TA.Mat{vd}}:
441 Opt.fin(r, c, adam_l(Nat.mul(r, c), 0, lr, c1, b2, c2, eps, wd2, dec, step, bc2, Array.get(F32, wd, 0), Array.get(F32, gd, 0), Array.get(F32, md, 0), Array.get(F32, vd, 0)))
442 case Adam{lr, b1, b2, eps, wd2, dec, step, bc2, c1, c2} TA.Mat{wd} TA.Mat{gd} Packed{t}:
443 adamh_mat(r, c, o, wd, gd, t)
444 case _ w g st:
445 MatStep{w, g, st}
446
447 # What the user picks; `Opt.at` turns it into the step's `Opt` from q1 = 1 - b1^t and q2 = 1 - b2^t (t = 1 on the
448 # first step). Adam's c1 and c2 are 1 - b1 and 1 - b2, written as their own constants (adam_m above). CAdamWBF16 is
449 # AdamW with its first moment (and its second, when v16) stored as packed BF16 (PTree); weights and gradient stay F32.
450 type OptCfg is Data:
451 CSGD{lr: F32}
452 CMomentum{lr: F32, mu: F32}
453 CAdam{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, c1: F32, c2: F32}
454 CAdamW{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, c1: F32, c2: F32}
455 CAdamWBF16{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, c1: F32, c2: F32, v16: Bool}
456
457 def Opt.at(+cfg: OptCfg, +q1: F32, +q2: F32) -> Opt:
458 match cfg:
459 case CSGD{lr}:
460 SGD{lr}
461 case CMomentum{lr, mu}:
462 Momentum{lr, mu}
463 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
464 Adam{lr, b1, b2, eps, wd, False{}, (lr / q1 : F32), F32.sqrt(q2), c1, c2}
465 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
466 Adam{lr, b1, b2, eps, wd, True{}, (lr / q1 : F32), F32.sqrt(q2), c1, c2}
467 case CAdamWBF16{lr, b1, b2, eps, wd, c1, c2, v16}:
468 Adam{lr, b1, b2, eps, wd, True{}, (lr / q1 : F32), F32.sqrt(q2), c1, c2}
469
470 def OptCfg.b1(+cfg: OptCfg) -> F32:
471 match cfg:
472 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
473 b1
474 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
475 b1
476 case CAdamWBF16{lr, b1, b2, eps, wd, c1, c2, v16}:
477 b1
478 case _:
479 0.0
480
481 def OptCfg.b2(+cfg: OptCfg) -> F32:
482 match cfg:
483 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
484 b2
485 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
486 b2
487 case CAdamWBF16{lr, b1, b2, eps, wd, c1, c2, v16}:
488 b2
489 case _:
490 0.0
491
492 def OptCfg.c1(+cfg: OptCfg) -> F32:
493 match cfg:
494 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
495 c1
496 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
497 c1
498 case CAdamWBF16{lr, b1, b2, eps, wd, c1, c2, v16}:
499 c1
500 case _:
501 0.0
502
503 def OptCfg.c2(+cfg: OptCfg) -> F32:
504 match cfg:
505 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
506 c2
507 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
508 c2
509 case CAdamWBF16{lr, b1, b2, eps, wd, c1, c2, v16}:
510 c2
511 case _:
512 0.0
513
514 # c = 1 - b up to F32's rounding, with 0 < c <= 1 (PyTorch's 0 <= beta < 1). For b in [0, 1), 1 - b taken in F32 is
515 # within half an ulp of the exact value and c's own rounding adds at most as much, so a c written as 1 - b is always
516 # under 2^-23 from it, while a typo (0.01 for 0.001, c1 and c2 swapped) is far above. A NaN fails every comparison.
517 def beta_ok(+b: F32, +c: F32) -> Bool:
518 +d = ((1.0 - b : F32) - c : F32)
519 Bool.and(Bool.and((c > 0.0 : F32), (c <= 1.0 : F32)), ((d * d : F32) <= 0.0000000000000142108547152020037174224853515625 : F32))
520
521 # True unless an Adam configuration's c1, c2 disagree with its b1, b2. Train.new poisons the state of a configuration
522 # that fails (every loss after the first is NaN); a program checks it first with OptCfg.check to stop with a message.
523 def OptCfg.ok(+cfg: OptCfg) -> Bool:
524 match cfg:
525 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
526 Bool.and(beta_ok(b1, c1), beta_ok(b2, c2))
527 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
528 Bool.and(beta_ok(b1, c1), beta_ok(b2, c2))
529 case CAdamWBF16{lr, b1, b2, eps, wd, c1, c2, v16}:
530 Bool.and(beta_ok(b1, c1), beta_ok(b2, c2))
531 case _:
532 True{}
533
534 def cfg_stop(ok: Bool) -> IO(Unit):
535 match ok:
536 case True{}:
537 IO.pure(Unit, Unit{})
538 case False{}:
539 IO.die(Unit, 1, "CAdam/CAdamW: c1, c2 do not match b1, b2 (c1 = 1 - b1, c2 = 1 - b2, e.g. 0.9, 0.999 with 0.1, 0.001)")
540
541 def OptCfg.check(+cfg: OptCfg) -> IO(Unit):
542 cfg_stop(OptCfg.ok(cfg))
543
544 # dump and load for a matrix: its r*c numbers, row by row
545 def mat_dump_fin(-r: Nat, -c: Nat, ml: TA.ML) -> PL>:
546 match ml:
547 case TA.ML{m, xs}:
548 PL{m, xs}
549
550 def Mat.dump(+r: Nat, +c: Nat, m: TA.Mat) -> PL>:
551 mat_dump_fin(r, c, TA.Mat.to_list(r, c, m))
552
553 def load_cells(left: Nat, +i: U32, +off: U32, rx: Array & F32, dst: Array) -> TA.P2:
554 match left rx:
555 case 0n Tuple{xs, v}: TA.P2{xs, dst}
556 case 1n+q Tuple{xs, v}:
557 load_cells(q, (i + 1 : U32), (off + 1 : U32), Array.get(F32, xs, (off + 1 : U32)), Array.set(F32, dst, i, v))
558
559 def mat_restore_fin(-r: Nat, -c: Nat, +off: U32, q: TA.P2) -> PA>:
560 match q:
561 case TA.P2{xs, dst}: PA{TA.Mat{dst}, xs, off}
562
563 def Mat.restore(+r: Nat, +c: Nat, m: TA.Mat, xs: Array, +off: U32) -> PA>:
564 match m:
565 case TA.Mat{dst}:
566 mat_restore_fin(r, c, (off + U32.from_nat(Nat.mul(r, c)) : U32), load_cells(Nat.mul(r, c), 0, off, Array.get(F32, xs, off), dst))
567
568 # load_cells' inverse: copies the matrix's cells i.. into xs from `off`; both arrays come back (the matrix's first)
569 def store_cells(left: Nat, +i: U32, +off: U32, rm: Array & F32, xs: Array) -> TA.P2:
570 match left rm:
571 case 0n Tuple{ma, v}: TA.P2{ma, xs}
572 case 1n+q Tuple{ma, v}:
573 store_cells(q, (i + 1 : U32), (off + 1 : U32), Array.get(F32, ma, (i + 1 : U32)), Array.set(F32, xs, off, v))
574
575 def mat_store_fin(-r: Nat, -c: Nat, +off: U32, q: TA.P2) -> PA>:
576 match q:
577 case TA.P2{ma, xs}: PA{TA.Mat{ma}, xs, off}
578
579 def Mat.store(+r: Nat, +c: Nat, m: TA.Mat, xs: Array, +off: U32) -> PA>:
580 match m:
581 case TA.Mat{ma}:
582 mat_store_fin(r, c, (off + U32.from_nat(Nat.mul(r, c)) : U32), store_cells(Nat.mul(r, c), 0, off, Array.get(F32, ma, 0), xs))
583
584 def pa_second(-A: Type, -B: Type, a: A, rb: PA) -> PA>:
585 match rb:
586 case PA{b, xs, off}: PA{Two{a, b}, xs, off}
587
588 def pl_pair(-A: Type, -B: Type, ra: PL, rb: PL) -> PL>:
589 match ra rb:
590 case PL{a, xa} PL{b, xb}:
591 PL{Two{a, b}, List.append(&2, F32, xa, xb)}
592
593 def pl_second(-A: Type, -B: Type, a: A, rb: PL) -> PL>:
594 match rb:
595 case PL{b, rest}:
596 PL{Two{a, b}, rest}
597
598 # =====================================================================================================
599 # Linear: y = x·Wᵀ + b, with W stored out x in (PyTorch's nn.Linear layout); saves its input
600 # =====================================================================================================
601
602 type Lin<-i: Nat, -o: Nat> is Type:
603 Lin{w: TA.Mat, b: TA.Mat<1n, o>}
604
605 def lin_f3(-n: Nat, -i: Nat, -o: Nat, w: TA.Mat, x: TA.Mat, r: TA.MBias) -> Fwd, TA.Mat, TA.Mat>:
606 match r:
607 case TA.MBias{y, b}:
608 Fwd{Lin{w, b}, y, x}
609
610 def lin_f2(+n: Nat, -i: Nat, +o: Nat, b: TA.Mat<1n, o>, r: TA.MMulNT) -> Fwd, TA.Mat, TA.Mat>:
611 match r:
612 case TA.MMulNT{x, w, z}:
613 lin_f3(n, i, o, w, x, TA.Mat.add_row(n, o, z, b))
614
615 def lin_fwd(+n: Nat, +i: Nat, +o: Nat, par: Nat, p: Lin, x: TA.Mat) -> Fwd, TA.Mat, TA.Mat>:
616 match p:
617 case Lin{w, b}:
618 lin_f2(n, i, o, b, TA.Mat.matmul_nt(n, i, o, par, x, w))
619
620 def lin_b4(-n: Nat, -i: Nat, -o: Nat, w: TA.Mat, b: TA.Mat<1n, o>, dx: TA.Mat, gw: TA.Mat, r: TA.MColSum) -> Bwd, TA.Mat>:
621 match r:
622 case TA.MColSum{dy, gb}:
623 Bwd{Lin{w, b}, dx, Lin{gw, gb}}
624
625 def lin_b3(+n: Nat, -i: Nat, +o: Nat, w: TA.Mat, b: TA.Mat<1n, o>, dx: TA.Mat, gb: TA.Mat<1n, o>, r: TA.MMulTN) -> Bwd, TA.Mat>:
626 match r:
627 case TA.MMulTN{dy, x, gw}:
628 lin_b4(n, i, o, w, b, dx, gw, TA.Mat.col_sums_acc(n, o, dy, gb))
629
630 def lin_b2(+n: Nat, +i: Nat, +o: Nat, par: Nat, b: TA.Mat<1n, o>, x: TA.Mat, g: Lin, r: TA.MMul) -> Bwd, TA.Mat>:
631 match g r:
632 case Lin{gw, gb} TA.MMul{dy, w, dx}:
633 lin_b3(n, i, o, w, b, dx, gb, TA.Mat.matmul_tn_acc(o, n, i, par, dy, x, gw))
634
635 # dX = dY·W, dW += dYᵀ·X, db += the column sums of dY; the two sums are added straight into the gradient (the
636 # accumulating products of bend-ml-tensor-array 0.1.10.0), with the bits of computing them apart and adding them
637 def lin_bwd(+n: Nat, +i: Nat, +o: Nat, +par: Nat, p: Lin, x: TA.Mat, dy: TA.Mat, g: Lin) -> Bwd, TA.Mat>:
638 match p:
639 case Lin{w, b}:
640 lin_b2(n, i, o, par, b, x, g, TA.Mat.matmul_precise(n, o, i, par, dy, w))
641
642 def pair_s3(-P1: Type, -P2: Type, r1: Step, r2: Step) -> Step>:
643 match r1 r2:
644 case Step{p1, g1, s1} Step{p2, g2, s2}:
645 Step{Two{p1, p2}, Two{g1, g2}, OptState.join(P1, P2, s1, s2)}
646
647 def lin_pair(-i: Nat, -o: Nat, p: Lin) -> Two, TA.Mat<1n, o>>:
648 match p:
649 case Lin{w, b}: Two{w, b}
650
651 def lin_unpair(-i: Nat, -o: Nat, p: Two, TA.Mat<1n, o>>) -> Lin:
652 match p:
653 case Two{w, b}: Lin{w, b}
654
655 def lin_s2(-i: Nat, -o: Nat, a: MatStep, c: MatStep<1n, o>) -> Step>:
656 match a c:
657 case MatStep{w, gw, sw} MatStep{b, gb, sb}:
658 Step{Lin{w, b}, Lin{gw, gb}, OptState.map(Two, TA.Mat<1n, o>>, Lin, OptState.join(TA.Mat, TA.Mat<1n, o>, sw, sb), x => lin_unpair(i, o, x), x => lin_unpair(i, o, x))}
659
660 def lin_step2(+i: Nat, +o: Nat, +op: Opt, p: Lin, g: Lin, st: Two>, OptState>>) -> Step>:
661 match p g st:
662 case Lin{w, b} Lin{gw, gb} Two{sw, sb}:
663 lin_s2(i, o, Opt.mat(o, i, op, w, gw, sw), Opt.mat(1n, o, op, b, gb, sb))
664
665 def lin_step(+i: Nat, +o: Nat, +op: Opt, p: Lin, g: Lin, st: OptState>) -> Step>:
666 lin_step2(i, o, op, p, g, OptState.split(TA.Mat, TA.Mat<1n, o>, OptState.map(Lin, Two, TA.Mat<1n, o>>, st, x => lin_pair(i, o, x), x => lin_pair(i, o, x))))
667
668 def lin_dump2(-i: Nat, -o: Nat, r: PL, TA.Mat<1n, o>>>) -> PL>:
669 match r:
670 case PL{Two{w, b}, xs}:
671 PL{Lin{w, b}, xs}
672
673 def lin_dump(+i: Nat, +o: Nat, p: Lin) -> PL>:
674 match p:
675 case Lin{w, b}:
676 lin_dump2(i, o, pl_pair(TA.Mat, TA.Mat<1n, o>, Mat.dump(o, i, w), Mat.dump(1n, o, b)))
677
678 def lin_load3(-i: Nat, -o: Nat, r: PA, TA.Mat<1n, o>>>) -> PA>:
679 match r:
680 case PA{Two{w, b}, xs, off}: PA{Lin{w, b}, xs, off}
681
682 def lin_load2(+i: Nat, +o: Nat, b: TA.Mat<1n, o>, r: PA>) -> PA>:
683 match r:
684 case PA{w, xs, off}:
685 lin_load3(i, o, pa_second(TA.Mat, TA.Mat<1n, o>, w, Mat.restore(1n, o, b, xs, off)))
686
687 def lin_load(+i: Nat, +o: Nat, p: Lin, xs: Array, off: U32) -> PA>:
688 match p:
689 case Lin{w, b}: lin_load2(i, o, b, Mat.restore(o, i, w, xs, off))
690
691 def lin_store2(+i: Nat, +o: Nat, b: TA.Mat<1n, o>, r: PA>) -> PA>:
692 match r:
693 case PA{w, xs, off}:
694 lin_load3(i, o, pa_second(TA.Mat, TA.Mat<1n, o>, w, Mat.store(1n, o, b, xs, off)))
695
696 def lin_store(+i: Nat, +o: Nat, p: Lin, xs: Array, off: U32) -> PA>:
697 match p:
698 case Lin{w, b}: lin_store2(i, o, b, Mat.store(o, i, w, xs, off))
699
700 def lin_zeros(+i: Nat, +o: Nat) -> Lin:
701 Lin{TA.Mat.zeros(o, i), TA.Mat.zeros(1n, o)}
702
703 # a linear layer from i to o features on batches of n rows; its products run in 2^par blocks of rows
704 def Linear(+n: Nat, +i: Nat, +o: Nat, +par: Nat) -> Layer, TA.Mat, TA.Mat, TA.Mat>:
705 Layer{p => x => lin_fwd(n, i, o, par, p, x), p => s => dy => g => lin_bwd(n, i, o, par, p, s, dy, g), op => p => g => st => lin_step(i, o, op, p, g, st), u => lin_zeros(i, o), p => lin_dump(i, o, p), p => xs => off => lin_load(i, o, p, xs, off), p => xs => off => lin_store(i, o, p, xs, off), u => U32.from_nat(Nat.mul(o, 1n+i))}
706
707 # the first layer of a model, whose input (the data) needs no gradient, as an input without requires_grad in PyTorch:
708 # the backward pass computes dW and db only, skipping dX = dY·W (a third of a linear layer's work), and returns zeros
709 # as dX. Use it only where nothing before the layer has parameters.
710 def linf_b3(+n: Nat, +i: Nat, +o: Nat, w: TA.Mat, b: TA.Mat<1n, o>, gb: TA.Mat<1n, o>, r: TA.MMulTN) -> Bwd, TA.Mat>:
711 match r:
712 case TA.MMulTN{dy, x, gw}:
713 lin_b4(n, i, o, w, b, TA.Mat.zeros(n, i), gw, TA.Mat.col_sums_acc(n, o, dy, gb))
714
715 def linf_bwd(+n: Nat, +i: Nat, +o: Nat, +par: Nat, p: Lin, x: TA.Mat, dy: TA.Mat, g: Lin) -> Bwd, TA.Mat>:
716 match p g:
717 case Lin{w, b} Lin{gw, gb}:
718 linf_b3(n, i, o, w, b, gb, TA.Mat.matmul_tn_acc(o, n, i, par, dy, x, gw))
719
720 def LinearFirst(+n: Nat, +i: Nat, +o: Nat, +par: Nat) -> Layer, TA.Mat, TA.Mat, TA.Mat>:
721 Layer{p => x => lin_fwd(n, i, o, par, p, x), p => s => dy => g => linf_bwd(n, i, o, par, p, s, dy, g), op => p => g => st => lin_step(i, o, op, p, g, st), u => lin_zeros(i, o), p => lin_dump(i, o, p), p => xs => off => lin_load(i, o, p, xs, off), p => xs => off => lin_store(i, o, p, xs, off), u => U32.from_nat(Nat.mul(o, 1n+i))}
722
723 # =====================================================================================================
724 # Layers without parameters
725 # =====================================================================================================
726
727 def pick(b: Bool, +x: F32, +y: F32) -> F32:
728 match b:
729 case True{}:
730 x
731 case False{}:
732 y
733
734 def no_step(-P: Type, +op: Opt, p: P, g: P, st: OptState) -> Step
:
735 Step{p, g, st}
736
737 # an element-wise activation that saves a copy of its output (relu, sigmoid, tanh: their derivative is a function of
738 # the output). `f` is the activation, `d(dy, y)` the gradient of its input.
739 def act_f2(-n: Nat, -c: Nat, r: TA.MM2) -> Fwd, TA.Mat>:
740 match r:
741 case TA.MM2{y, s}:
742 Fwd{Unit{}, y, s}
743
744 def act_b2(-n: Nat, -c: Nat, r: TA.MPair) -> Bwd>:
745 match r:
746 case TA.MPair{dx, y}:
747 Bwd{Unit{}, dx, Unit{}}
748
749 def ActOut(~f: @+a: F32 -> F32, ~d: @+a: F32 -> @+b: F32 -> F32, +n: Nat, +c: Nat) -> Layer, TA.Mat, TA.Mat>:
750 Layer{p => x => act_f2(n, c, TA.Mat.clone(n, c, Ew.map(~f, n, c, x))), p => s => dy => g => act_b2(n, c, Ew.zip(~d, n, c, dy, s)), op => p => g => st => no_step(Unit, op, p, g, st), u => Unit{}, p => PL{p, Nil{}}, p => xs => off => PA{p, xs, off}, p => xs => off => PA{p, xs, off}, u => 0}
751
752 # relu(x) = x unless x < 0 (so relu(NaN) = NaN and relu(-0) = -0); its gradient is +0 where the output is <= 0 and
753 # passes everywhere else, NaN included (PyTorch's threshold_backward)
754 def relu_v(+x: F32) -> F32:
755 pick(F32.is_lt(x, 0.0), 0.0, x)
756
757 def relu_d(+dy: F32, +y: F32) -> F32:
758 pick(F32.is_le(y, 0.0), 0.0, dy)
759
760 def ReLU(+n: Nat, +c: Nat) -> Layer, TA.Mat, TA.Mat>:
761 ActOut(~relu_v, ~relu_d, n, c)
762
763 # =====================================================================================================
764 # Seq: la then lb. Parameters, gradients, saved values and optimizer state are pairs of the two layers'.
765 # =====================================================================================================
766
767 def seq_f3(-P1: Type, -P2: Type, -Z: Type, -S1: Type, -S2: Type, p1: P1, s1: S1, r: Fwd) -> Fwd, Z, Two>:
768 match r:
769 case Fwd{p2, z, s2}:
770 Fwd{Two{p1, p2}, z, Two{s1, s2}}
771
772 def seq_f2(~P2: Type, ~Y: Type, ~Z: Type, ~S2: Type, ~lb: Layer, -P1: Type, -S1: Type, p2: P2, r: Fwd) -> Fwd, Z, Two>:
773 match r:
774 case Fwd{p1, y, s1}:
775 seq_f3(P1, P2, Z, S1, S2, p1, s1, Layer.fwd(P2, Y, Z, S2, lb, p2, y))
776
777 def seq_fwd(~P1: Type, ~P2: Type, ~X: Type, ~Y: Type, ~Z: Type, ~S1: Type, ~S2: Type, ~la: Layer, ~lb: Layer, p: Two, x: X) -> Fwd, Z, Two>:
778 match p:
779 case Two{p1, p2}:
780 seq_f2(~P2, ~Y, ~Z, ~S2, ~lb, P1, S1, p2, Layer.fwd(P1, X, Y, S1, la, p1, x))
781
782 def seq_b3(-P1: Type, -P2: Type, -X: Type, p2: P2, g2: P2, r: Bwd) -> Bwd, X>:
783 match r:
784 case Bwd{p1, dx, g1}:
785 Bwd{Two{p1, p2}, dx, Two{g1, g2}}
786
787 def seq_b2(~P1: Type, ~X: Type, ~Y: Type, ~S1: Type, ~la: Layer, -P2: Type, p1: P1, s1: S1, g1: P1, r: Bwd) -> Bwd, X>:
788 match r:
789 case Bwd{p2, dy, g2}:
790 seq_b3(P1, P2, X, p2, g2, Layer.bwd(P1, X, Y, S1, la, p1, s1, dy, g1))
791
792 def seq_bwd(~P1: Type, ~P2: Type, ~X: Type, ~Y: Type, ~Z: Type, ~S1: Type, ~S2: Type, ~la: Layer, ~lb: Layer, p: Two, s: Two, dz: Z, g: Two) -> Bwd, X>:
793 match p s g:
794 case Two{p1, p2} Two{s1, s2} Two{g1, g2}:
795 seq_b2(~P1, ~X, ~Y, ~S1, ~la, P2, p1, s1, g1, Layer.bwd(P2, Y, Z, S2, lb, p2, s2, dz, g2))
796
797 def seq_s3(-P1: Type, -P2: Type, r1: Step, r2: Step) -> Step>:
798 pair_s3(P1, P2, r1, r2)
799
800 def seq_s2(~P2: Type, ~Y: Type, ~Z: Type, ~S2: Type, ~lb: Layer, -P1: Type, +op: Opt, p2: P2, g2: P2, s2: OptState, r1: Step) -> Step>:
801 seq_s3(P1, P2, r1, Layer.step(P2, Y, Z, S2, lb, op, p2, g2, s2))
802
803 def seq_step2(~P1: Type, ~P2: Type, ~X: Type, ~Y: Type, ~Z: Type, ~S1: Type, ~S2: Type, ~la: Layer