1use std::cell::RefCell;
50use std::sync::atomic::{AtomicU32, Ordering};
51use std::sync::Arc;
52
53#[derive(Debug)]
60pub struct LoraScale(AtomicU32);
61
62impl LoraScale {
63 pub fn new(scale: f32) -> Arc<Self> {
64 Arc::new(Self(AtomicU32::new(scale.to_bits())))
65 }
66
67 pub fn get(&self) -> f32 {
68 f32::from_bits(self.0.load(Ordering::Relaxed))
69 }
70
71 pub fn set(&self, scale: f32) {
72 self.0.store(scale.to_bits(), Ordering::Relaxed);
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct LoraShapeError {
81 pub rows: usize,
82 pub cols: usize,
83 pub rank: usize,
84 pub a_len: usize,
85 pub b_len: usize,
86}
87
88impl std::fmt::Display for LoraShapeError {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(
91 f,
92 "LoRA pair does not fit a [{} x {}] base at rank {}: lora_a has {} values \
93 (want rank x cols = {}), lora_b has {} values (want rows x rank = {})",
94 self.rows,
95 self.cols,
96 self.rank,
97 self.a_len,
98 self.rank * self.cols,
99 self.b_len,
100 self.rows * self.rank
101 )
102 }
103}
104
105impl std::error::Error for LoraShapeError {}
106
107#[derive(Debug)]
109pub struct LoraDelta {
110 a: Vec<f32>,
112 b: Vec<f32>,
114 rank: usize,
115 rows: usize,
116 cols: usize,
117 alpha_over_rank: f32,
120 scale: Arc<LoraScale>,
121}
122
123impl LoraDelta {
124 pub fn new(
128 a: Vec<f32>,
129 b: Vec<f32>,
130 rank: usize,
131 rows: usize,
132 cols: usize,
133 alpha: f32,
134 scale: Arc<LoraScale>,
135 ) -> Result<Self, LoraShapeError> {
136 if rank == 0 || a.len() != rank * cols || b.len() != rows * rank {
137 return Err(LoraShapeError {
138 rows,
139 cols,
140 rank,
141 a_len: a.len(),
142 b_len: b.len(),
143 });
144 }
145 let alpha_over_rank = if alpha != 0.0 {
146 alpha / rank as f32
147 } else {
148 1.0
149 };
150 Ok(Self {
151 a,
152 b,
153 rank,
154 rows,
155 cols,
156 alpha_over_rank,
157 scale,
158 })
159 }
160
161 pub fn rank(&self) -> usize {
162 self.rank
163 }
164
165 pub fn rows(&self) -> usize {
166 self.rows
167 }
168
169 pub fn cols(&self) -> usize {
170 self.cols
171 }
172
173 pub fn scale_handle(&self) -> &Arc<LoraScale> {
175 &self.scale
176 }
177
178 pub fn resident_bytes(&self) -> usize {
180 (self.a.len() + self.b.len()) * 4
181 }
182
183 #[inline]
186 fn effective_scale(&self) -> f32 {
187 self.scale.get() * self.alpha_over_rank
188 }
189
190 #[inline]
192 fn project(&self, x: &[f32], y: &mut [f32]) {
193 for (k, yk) in y.iter_mut().enumerate() {
194 let row = &self.a[k * self.cols..(k + 1) * self.cols];
195 *yk = dot(row, x);
196 }
197 }
198
199 #[inline]
201 fn accumulate(&self, s: f32, y: &[f32], out: &mut [f32]) {
202 for (r, o) in out.iter_mut().enumerate() {
203 let brow = &self.b[r * self.rank..(r + 1) * self.rank];
204 *o += s * dot(brow, y);
205 }
206 }
207
208 pub fn add_to(&self, x: &[f32], out: &mut [f32], scratch: &mut Vec<f32>) {
210 debug_assert_eq!(x.len(), self.cols);
211 debug_assert_eq!(out.len(), self.rows);
212 let s = self.effective_scale();
213 if s == 0.0 {
214 return;
215 }
216 scratch.clear();
217 scratch.resize(self.rank, 0.0);
218 self.project(x, scratch);
219 self.accumulate(s, scratch, out);
220 }
221
222 pub fn add_row_to(&self, r: usize, out_row: &mut [f32]) {
225 debug_assert!(r < self.rows);
226 debug_assert_eq!(out_row.len(), self.cols);
227 let s = self.effective_scale();
228 if s == 0.0 {
229 return;
230 }
231 let brow = &self.b[r * self.rank..(r + 1) * self.rank];
232 for (k, &bk) in brow.iter().enumerate() {
233 let arow = &self.a[k * self.cols..(k + 1) * self.cols];
234 let sb = s * bk;
235 for (o, &a) in out_row.iter_mut().zip(arow) {
236 *o += sb * a;
237 }
238 }
239 }
240
241 pub fn add_batch_to(&self, x_batch: &[f32], batch: usize, out: &mut [f32]) {
249 debug_assert_eq!(x_batch.len(), batch * self.cols);
250 debug_assert_eq!(out.len(), batch * self.rows);
251 let s = self.effective_scale();
252 if s == 0.0 || batch == 0 {
253 return;
254 }
255 let rows = self.rows;
256 let cols = self.cols;
257 crate::par::chunks_mut(out, rows, 1, |b, out_b| {
258 SCRATCH.with(|cell| {
259 let mut y = cell.borrow_mut();
260 y.clear();
261 y.resize(self.rank, 0.0);
262 self.project(&x_batch[b * cols..(b + 1) * cols], &mut y);
263 self.accumulate(s, &y, out_b);
264 });
265 });
266 }
267}
268
269thread_local! {
270 static SCRATCH: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
273}
274
275#[inline]
276fn dot(a: &[f32], b: &[f32]) -> f32 {
277 a.iter().zip(b).map(|(x, y)| x * y).sum()
278}
279
280#[derive(Debug, Default)]
284pub struct LoraStack {
285 deltas: Vec<LoraDelta>,
286}
287
288impl LoraStack {
289 pub fn new(delta: LoraDelta) -> Self {
290 Self {
291 deltas: vec![delta],
292 }
293 }
294
295 pub fn push(&mut self, delta: LoraDelta) {
296 self.deltas.push(delta);
297 }
298
299 pub fn len(&self) -> usize {
300 self.deltas.len()
301 }
302
303 pub fn is_empty(&self) -> bool {
304 self.deltas.is_empty()
305 }
306
307 pub fn deltas(&self) -> &[LoraDelta] {
308 &self.deltas
309 }
310
311 pub fn resident_bytes(&self) -> usize {
312 self.deltas.iter().map(LoraDelta::resident_bytes).sum()
313 }
314
315 pub fn add_to(&self, x: &[f32], out: &mut [f32]) {
317 SCRATCH.with(|cell| {
318 let mut scratch = cell.borrow_mut();
319 for d in &self.deltas {
320 d.add_to(x, out, &mut scratch);
321 }
322 });
323 }
324
325 pub fn add_row_to(&self, r: usize, out_row: &mut [f32]) {
327 for d in &self.deltas {
328 d.add_row_to(r, out_row);
329 }
330 }
331
332 pub fn add_batch_to(&self, x_batch: &[f32], batch: usize, out: &mut [f32]) {
334 for d in &self.deltas {
335 d.add_batch_to(x_batch, batch, out);
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 fn delta(rows: usize, cols: usize, rank: usize, alpha: f32, scale: f32) -> LoraDelta {
345 let a: Vec<f32> = (0..rank * cols).map(|i| (i as f32 * 0.37).sin()).collect();
346 let b: Vec<f32> = (0..rows * rank).map(|i| (i as f32 * 0.53).cos()).collect();
347 LoraDelta::new(a, b, rank, rows, cols, alpha, LoraScale::new(scale)).unwrap()
348 }
349
350 fn dense_delta(d: &LoraDelta, x: &[f32]) -> Vec<f32> {
352 let s = d.effective_scale();
353 (0..d.rows)
354 .map(|r| {
355 let mut acc = 0.0;
356 for k in 0..d.rank {
357 let bk = d.b[r * d.rank + k];
358 for (c, &xc) in x.iter().enumerate() {
359 acc += s * bk * d.a[k * d.cols + c] * xc;
360 }
361 }
362 acc
363 })
364 .collect()
365 }
366
367 fn close(a: &[f32], b: &[f32]) {
368 assert_eq!(a.len(), b.len());
369 for (x, y) in a.iter().zip(b) {
370 assert!((x - y).abs() < 1e-4, "{x} vs {y}");
371 }
372 }
373
374 #[test]
375 fn matvec_delta_matches_the_materialised_product() {
376 let d = delta(7, 5, 3, 6.0, 0.8);
377 let x: Vec<f32> = (0..5).map(|i| i as f32 - 2.0).collect();
378 let mut out = vec![0.0; 7];
379 d.add_to(&x, &mut out, &mut Vec::new());
380 close(&out, &dense_delta(&d, &x));
381 }
382
383 #[test]
384 fn alpha_over_rank_is_llama_cpp_s_get_scale() {
385 let with = delta(4, 4, 3, 6.0, 0.5);
387 let without = delta(4, 4, 3, 0.0, 0.5);
388 assert!((with.effective_scale() - 1.0).abs() < 1e-7);
389 assert!((without.effective_scale() - 0.5).abs() < 1e-7);
390 }
391
392 #[test]
393 fn row_delta_agrees_with_the_matvec_on_a_unit_vector() {
394 let d = delta(6, 8, 2, 4.0, 1.3);
395 for r in 0..6 {
396 let mut row = vec![0.0; 8];
397 d.add_row_to(r, &mut row);
398 for c in 0..8 {
400 let mut e = vec![0.0; 8];
401 e[c] = 1.0;
402 let mut out = vec![0.0; 6];
403 d.add_to(&e, &mut out, &mut Vec::new());
404 assert!((out[r] - row[c]).abs() < 1e-5);
405 }
406 }
407 }
408
409 #[test]
410 fn batch_delta_is_the_matvec_delta_per_position() {
411 let d = delta(5, 6, 4, 8.0, 0.25);
412 let batch = 3;
413 let x: Vec<f32> = (0..batch * 6).map(|i| (i as f32 * 0.11).cos()).collect();
414 let mut out = vec![0.0; batch * 5];
415 d.add_batch_to(&x, batch, &mut out);
416 for b in 0..batch {
417 let mut one = vec![0.0; 5];
418 d.add_to(&x[b * 6..(b + 1) * 6], &mut one, &mut Vec::new());
419 close(&out[b * 5..(b + 1) * 5], &one);
420 }
421 }
422
423 #[test]
424 fn a_zero_scale_adds_nothing_bit_for_bit() {
425 let d = delta(5, 6, 4, 8.0, 0.0);
426 let x = vec![1.0; 6];
427 let mut out = vec![0.1, 0.2, 0.3, 0.4, 0.5];
428 let before = out.clone();
429 d.add_to(&x, &mut out, &mut Vec::new());
430 assert_eq!(out, before);
431 let mut batch = vec![0.7; 10];
432 d.add_batch_to(&[x.clone(), x.clone()].concat(), 2, &mut batch);
433 assert_eq!(batch, vec![0.7; 10]);
434 }
435
436 #[test]
437 fn the_scale_is_read_at_apply_time() {
438 let d = delta(5, 6, 4, 0.0, 1.0);
439 let x = vec![1.0; 6];
440 let mut at_one = vec![0.0; 5];
441 d.add_to(&x, &mut at_one, &mut Vec::new());
442 d.scale_handle().set(0.5);
443 let mut at_half = vec![0.0; 5];
444 d.add_to(&x, &mut at_half, &mut Vec::new());
445 let halved: Vec<f32> = at_one.iter().map(|v| v * 0.5).collect();
446 close(&at_half, &halved);
447 }
448
449 #[test]
450 fn a_stack_sums_its_adapters() {
451 let d1 = delta(5, 6, 2, 0.0, 1.0);
452 let d2 = delta(5, 6, 3, 0.0, 0.5);
453 let x: Vec<f32> = (0..6).map(|i| i as f32 * 0.3 - 1.0).collect();
454 let mut want = vec![0.0; 5];
455 d1.add_to(&x, &mut want, &mut Vec::new());
456 d2.add_to(&x, &mut want, &mut Vec::new());
457 let mut stack = LoraStack::new(d1);
458 stack.push(d2);
459 let mut got = vec![0.0; 5];
460 stack.add_to(&x, &mut got);
461 close(&got, &want);
462 }
463
464 #[test]
465 fn a_pair_of_the_wrong_shape_is_refused_with_every_dimension() {
466 let err = LoraDelta::new(
467 vec![0.0; 6],
468 vec![0.0; 5],
469 2,
470 3,
471 4,
472 1.0,
473 LoraScale::new(1.0),
474 )
475 .unwrap_err();
476 let msg = err.to_string();
477 assert!(msg.contains("[3 x 4]"), "{msg}");
478 assert!(msg.contains("rank 2"), "{msg}");
479 assert!(msg.contains("want rank x cols = 8"), "{msg}");
480 assert!(msg.contains("want rows x rank = 6"), "{msg}");
481 }
482}