aboutsummaryrefslogtreecommitdiffstats
path: root/src/lora.rs
blob: 169289f6c69d4c6c87a3d5634e4b37fa9dafb210 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
use burn::module::{Module, Param, ParamId};
use burn::nn::{Linear, LinearConfig};
use burn::prelude::*;
use burn::tensor::Distribution;

/// Low-rank adapter: delta_W = (A @ B) * scaling.
/// A starts as Gaussian noise and B as zeros, so at the start of training
/// delta_W = 0 and the model is identical to the base one.
#[derive(Module, Debug)]
pub struct LoraAdapter<B: Backend> {
    /// [d_in, rank]
    pub a: Param<Tensor<B, 2>>,
    /// [rank, d_out]
    pub b: Param<Tensor<B, 2>>,
    pub scaling: f64,
}

impl<B: Backend> LoraAdapter<B> {
    pub fn new(d_in: usize, d_out: usize, rank: usize, scaling: f64, device: &B::Device) -> Self {
        let std = 1.0 / (rank as f64).sqrt();
        let a = Tensor::random([d_in, rank], Distribution::Normal(0.0, std), device);
        let b = Tensor::zeros([rank, d_out], device);
        Self {
            a: Param::from_tensor(a),
            b: Param::from_tensor(b),
            scaling,
        }
    }
}

/// Frozen Linear + optional LoRA adapter.
#[derive(Module, Debug)]
pub struct LoraLinear<B: Backend> {
    pub base: Linear<B>,
    pub lora: Option<LoraAdapter<B>>,
}

impl<B: Backend> LoraLinear<B> {
    pub fn new(d_in: usize, d_out: usize, rank: Option<(usize, f64)>, device: &B::Device) -> Self {
        let base = LinearConfig::new(d_in, d_out).with_bias(false).init(device);
        let lora = rank.map(|(r, scaling)| LoraAdapter::new(d_in, d_out, r, scaling, device));
        Self { base, lora }
    }

    pub fn forward(&self, x: Tensor<B, 3>) -> Tensor<B, 3> {
        let out = self.base.forward(x.clone());
        match &self.lora {
            None => out,
            Some(lora) => {
                let delta = x
                    .matmul(lora.a.val().unsqueeze())
                    .matmul(lora.b.val().unsqueeze());
                out + delta * lora.scaling
            }
        }
    }

    /// Freezes the base weight: it stops accumulating gradients in the autodiff graph.
    pub fn freeze_base(self) -> Self {
        let Self { base, lora } = self;
        let base = Linear {
            weight: base.weight.set_require_grad(false),
            bias: base.bias.map(|b| b.set_require_grad(false)),
        };
        Self { base, lora }
    }

    /// IDs of the trainable parameters (only A and B).
    pub fn trainable_ids(&self, out: &mut Vec<ParamId>) {
        if let Some(lora) = &self.lora {
            out.push(lora.a.id);
            out.push(lora.b.id);
        }
    }
}