sourcework/xetal-ml/libs/LoRA/src/LoRA.xtl

1⍝│ LoRA: low-rank adapters: the adapted forward pass in factored form, the merge, the counts, the gradients and steps, LoRA-FA 2⍝│ 3⍝│ Import it with an alias of your choice: "lo:" u_se< "LoRA". 4⍝│ Put libs/LoRA/src on XETAL_PATH ("just path"); the reference is libs/LoRA/docs. 5⍝│ Names with l: are exported; h: names are private to this file. 6⍝│ 7⍝│ A frozen weight matrix W (m x n: m outputs, n inputs) is adapted by 8⍝│ a low-rank update s B A: A is r x n, B is m x r, the rank r much 9⍝│ smaller than m and n, s the scale alpha / r. An adapter is the tuple 10⍝│ (W, A, B, s). The inputs are columns: x is one input of n numbers or 11⍝│ an n x N matrix of N inputs, and W x is W '+ '× i̲nner x. The 12⍝│ forward pass keeps the factors apart, B (A x), as training does: 13⍝│ the merged matrix W + s B A is made only to deploy it. 14⍝│ 15⍝│ Training moves A and B by the gradients of the squared error 16⍝│ (g_rads, s_gd, a_dam through the Optim library); LoRA-FA freezes A 17⍝│ (regenerated from a seed, s_eeded) and trains B alone. 18 19"op:" u̲se< "Optim" 20 21⍝━━ The forward pass 22 23⍝│ alpha s̲caling r: the adapter's scale s, alpha / r (so that a 24⍝│ change of rank keeps the update's size). 25⍝│ >> "lo:" u_se< "LoRA" 26⍝│ >> 16.0 lo:s_caling 8 27⍝│ 2.0 28ˡs̲caling ← { alpha r → alpha ÷ f̲loat r } 29 30⍝│ alpha s̲calingRs r: rsLoRA's rank-stabilized scale, alpha / sqrt r 31⍝│ (Kalajdzievski, "A Rank Stabilization Scaling Factor for Fine-Tuning 32⍝│ with LoRA", 2023): with alpha / r the update's learning slows as r 33⍝│ grows; over sqrt r it does not. An adapter takes either, (W, A, B, s). 34⍝│ >> "lo:" u_se< "LoRA" 35⍝│ >> 16.0 lo:s_calingRs 4 36⍝│ 8.0 37ˡs̲calingRs ← { alpha r → alpha ÷ s̲qrt f̲loat r } 38 39⍝│ (W, A, B, s) f̲orward x: the adapted layer, W x + s B (A x), the 40⍝│ update in its factored form: A x (r numbers per input), then B of 41⍝│ that, never the m x n matrix B A. 42⍝│ >> "lo:" u_se< "LoRA" 43⍝│ >> (2 2 r_eshape 1.0 0.0 0.0 1.0, 1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5) lo:f_orward 2.0 4.0 44⍝│ 5.0 1.0 45ˡf̲orward ← { (W, A, B, s) x → (W '+ '× i̲nner x) + s × B '+ '× i̲nner A '+ '× i̲nner x } 46 47⍝━━ The merge 48 49⍝│ d̲elta (A, B, s): the update itself, s B A, an m x n matrix of 50⍝│ rank r at most. 51⍝│ >> "lo:" u_se< "LoRA" 52⍝│ >> lo:d_elta (1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5) 53⍝│ 0.5 0.5 54⍝│ -0.5 -0.5 55ˡd̲elta ← { (A, B, s) → s × B '+ '× i̲nner A } 56 57⍝│ m̲erge (W, A, B, s): the adapter folded into the weights, W + s B 58⍝│ A: one matrix again, as fast as the base layer, for one task. 59⍝│ >> "lo:" u_se< "LoRA" 60⍝│ >> lo:m_erge (2 2 r_eshape 1.0 0.0 0.0 1.0, 1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5) 61⍝│ 1.5 0.5 62⍝│ -0.5 0.5 63ˡm̲erge ← { (W, A, B, s) → W + ˡd̲elta (A, B, s) } 64 65⍝━━ Gradients 66 67⍝│ (W, A, B, s) l̲oss (x, y): the squared error of the adapted layer 68⍝│ on the inputs x (columns) against the wanted outputs y, half the sum 69⍝│ of the squared differences, so that its gradient at the output, g, 70⍝│ is the difference itself. 71⍝│ >> "lo:" u_se< "LoRA" 72⍝│ >> (2 2 r_eshape 1.0 0.0 0.0 1.0, 1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5) lo:l_oss (2 1 r_eshape 2.0 4.0, 2 1 r_eshape 4.0 2.0) 73⍝│ 1.0 74ˡl̲oss ← { a (x, y) → 75 e ← (a ˡf̲orward x) − y 76 0.5 × '+ r̲/ r̲avel e × e 77} 78 79⍝│ (W, A, B, s) g̲rads (x, g): the gradients for the inputs x (an n x 80⍝│ N matrix, one input per column) and g, the loss's gradient at the 81⍝│ output (m x N): with z = A x, dL/dA = s (B' g) x', dL/dB = s g z' and 82⍝│ dL/dx = W' g + s A' (B' g), as the tuple (dA, dB, dx). B' g is made 83⍝│ once and used twice; no m x n matrix but W's own is made. 84⍝│ >> "lo:" u_se< "LoRA" 85⍝│ >> (dA, dB, dx) := (2 2 r_eshape 1.0 0.0 0.0 1.0, 1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5) lo:g_rads (2 1 r_eshape 2.0 4.0, 2 1 r_eshape 1.0 -1.0) 86⍝│ >> dB 87⍝│ 3.0 88⍝│ -3.0 89ˡg̲rads ← { (W, A, B, s) (x, g) → 90 b ← (o̲\ B) '+ '× i̲nner g 91 (s × b '+ '× i̲nner o̲\ x, s × g '+ '× i̲nner o̲\ A '+ '× i̲nner x, ((o̲\ W) '+ '× i̲nner g) + s × (o̲\ A) '+ '× i̲nner b) 92} 93 94⍝━━ Training steps 95 96⍝│ lr s̲gd ((W, A, B, s), (x, y)): one step of gradient descent on 97⍝│ the squared error (l_oss) at the rate lr, A and B moved by Optim's 98⍝│ s_gd, W and s left as they are: the adapter after the step. p_ower 99⍝│ repeats it. 100⍝│ >> "lo:" u_se< "LoRA" 101⍝│ >> (_, A, B, _) := 0.1 lo:s_gd ((2 2 r_eshape 1.0 0.0 0.0 1.0, 1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5), (2 1 r_eshape 2.0 4.0, 2 1 r_eshape 4.0 2.0)) 102⍝│ >> B 103⍝│ 0.7 104⍝│ -0.7 105ˡs̲gd ← { lr ((W, A, B, s), (x, y)) → 106 (dA, dB, _) ← (W, A, B, s) ˡg̲rads (x, ((W, A, B, s) ˡf̲orward x) − y) 107 (W, lr ᵒᵖs̲gd (A, dA), lr ᵒᵖs̲gd (B, dB), s) 108} 109 110⍝│ a̲damStart (W, A, B, s): what Adam starts from: the adapter, its 111⍝│ four running averages at 0 (A's two, then B's) and the step count 1. 112ˡa̲damStart ← { (W, A, B, s) → ((W, A, B, s), (0.0 × A, 0.0 × A, 0.0 × B, 0.0 × B), 1) } 113 114⍝│ lr a̲dam (((W, A, B, s), (mA, vA, mB, vB), t), (x, y)): Adam's step 115⍝│ t on the squared error, A and B moved by Optim's a_dam at the rate 116⍝│ lr; the next state, the step count one more. 117⍝│ >> "lo:" u_se< "LoRA" 118⍝│ >> ((_, _, B, _), _, _) := 0.1 lo:a_dam ((lo:a_damStart (2 2 r_eshape 1.0 0.0 0.0 1.0, 1 2 r_eshape 1.0 1.0, 2 1 r_eshape 1.0 -1.0, 0.5)), (2 1 r_eshape 2.0 4.0, 2 1 r_eshape 4.0 2.0)) 119⍝│ >> B 120⍝│ 0.9000000003333333 121⍝│ -0.9000000003333333 122ˡa̲dam ← { lr (((W, A, B, s), (mA, vA, mB, vB), t), (x, y)) → 123 (dA, dB, _) ← (W, A, B, s) ˡg̲rads (x, ((W, A, B, s) ˡf̲orward x) − y) 124 (A2, mA2, vA2) ← (lr, t) ᵒᵖa̲dam (A, mA, vA, dA) 125 (B2, mB2, vB2) ← (lr, t) ᵒᵖa̲dam (B, mB, vB, dB) 126 ((W, A2, B2, s), (mA2, vA2, mB2, vB2), t + 1) 127} 128 129⍝━━ LoRA-FA: A frozen 130 131⍝│ seed s̲eeded r n: an r x n matrix regenerated from a whole-number 132⍝│ seed (LoRA-FA's frozen A, which a checkpoint then need not hold): 133⍝│ sin(seed k k) for k = 1 .. r n, over the square root of n (the 134⍝│ inputs' count, so A x keeps x's size). No chance: the same seed 135⍝│ gives the same matrix everywhere. 136⍝│ >> "lo:" u_se< "LoRA" 137⍝│ >> 7 lo:s_eeded 2 4 138⍝│ 0.32849329935939453 0.13545289415393452 0.08367785015140346 -0.44499780218341667 139⍝│ -0.4005672975890204 0.3115061055018265 -0.26829917759428185 0.47413628607807934 140ˡs̲eeded ← { seed rn → 141 k ← f̲loat r̲ange '× r̲/ rn 142 rn r̲eshape (s̲in (f̲loat seed) × k × k) ÷ s̲qrt f̲loat 2 s̲elect rn 143} 144 145⍝│ (W, A, B, s) g̲radsFA (x, g): LoRA-FA's gradients, (dB, dx): A is 146⍝│ frozen, so its gradient is skipped, but x's still flows back through 147⍝│ A, dx = W' g + s A' (B' g). 148ˡg̲radsFA ← { (W, A, B, s) (x, g) → 149 (s × g '+ '× i̲nner o̲\ A '+ '× i̲nner x, ((o̲\ W) '+ '× i̲nner g) + s × (o̲\ A) '+ '× i̲nner (o̲\ B) '+ '× i̲nner g) 150} 151 152⍝│ lr s̲gdFA ((W, A, B, s), (x, y)): LoRA-FA's step of gradient 153⍝│ descent: B alone moves. Every update s B A it reaches has its rows 154⍝│ in A's row space. 155ˡs̲gdFA ← { lr ((W, A, B, s), (x, y)) → 156 (dB, _) ← (W, A, B, s) ˡg̲radsFA (x, ((W, A, B, s) ˡf̲orward x) − y) 157 (W, A, lr ᵒᵖs̲gd (B, dB), s) 158} 159 160⍝│ a̲damStartFA (W, A, B, s): LoRA-FA's Adam state: the adapter, B's 161⍝│ two running averages at 0 (A has none: half of LoRA's optimizer 162⍝│ memory on a square layer), the step 1. 163ˡa̲damStartFA ← { (W, A, B, s) → ((W, A, B, s), (0.0 × B, 0.0 × B), 1) } 164 165⍝│ lr a̲damFA (((W, A, B, s), (mB, vB), t), (x, y)): LoRA-FA's Adam 166⍝│ step: B alone moves; the next state. 167ˡa̲damFA ← { lr (((W, A, B, s), (mB, vB), t), (x, y)) → 168 (dB, _) ← (W, A, B, s) ˡg̲radsFA (x, ((W, A, B, s) ˡf̲orward x) − y) 169 (B2, mB2, vB2) ← (lr, t) ᵒᵖa̲dam (B, mB, vB, dB) 170 ((W, A, B2, s), (mB2, vB2), t + 1) 171} 172 173⍝━━ Rank 174 175⍝│ One step of Gram-Schmidt on the rows: the k-th row's remainder R[k], 176⍝│ if larger than tol, becomes a direction and is taken out of every 177⍝│ row, and the count c goes up. 178ʰs̲tep ← { tol (R, k, c) → 179 v ← k s̲elect R 180 n ← s̲qrt '+ r̲/ v × v 181 n ≤ tol ? (R, k + 1, c) 182 (R − (R '+ '× i̲nner v ÷ n) '× t̲able v ÷ n, k + 1, c + 1) 183} 184 185⍝│ r̲ank M: the number of independent rows of the matrix M (its 186⍝│ rank), by Gram-Schmidt: a row counts when what is left of it, after 187⍝│ the directions of the rows before it are taken out, is larger than 188⍝│ 1e-9 times the largest row. 189⍝│ >> "lo:" u_se< "LoRA" 190⍝│ >> lo:r_ank 3 3 r_eshape 1.0 2.0 3.0 2.0 4.0 6.0 0.0 1.0 1.0 191⍝│ 2 192ˡr̲ank ← { M → 193 tol ← 0.000000001 × s̲qrt 'm̲ax r̲/ '+ r̲/₂ M × M 194 (_, _, c) ← (f̲irst s̲hape M) '{ R → tol ʰs̲tep R } p̲ower (M, 1, 0) 195 c 196} 197 198⍝━━ Counts 199 200⍝│ r p̲arams m n: the numbers an adapter of rank r on an m x n matrix 201⍝│ trains, r (m + n) (B's m r and A's r n), against the m n of the 202⍝│ matrix itself. 203⍝│ >> "lo:" u_se< "LoRA" 204⍝│ >> 16 lo:p_arams 4096 14336 205⍝│ 294912 206ˡp̲arams ← { r mn → r × '+ r̲/ mn } 207 208⍝│ r e̲xpert d f: the numbers rank-r adapters train on one SwiGLU 209⍝│ expert of hidden size d and inner size f: its three matrices (gate 210⍝│ and up, f x d; down, d x f), 3 r (d + f). 211⍝│ >> "lo:" u_se< "LoRA" 212⍝│ >> 16 lo:e_xpert 4096 14336 213⍝│ 884736 214ˡe̲xpert ← { r df → 3 × r ˡp̲arams df } 215 216⍝│ r e̲xpertFA d f: the same expert under LoRA-FA: the numbers it 217⍝│ trains (the B's, r (2f + d)), then the frozen A's it regenerates 218⍝│ (r (2d + f)); together r e_xpert d f. 219⍝│ >> "lo:" u_se< "LoRA" 220⍝│ >> 16 lo:e_xpertFA 4096 14336 221⍝│ 524288 360448 222ˡe̲xpertFA ← { r df → 223 (d, f) ← (f̲irst df, 2 s̲elect df) 224 (r × f + f + d) c̲at r × d + d + f 225} 226 227⍝│ b b̲ytes n: the bytes n numbers take at b bits each (16 for BF16 228⍝│ and FP16, 32 for FP32, 4 for NF4). 229⍝│ >> "lo:" u_se< "LoRA" 230⍝│ >> 16 lo:b_ytes 884736 231⍝│ 1769472.0 232ˡb̲ytes ← { b n → (f̲loat b × n) ÷ 8.0 } 233 234⍝│ r c̲osts m n: the multiply-adds for one input through rank-r 235⍝│ factors on an m x n matrix, B (A x) as r (m + n), then what making 236⍝│ the matrix B A first costs, r m n. 237⍝│ >> "lo:" u_se< "LoRA" 238⍝│ >> 8 lo:c_osts 64 64 239⍝│ 1024 32768 240ˡc̲osts ← { r mn → (r ˡp̲arams mn) c̲at r × '× r̲/ mn }