>:
72 match a b:
73 case OneMom{x} OneMom{y}: OneMom{Two{x, y}}
74 case TwoMom{x, u} TwoMom{y, v}: TwoMom{Two{x, y}, Two{u, v}}
75 case _ _: NoMom{}
76
77 # what an optimizer step returns: the new parameters, the zeroed gradient, the optimizer state
78 type Step<-P: Type> is Type:
79 Step{p: P, g: P, st: OptState}
80
81 # The optimizer, with the scalars of the current step. Adam's `step` is lr / (1 - b1^t) and `bc2` is
82 # sqrt(1 - b2^t), as PyTorch computes them; `Opt.at` fills them from an `OptCfg` and the step count.
83 # `dec` picks AdamW's decoupled weight decay (w *= 1 - lr*wd) over Adam's (g += wd*w).
84 type Opt is Data:
85 SGD{lr: F32}
86 Momentum{lr: F32, mu: F32}
87 Adam{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, dec: Bool, step: F32, bc2: F32, c1: F32, c2: F32}
88
89 # Parameters and their flat dump in a fixed order. Loading has a separate array cursor (PA).
90 type PL<-P: Type> is Type:
91 PL{p: P, xs: List<&2, F32>}
92
93 # Loading cursor: the shared source array and next element offset, handed back without a list or a clone.
94 # The IO boundary checks the source length against Layer.count before calling the pure loaders.
95 type PA<-P: Type> is Type:
96 PA{p: P, xs: Array, off: U32}
97
98 type Layer<-P: Type, -X: Type, -Y: Type, -S: Type> is Type:
99 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}
100
101 def Layer.fwd(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, x: X) -> Fwd
:
102 match l:
103 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
104 fwd(p, x)
105
106 def Layer.bwd(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, s: S, dy: Y, g: P) -> Bwd
:
107 match l:
108 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
109 bwd(p, s, dy, g)
110
111 def Layer.step(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, +o: Opt, p: P, g: P, st: OptState
) -> Step
:
112 match l:
113 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
114 step(o, p, g, st)
115
116 def Layer.zeros(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
) -> P:
117 match l:
118 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
119 zeros(Unit{})
120
121 # every number of the parameters, in the layer's fixed order (the parameters handed back)
122 def Layer.dump(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P) -> PL
:
123 match l:
124 case Layer{fwd, bwd, step, zeros, dump, load, store, count}:
125 dump(p)
126
127 # Parameters overwritten from xs at an element offset, in dump order; xs and the next offset come back.
128 def Layer.load(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, xs: Array, off: U32) -> PA:
129 match l:
130 case Layer{fwd, bwd, step, zeros, dump, load, store, count}: load(p, xs, off)
131
132 # The parameters copied into xs from an element offset, in dump order (load's inverse); xs and the next offset come back.
133 def Layer.store(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
, p: P, xs: Array, off: U32) -> PA:
134 match l:
135 case Layer{fwd, bwd, step, zeros, dump, load, store, count}: store(p, xs, off)
136
137 def Layer.count(-P: Type, -X: Type, -Y: Type, -S: Type, l: Layer
) -> U32:
138 match l:
139 case Layer{fwd, bwd, step, zeros, dump, load, store, count}: count(Unit{})
140
141 # =====================================================================================================
142 # Element-wise kernels (templates: one copy per function, no closure at run time)
143 # =====================================================================================================
144
145 # a[t] = f(a[t]) for the n numbers from t; ra holds the array and a[t], already read
146 def ew1_l(~f: @+a: F32 -> F32, nleft: Nat, +t: U32, ra: Array & F32) -> Array:
147 match nleft ra:
148 case 0n Tuple{a, v}:
149 a
150 case 1n+p Tuple{a, +v}:
151 ew1_l(~f, p, (t + 1 : U32), Array.get(F32, Array.set(F32, a, t, f(v)), (t + 1 : U32)))
152
153 # a[t] = f(a[t], b[t]): the result replaces a; b comes back unchanged
154 def ew2_l(~f: @+a: F32 -> @+b: F32 -> F32, nleft: Nat, +t: U32, ra: Array & F32, rb: Array & F32) -> TA.P2:
155 match nleft ra rb:
156 case 0n Tuple{a, v} Tuple{b, w}:
157 TA.P2{a, b}
158 case 1n+p Tuple{a, +v} Tuple{b, +w}:
159 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)))
160
161 def Ew.map(~f: @+a: F32 -> F32, +r: Nat, +c: Nat, m: TA.Mat) -> TA.Mat:
162 match m:
163 case TA.Mat{d}:
164 TA.Mat{ew1_l(~f, Nat.mul(r, c), 0, Array.get(F32, d, 0))}
165
166 def ew2_fin(-r: Nat, -c: Nat, p: TA.P2) -> TA.MPair:
167 match p:
168 case TA.P2{a, b}:
169 TA.MPair{TA.Mat{a}, TA.Mat{b}}
170
171 # (f(a, b), b), element by element
172 def Ew.zip(~f: @+a: F32 -> @+b: F32 -> F32, +r: Nat, +c: Nat, a: TA.Mat, b: TA.Mat) -> TA.MPair:
173 match a b:
174 case TA.Mat{x} TA.Mat{y}:
175 ew2_fin(r, c, ew2_l(~f, Nat.mul(r, c), 0, Array.get(F32, x, 0), Array.get(F32, y, 0)))
176
177 def f32_add(+a: F32, +b: F32) -> F32:
178 (a + b : F32)
179
180 def f32_mul(+a: F32, +b: F32) -> F32:
181 (a * b : F32)
182
183 # (a + b, b)
184 def Mat.add(+r: Nat, +c: Nat, a: TA.Mat, b: TA.Mat) -> TA.MPair:
185 Ew.zip(~f32_add, r, c, a, b)
186
187 def zero_of(+x: F32) -> F32:
188 0.0
189
190 # =====================================================================================================
191 # Optimizers: one pass over a parameter, its gradient and the two moment buffers; the gradient is zeroed
192 # =====================================================================================================
193
194 type Q3 is Type:
195 Q3{w: Array, g: Array, m: Array}
196
197 type Q4 is Type:
198 Q4{w: Array, g: Array, m: Array, v: Array}
199
200 type MatStep<-r: Nat, -c: Nat> is Type:
201 MatStep{w: TA.Mat, g: TA.Mat, st: OptState>}
202
203 # w <- w + (-lr)*g, as PyTorch's p.add_(g, alpha=-lr)
204 def sgd_l(nleft: Nat, +t: U32, +lr: F32, rw: Array & F32, rg: Array & F32) -> TA.P2:
205 match nleft rw rg:
206 case 0n Tuple{w, x} Tuple{g, y}:
207 TA.P2{w, g}
208 case 1n+p Tuple{w, +x} Tuple{g, +y}:
209 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)))
210
211 # buf <- buf*mu + g; w <- w + (-lr)*buf, as PyTorch's SGD with momentum (no dampening, not Nesterov)
212 def mom_l(nleft: Nat, +t: U32, +lr: F32, +mu: F32, rw: Array & F32, rg: Array & F32, rm: Array & F32) -> Q3:
213 match nleft rw rg rm:
214 case 0n Tuple{w, x} Tuple{g, y} Tuple{m, b}:
215 Q3{w, g, m}
216 case 1n+p Tuple{w, +x} Tuple{g, +y} Tuple{m, +b}:
217 +nb = ((b * mu : F32) + y : F32)
218 +u = (t + 1 : U32)
219 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))
220
221 # the Adam family, in PyTorch's order of operations (torch.optim.adam._single_tensor_adam):
222 # 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';
223 # w = w' + (-step*m) / (sqrt(v)/bc2 + eps)
224 def adam_grad(dec: Bool, +wd: F32, +w: F32, +g: F32) -> F32:
225 match dec:
226 case True{}:
227 g
228 case False{}:
229 (g + (w * wd : F32) : F32)
230
231 def adam_decay(dec: Bool, +lr: F32, +wd: F32, +w: F32) -> F32:
232 match dec:
233 case True{}:
234 (w * (1.0 - (lr * wd : F32) : F32) : F32)
235 case False{}:
236 w
237
238 # c1 = 1 - b1 and c2 = 1 - b2 are given as their own constants (0.1, 0.001), as PyTorch computes 1 - beta in float64
239 # 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
240 # that alone put the tiny Llama 21x further from float64 than PyTorch's float32 (NOTES, 2026-10-18)
241 def adam_m(+c1: F32, +m: F32, +g: F32) -> F32:
242 (m + (c1 * (g - m : F32) : F32) : F32)
243
244 def adam_v(+b2: F32, +c2: F32, +v: F32, +g: F32) -> F32:
245 ((v * b2 : F32) + ((c2 * g : F32) * g : F32) : F32)
246
247 def adam_w(+step: F32, +bc2: F32, +eps: F32, +w: F32, +m: F32, +v: F32) -> F32:
248 (w + ((F32.neg(step) * m : F32) / ((F32.sqrt(v) / bc2 : F32) + eps : F32) : F32) : F32)
249
250 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:
251 match nleft rw rg rm rv:
252 case 0n Tuple{w, x} Tuple{g, y} Tuple{m, a} Tuple{v, b}:
253 Q4{w, g, m, v}
254 case 1n+p Tuple{w, +x} Tuple{g, +y} Tuple{m, +a} Tuple{v, +b}:
255 +gg = adam_grad(dec, wd, x, y)
256 +nm = adam_m(c1, a, gg)
257 +nv = adam_v(b2, c2, b, gg)
258 +nw = adam_w(step, bc2, eps, adam_decay(dec, lr, wd, x), nm, nv)
259 +u = (t + 1 : U32)
260 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))
261
262 # One matrix step, with only the buffers this optimizer reads.
263 def Opt.sgd_fin(-r: Nat, -c: Nat, q: TA.P2) -> MatStep:
264 match q:
265 case TA.P2{w, g}: MatStep{TA.Mat{w}, TA.Mat{g}, NoMom{}}
266
267 def Opt.mom_fin(-r: Nat, -c: Nat, q: Q3) -> MatStep:
268 match q:
269 case Q3{w, g, m}: MatStep{TA.Mat{w}, TA.Mat{g}, OneMom{TA.Mat{m}}}
270
271 def Opt.fin(-r: Nat, -c: Nat, q: Q4) -> MatStep:
272 match q:
273 case Q4{w, g, m, v}: MatStep{TA.Mat{w}, TA.Mat{g}, TwoMom{TA.Mat{m}, TA.Mat{v}}}
274
275 # A mismatched state is initialized only at the training boundary; Opt.mat requires the matching variant.
276 # The fallback leaves the values untouched, so it cannot fabricate a moment buffer.
277 def Opt.mat(+r: Nat, +c: Nat, +o: Opt, w: TA.Mat, g: TA.Mat, st: OptState>) -> MatStep:
278 match o w g st:
279 case SGD{lr} TA.Mat{wd} TA.Mat{gd} _:
280 Opt.sgd_fin(r, c, sgd_l(Nat.mul(r, c), 0, lr, Array.get(F32, wd, 0), Array.get(F32, gd, 0)))
281 case Momentum{lr, mu} TA.Mat{wd} TA.Mat{gd} OneMom{TA.Mat{md}}:
282 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)))
283 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}}:
284 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)))
285 case _ w g st:
286 MatStep{w, g, st}
287
288 # 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
289 # first step). Adam's c1 and c2 are 1 - b1 and 1 - b2, written as their own constants (adam_m above).
290 type OptCfg is Data:
291 CSGD{lr: F32}
292 CMomentum{lr: F32, mu: F32}
293 CAdam{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, c1: F32, c2: F32}
294 CAdamW{lr: F32, b1: F32, b2: F32, eps: F32, wd: F32, c1: F32, c2: F32}
295
296 def Opt.at(+cfg: OptCfg, +q1: F32, +q2: F32) -> Opt:
297 match cfg:
298 case CSGD{lr}:
299 SGD{lr}
300 case CMomentum{lr, mu}:
301 Momentum{lr, mu}
302 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
303 Adam{lr, b1, b2, eps, wd, False{}, (lr / q1 : F32), F32.sqrt(q2), c1, c2}
304 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
305 Adam{lr, b1, b2, eps, wd, True{}, (lr / q1 : F32), F32.sqrt(q2), c1, c2}
306
307 def OptCfg.b1(+cfg: OptCfg) -> F32:
308 match cfg:
309 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
310 b1
311 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
312 b1
313 case _:
314 0.0
315
316 def OptCfg.b2(+cfg: OptCfg) -> F32:
317 match cfg:
318 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
319 b2
320 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
321 b2
322 case _:
323 0.0
324
325 def OptCfg.c1(+cfg: OptCfg) -> F32:
326 match cfg:
327 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
328 c1
329 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
330 c1
331 case _:
332 0.0
333
334 def OptCfg.c2(+cfg: OptCfg) -> F32:
335 match cfg:
336 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
337 c2
338 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
339 c2
340 case _:
341 0.0
342
343 # 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
344 # 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
345 # 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.
346 def beta_ok(+b: F32, +c: F32) -> Bool:
347 +d = ((1.0 - b : F32) - c : F32)
348 Bool.and(Bool.and((c > 0.0 : F32), (c <= 1.0 : F32)), ((d * d : F32) <= 0.0000000000000142108547152020037174224853515625 : F32))
349
350 # True unless an Adam configuration's c1, c2 disagree with its b1, b2. Train.new poisons the state of a configuration
351 # that fails (every loss after the first is NaN); a program checks it first with OptCfg.check to stop with a message.
352 def OptCfg.ok(+cfg: OptCfg) -> Bool:
353 match cfg:
354 case CAdam{lr, b1, b2, eps, wd, c1, c2}:
355 Bool.and(beta_ok(b1, c1), beta_ok(b2, c2))
356 case CAdamW{lr, b1, b2, eps, wd, c1, c2}:
357 Bool.and(beta_ok(b1, c1), beta_ok(b2, c2))
358 case _:
359 True{}
360
361 def cfg_stop(ok: Bool) -> IO(Unit):
362 match ok:
363 case True{}:
364 IO.pure(Unit, Unit{})
365 case False{}:
366 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)")
367
368 def OptCfg.check(+cfg: OptCfg) -> IO(Unit):
369 cfg_stop(OptCfg.ok(cfg))
370
371 # dump and load for a matrix: its r*c numbers, row by row
372 def mat_dump_fin(-r: Nat, -c: Nat, ml: TA.ML) -> PL>:
373 match ml:
374 case TA.ML{m, xs}:
375 PL{m, xs}
376
377 def Mat.dump(+r: Nat, +c: Nat, m: TA.Mat) -> PL>:
378 mat_dump_fin(r, c, TA.Mat.to_list(r, c, m))
379
380 def load_cells(left: Nat, +i: U32, +off: U32, rx: Array & F32, dst: Array) -> TA.P2:
381 match left rx:
382 case 0n Tuple{xs, v}: TA.P2{xs, dst}
383 case 1n+q Tuple{xs, v}:
384 load_cells(q, (i + 1 : U32), (off + 1 : U32), Array.get(F32, xs, (off + 1 : U32)), Array.set(F32, dst, i, v))
385
386 def mat_restore_fin(-r: Nat, -c: Nat, +off: U32, q: TA.P2) -> PA>:
387 match q:
388 case TA.P2{xs, dst}: PA{TA.Mat{dst}, xs, off}
389
390 def Mat.restore(+r: Nat, +c: Nat, m: TA.Mat, xs: Array, +off: U32) -> PA>:
391 match m:
392 case TA.Mat{dst}:
393 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))
394
395 # load_cells' inverse: copies the matrix's cells i.. into xs from `off`; both arrays come back (the matrix's first)
396 def store_cells(left: Nat, +i: U32, +off: U32, rm: Array & F32, xs: Array) -> TA.P2:
397 match left rm:
398 case 0n Tuple{ma, v}: TA.P2{ma, xs}
399 case 1n+q Tuple{ma, v}:
400 store_cells(q, (i + 1 : U32), (off + 1 : U32), Array.get(F32, ma, (i + 1 : U32)), Array.set(F32, xs, off, v))
401
402 def mat_store_fin(-r: Nat, -c: Nat, +off: U32, q: TA.P2) -> PA>:
403 match q:
404 case TA.P2{ma, xs}: PA{TA.Mat{ma}, xs, off}
405
406 def Mat.store(+r: Nat, +c: Nat, m: TA.Mat, xs: Array, +off: U32) -> PA>:
407 match m:
408 case TA.Mat{ma}:
409 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))
410
411 def pa_second(-A: Type, -B: Type, a: A, rb: PA) -> PA>:
412 match rb:
413 case PA{b, xs, off}: PA{Two{a, b}, xs, off}
414
415 def pl_pair(-A: Type, -B: Type, ra: PL, rb: PL) -> PL>:
416 match ra rb:
417 case PL{a, xa} PL{b, xb}:
418 PL{Two{a, b}, List.append(&2, F32, xa, xb)}
419
420 def pl_second(-A: Type, -B: Type, a: A, rb: PL) -> PL>:
421 match rb:
422 case PL{b, rest}:
423 PL{Two{a, b}, rest}
424
425 # =====================================================================================================
426 # Linear: y = x·Wᵀ + b, with W stored out x in (PyTorch's nn.Linear layout); saves its input
427 # =====================================================================================================
428
429 type Lin<-i: Nat, -o: Nat> is Type:
430 Lin{w: TA.Mat, b: TA.Mat<1n, o>}
431
432 def lin_f3(-n: Nat, -i: Nat, -o: Nat, w: TA.Mat, x: TA.Mat, r: TA.MBias) -> Fwd, TA.Mat, TA.Mat>:
433 match r:
434 case TA.MBias{y, b}:
435 Fwd{Lin{w, b}, y, x}
436
437 def lin_f2(+n: Nat, -i: Nat, +o: Nat, b: TA.Mat<1n, o>, r: TA.MMulNT) -> Fwd, TA.Mat, TA.Mat>:
438 match r:
439 case TA.MMulNT{x, w, z}:
440 lin_f3(n, i, o, w, x, TA.Mat.add_row(n, o, z, b))
441
442 def lin_fwd(+n: Nat, +i: Nat, +o: Nat, par: Nat, p: Lin, x: TA.Mat) -> Fwd, TA.Mat, TA.Mat>:
443 match p:
444 case Lin{w, b}:
445 lin_f2(n, i, o, b, TA.Mat.matmul_nt(n, i, o, par, x, w))
446
447 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>:
448 match r:
449 case TA.MColSum{dy, gb}:
450 Bwd{Lin{w, b}, dx, Lin{gw, gb}}
451
452 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>:
453 match r:
454 case TA.MMulTN{dy, x, gw}:
455 lin_b4(n, i, o, w, b, dx, gw, TA.Mat.col_sums_acc(n, o, dy, gb))
456
457 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>:
458 match g r:
459 case Lin{gw, gb} TA.MMul{dy, w, dx}:
460 lin_b3(n, i, o, w, b, dx, gb, TA.Mat.matmul_tn_acc(o, n, i, par, dy, x, gw))
461
462 # dX = dY·W, dW += dYᵀ·X, db += the column sums of dY; the two sums are added straight into the gradient (the
463 # accumulating products of bend-ml-tensor-array 0.1.10.0), with the bits of computing them apart and adding them
464 def lin_bwd(+n: Nat, +i: Nat, +o: Nat, +par: Nat, p: Lin, x: TA.Mat, dy: TA.Mat, g: Lin) -> Bwd, TA.Mat>:
465 match p:
466 case Lin{w, b}:
467 lin_b2(n, i, o, par, b, x, g, TA.Mat.matmul_precise(n, o, i, par, dy, w))
468
469 def pair_s3(-P1: Type, -P2: Type, r1: Step, r2: Step) -> Step>:
470 match r1 r2:
471 case Step{p1, g1, s1} Step{p2, g2, s2}:
472 Step{Two{p1, p2}, Two{g1, g2}, OptState.join(P1, P2, s1, s2)}
473
474 def lin_pair(-i: Nat, -o: Nat, p: Lin) -> Two, TA.Mat<1n, o>>:
475 match p:
476 case Lin{w, b}: Two{w, b}
477
478 def lin_unpair(-i: Nat, -o: Nat, p: Two, TA.Mat<1n, o>>) -> Lin:
479 match p:
480 case Two{w, b}: Lin{w, b}
481
482 def lin_s2(-i: Nat, -o: Nat, a: MatStep, c: MatStep<1n, o>) -> Step>:
483 match a c:
484 case MatStep{w, gw, sw} MatStep{b, gb, sb}:
485 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))}
486
487 def lin_step2(+i: Nat, +o: Nat, +op: Opt, p: Lin, g: Lin, st: Two>, OptState>>) -> Step>:
488 match p g st:
489 case Lin{w, b} Lin{gw, gb} Two{sw, sb}:
490 lin_s2(i, o, Opt.mat(o, i, op, w, gw, sw), Opt.mat(1n, o, op, b, gb, sb))
491
492 def lin_step(+i: Nat, +o: Nat, +op: Opt, p: Lin, g: Lin, st: OptState>) -> Step>:
493 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))))
494
495 def lin_dump2(-i: Nat, -o: Nat, r: PL, TA.Mat<1n, o>>>) -> PL>:
496 match r:
497 case PL{Two{w, b}, xs}:
498 PL{Lin{w, b}, xs}
499
500 def lin_dump(+i: Nat, +o: Nat, p: Lin) -> PL>:
501 match p:
502 case Lin{w, b}:
503 lin_dump2(i, o, pl_pair(TA.Mat, TA.Mat<1n, o>, Mat.dump(o, i, w), Mat.dump(1n, o, b)))
504
505 def lin_load3(-i: Nat, -o: Nat, r: PA, TA.Mat<1n, o>>>) -> PA>:
506 match r:
507 case PA{Two{w, b}, xs, off}: PA{Lin{w, b}, xs, off}
508
509 def lin_load2(+i: Nat, +o: Nat, b: TA.Mat<1n, o>, r: PA>) -> PA>:
510 match r:
511 case PA{w, xs, off}:
512 lin_load3(i, o, pa_second(TA.Mat, TA.Mat<1n, o>, w, Mat.restore(1n, o, b, xs, off)))
513
514 def lin_load(+i: Nat, +o: Nat, p: Lin, xs: Array, off: U32) -> PA>:
515 match p:
516 case Lin{w, b}: lin_load2(i, o, b, Mat.restore(o, i, w, xs, off))
517
518 def lin_store2(+i: Nat, +o: Nat, b: TA.Mat<1n, o>, r: PA>) -> PA>:
519 match r:
520 case PA{w, xs, off}:
521 lin_load3(i, o, pa_second(TA.Mat, TA.Mat<1n, o>, w, Mat.store(1n, o, b, xs, off)))
522
523 def lin_store(+i: Nat, +o: Nat, p: Lin, xs: Array, off: U32) -> PA>:
524 match p:
525 case Lin{w, b}: lin_store2(i, o, b, Mat.store(o, i, w, xs, off))
526
527 def lin_zeros(+i: Nat, +o: Nat) -> Lin:
528 Lin{TA.Mat.zeros(o, i), TA.Mat.zeros(1n, o)}
529
530 # a linear layer from i to o features on batches of n rows; its products run in 2^par blocks of rows
531 def Linear(+n: Nat, +i: Nat, +o: Nat, +par: Nat) -> Layer, TA.Mat, TA.Mat, TA.Mat>:
532 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))}
533
534 # the first layer of a model, whose input (the data) needs no gradient, as an input without requires_grad in PyTorch:
535 # the backward pass computes dW and db only, skipping dX = dY·W (a third of a linear layer's work), and returns zeros
536 # as dX. Use it only where nothing before the layer has parameters.
537 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>:
538 match r:
539 case TA.MMulTN{dy, x, gw}:
540 lin_b4(n, i, o, w, b, TA.Mat.zeros(n, i), gw, TA.Mat.col_sums_acc(n, o, dy, gb))
541
542 def linf_bwd(+n: Nat, +i: Nat, +o: Nat, +par: Nat, p: Lin, x: TA.Mat, dy: TA.Mat, g: Lin) -> Bwd, TA.Mat>:
543 match p g:
544 case Lin{w, b} Lin{gw, gb}:
545 linf_b3(n, i, o, w, b, gb, TA.Mat.matmul_tn_acc(o, n, i, par, dy, x, gw))
546
547 def LinearFirst(+n: Nat, +i: Nat, +o: Nat, +par: Nat) -> Layer, TA.Mat, TA.Mat, TA.Mat>:
548 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))}
549
550 # =====================================================================================================
551 # Layers without parameters
552 # =====================================================================================================
553
554 def pick(b: Bool, +x: F32, +y: F32) -> F32:
555 match b:
556 case True{}:
557 x
558 case False{}:
559 y
560
561 def no_step(-P: Type, +op: Opt, p: P, g: P, st: OptState) -> Step
:
562 Step{p, g, st}
563
564 # an element-wise activation that saves a copy of its output (relu, sigmoid, tanh: their derivative is a function of
565 # the output). `f` is the activation, `d(dy, y)` the gradient of its input.
566 def act_f2(-n: Nat, -c: Nat, r: TA.MM2) -> Fwd, TA.Mat>:
567 match r:
568 case TA.MM2{y, s}:
569 Fwd{Unit{}, y, s}
570
571 def act_b2(-n: Nat, -c: Nat, r: TA.MPair) -> Bwd>:
572 match r:
573 case TA.MPair{dx, y}:
574 Bwd{Unit{}, dx, Unit{}}
575
576 def ActOut(~f: @+a: F32 -> F32, ~d: @+a: F32 -> @+b: F32 -> F32, +n: Nat, +c: Nat) -> Layer, TA.Mat, TA.Mat>:
577 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}
578
579 # relu(x) = x unless x < 0 (so relu(NaN) = NaN and relu(-0) = -0); its gradient is +0 where the output is <= 0 and
580 # passes everywhere else, NaN included (PyTorch's threshold_backward)
581 def relu_v(+x: F32) -> F32:
582 pick(F32.is_lt(x, 0.0), 0.0, x)
583
584 def relu_d(+dy: F32, +y: F32) -> F32:
585 pick(F32.is_le(y, 0.0), 0.0, dy)
586
587 def ReLU(+n: Nat, +c: Nat) -> Layer, TA.Mat, TA.Mat>:
588 ActOut(~relu_v, ~relu_d, n, c)
589
590 # =====================================================================================================
591 # Seq: la then lb. Parameters, gradients, saved values and optimizer state are pairs of the two layers'.
592 # =====================================================================================================
593
594 def seq_f3(-P1: Type, -P2: Type, -Z: Type, -S1: Type, -S2: Type, p1: P1, s1: S1, r: Fwd) -> Fwd, Z, Two>:
595 match r:
596 case Fwd{p2, z, s2}:
597 Fwd{Two{p1, p2}, z, Two{s1, s2}}
598
599 def seq_f2(~P2: Type, ~Y: Type, ~Z: Type, ~S2: Type, ~lb: Layer, -P1: Type, -S1: Type, p2: P2, r: Fwd) -> Fwd, Z, Two>:
600 match r:
601 case Fwd{p1, y, s1}:
602 seq_f3(P1, P2, Z, S1, S2, p1, s1, Layer.fwd(P2, Y, Z, S2, lb, p2, y))
603
604 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>:
605 match p:
606 case Two{p1, p2}:
607 seq_f2(~P2, ~Y, ~Z, ~S2, ~lb, P1, S1, p2, Layer.fwd(P1, X, Y, S1, la, p1, x))
608
609 def seq_b3(-P1: Type, -P2: Type, -X: Type, p2: P2, g2: P2, r: Bwd) -> Bwd, X>:
610 match r:
611 case Bwd{p1, dx, g1}:
612 Bwd{Two{p1, p2}, dx, Two{g1, g2}}
613
614 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>:
615 match r:
616 case Bwd{p2, dy, g2}:
617 seq_b3(P1, P2, X, p2, g2, Layer.bwd(P1, X, Y, S1, la, p1, s1, dy, g1))
618
619 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>:
620 match p s g:
621 case Two{p1, p2} Two{s1, s2} Two{g1, g2}:
622 seq_b2(~P1, ~X, ~Y, ~S1, ~la, P2, p1, s1, g1, Layer.bwd(P2, Y, Z, S2, lb, p2, s2, dz, g2))
623
624 def seq_s3(-P1: Type, -P2: Type, r1: Step, r2: Step) -> Step>:
625 pair_s3(P1, P2, r1, r2)
626
627 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>:
628 seq_s3(P1, P2, r1, Layer.step(P2, Y, Z, S2, lb, op, p2, g2, s2))
629
630 def seq_step2(~P1: Type, ~P2: Type, ~X: Type, ~Y: Type, ~Z: Type, ~S1: Type, ~S2: Type, ~la: Layer, ~lb: Layer, +op: Opt, p: Two, g: Two, st: Two, OptState>) -> Step>:
631 match p g st:
632 case Two{p1, p2} Two{g1, g2} Two{s1, s2}:
633 seq_s2(~P2, ~Y, ~Z, ~S2, ~lb, P1, op, p2, g2, s2, Layer.step(P1, X, Y, S1, la, op, p1, g1, s1))
634
635 def seq_step(~P1: Type, ~P2: Type, ~X: Type, ~Y: Type, ~Z: Type, ~S1: Type, ~S2: Type, ~la: Layer, ~lb: Layer, +op: Opt, p: Two, g: Two, st: OptState>) -> Step>:
636 seq_step2(~P1, ~P2, ~X, ~Y, ~Z, ~S1, ~S2, ~la, ~lb, op, p, g, OptState.split(P1, P2, st))
637
638 # dump and load for a pair of layers: the first layer's numbers, then the second's
639 def pair_dump(~P1: Type, ~X1: Type, ~Y1: Type, ~S1: Type, ~P2: Type, ~X2: Type, ~Y2: Type, ~S2: Type, ~la: Layer, ~lb: Layer, p: Two) -> PL>:
640 match p:
641 case Two{p1, p2}:
642 pl_pair(P1, P2, Layer.dump(P1, X1, Y1, S1, la, p1), Layer.dump(P2, X2, Y2, S2, lb, p2))
643
644 def pair_load2(~P2: Type, ~X2: Type, ~Y2: Type, ~S2: Type, ~lb: Layer, -P1: Type, p2: P2, r: PA) -> PA>:
645 match r:
646 case PA{p1, xs, off}:
647 pa_second(P1, P2, p1, Layer.load(P2, X2, Y2, S2, lb, p2, xs, off))
648
649 def pair_load(~P1: Type, ~X1: Type, ~Y1: Type, ~S1: Type, ~P2: Type, ~X2: Type, ~Y2: Type, ~S2: Type, ~la: Layer, ~lb: Layer, p: Two, xs: Array, off: U32) -> PA>:
650 match p:
651 case Two{p1, p2}:
652 pair_load2(~P2, ~X2, ~Y2, ~S2, ~lb, P1, p2, Layer.load(P1, X1, Y1, S1, la, p1, xs, off))
653
654 def pair_store2(~P2: Type, ~X2: Type, ~Y2: Type, ~S2: Type, ~lb: Layer, -P1: Type, p2: P2, r: PA) -> PA>:
655 match r:
656 case PA{p1, xs, off}:
657 pa_second(P1, P2, p1, Layer.store(P2, X2, Y2, S2, lb, p2, xs, off))
658
659 def pair_store(~P1: Type, ~X1: Type, ~Y1: Type, ~S1: Type, ~P2: Type, ~X2: Type, ~Y2: Type, ~S2: Type, ~la: Layer, ~lb: Layer