1use burn_tensor::{Distribution, FloatDType, Tensor};
2
3use crate::module::{LoraAdapter, Param, ParamGroup, Quantizer, Reparameterizer};
4
5#[derive(Debug, Clone)]
14pub struct Lora {
15 pub rank: usize,
17 pub alpha: f64,
19 pub init_std: Option<f64>,
21 pub param_group: ParamGroup,
23}
24
25impl Lora {
26 pub fn new(rank: usize, alpha: f64) -> Self {
28 Self {
29 rank,
30 alpha,
31 init_std: None,
32 param_group: ParamGroup::all(),
33 }
34 }
35
36 pub fn set_param_group(mut self, group: ParamGroup) -> Self {
38 self.param_group = group;
39 self
40 }
41}
42
43impl Reparameterizer for Lora {
44 type Reparam = LoraAdapter;
45
46 fn reparameterize<const D: usize>(
47 &mut self,
48 path: &str,
49 param: Param<Tensor<D>>,
50 ) -> (Param<Tensor<D>>, Option<Self::Reparam>) {
51 if D != 2 {
54 let (id, tensor, mapper) = param.consume();
55 return (
56 Param::from_mapped_value(id, tensor.set_require_grad(false), mapper),
57 None,
58 );
59 }
60
61 let rank = self.rank;
62 let (id, tensor, mapper) = param.consume();
63 let device = tensor.device();
64 let dims = tensor.dims();
65 let (d_in, d_out) = (dims[0], dims[1]);
66 let base_dtype = tensor.dtype();
73 let dtype = base_dtype.is_float().then(|| FloatDType::from(base_dtype));
74
75 let base = Param::from_mapped_value(id, tensor.set_require_grad(false), mapper);
77
78 if self.param_group.matches(&id, Some(path)) {
79 let std = self.init_std.unwrap_or(1.0 / rank as f64);
82 let a = Tensor::<2>::random([d_in, rank], Distribution::Normal(0.0, std), &device);
83 let b = Tensor::<2>::zeros([rank, d_out], &device);
84 let (a, b) = match dtype {
85 Some(dtype) => (a.cast(dtype), b.cast(dtype)),
86 None => (a, b),
87 };
88
89 let adapter = LoraAdapter {
90 a: Param::from_tensor(a),
91 b: Param::from_tensor(b),
92 scale: self.alpha / rank as f64,
93 };
94
95 return (base, Some(adapter));
96 }
97
98 (base, None)
99 }
100}
101
102pub struct QLora {
110 lora: Lora,
111 quantizer: Quantizer,
112}
113
114impl QLora {
115 pub fn new(lora: Lora, quantizer: Quantizer) -> Self {
117 Self { lora, quantizer }
118 }
119}
120
121impl Reparameterizer for QLora {
122 type Reparam = LoraAdapter;
123
124 fn reparameterize<const D: usize>(
125 &mut self,
126 path: &str,
127 param: Param<Tensor<D>>,
128 ) -> (Param<Tensor<D>>, Option<Self::Reparam>) {
129 let param = self.quantizer.map_float_at_path(param, path);
131 self.lora.reparameterize(path, param)
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 #[cfg(feature = "autodiff")]
139 use crate::module::AutodiffModule;
140 use crate::module::{Module, ParamId};
141 use crate::test_device;
142 use crate::test_utils::SimpleLinear;
143 use burn_tensor::Tolerance;
144
145 fn lora_model(in_features: usize, out_features: usize) -> (SimpleLinear, super::Lora) {
146 let device = test_device();
147 let lora = Lora::new(2, 4.0);
148 let model = SimpleLinear::new(in_features, out_features, &device).apply_lora(lora.clone());
149 (model, lora)
150 }
151
152 #[test]
153 fn materialize_lora_matches_base_plus_delta() {
154 let device = test_device();
155 let (model, config) = lora_model(4, 6);
156
157 let weight = &model.weight;
158 let adapter = weight.adapter().expect("adapter should be attached");
159
160 let expected = weight.base() + adapter.delta();
162 weight
163 .val()
164 .into_data()
165 .assert_approx_eq::<f32>(&expected.into_data(), Tolerance::default());
166
167 assert_eq!(adapter.scale, config.alpha / config.rank as f64);
169 let _ = device;
170 }
171
172 #[test]
173 fn lora_freezes_base_and_trains_adapter() {
174 let (model, _) = lora_model(4, 6);
175 let weight = &model.weight;
176 let adapter = weight.adapter().expect("adapter should be attached");
177
178 let ids = [weight.id, adapter.a.id, adapter.b.id];
180 assert_eq!(
181 ids.iter()
182 .collect::<alloc::collections::BTreeSet<&ParamId>>()
183 .len(),
184 3
185 );
186
187 assert_eq!(model.num_params(), 24 + 6 + 12 + 8);
190 }
191
192 #[test]
193 fn lora_b_is_zero_initialized_so_initial_delta_is_zero() {
194 let (model, _) = lora_model(4, 6);
195 let weight = &model.weight;
196 weight
198 .val()
199 .into_data()
200 .assert_approx_eq::<f32>(&weight.base().into_data(), Tolerance::default());
201 }
202
203 #[test]
204 fn lora_record_roundtrip_preserves_base_and_adapter() {
205 let (model, config) = lora_model(4, 6);
206
207 let device = test_device();
209 let target = SimpleLinear::new(4, 6, &device).apply_lora(config);
210
211 let record = model.clone().into_record();
212 let loaded = target.load_record(record);
213
214 loaded
216 .weight
217 .base()
218 .into_data()
219 .assert_eq(&model.weight.base().into_data(), true);
220 loaded
221 .weight
222 .adapter()
223 .unwrap()
224 .a
225 .val()
226 .into_data()
227 .assert_eq(&model.weight.adapter().unwrap().a.val().into_data(), true);
228 loaded
229 .weight
230 .val()
231 .into_data()
232 .assert_eq(&model.weight.val().into_data(), true);
233 }
234
235 #[cfg(feature = "autodiff")]
236 #[test]
237 fn lora_backward_grads_adapter_only() {
238 let device = test_device().autodiff();
239 let lora = Lora::new(2, 4.0);
240 let model = SimpleLinear::new(4, 6, &device).apply_lora(lora);
241
242 let loss = model.weight.val().sum();
244 let grads = loss.backward();
245
246 let adapter = model.weight.adapter().unwrap();
247 assert!(adapter.a.val().grad(&grads).is_some());
249 assert!(adapter.b.val().grad(&grads).is_some());
250 assert!(model.weight.base().grad(&grads).is_none());
251 }
252
253 #[test]
254 fn qlora_quantizes_base_and_attaches_adapter() {
255 use crate::module::Quantizer;
256 use burn_tensor::quantization::{Calibration, QuantLevel, QuantParam, QuantValue};
257
258 let device = test_device();
259 let scheme = device
260 .settings()
261 .quantization
262 .scheme
263 .with_value(QuantValue::Q8S)
264 .with_level(QuantLevel::Tensor)
265 .with_param(QuantParam::F32);
266 let quantizer = Quantizer::new(Calibration::MinMax, scheme);
267
268 let original = SimpleLinear::new(8, 8, &device).weight.val();
269
270 let qlora = QLora::new(Lora::new(2, 4.0), quantizer);
271 let model = SimpleLinear::new(8, 8, &device).apply_qlora(qlora);
272
273 let weight = &model.weight;
274 assert!(weight.adapter().is_some());
275
276 let composed = weight.val();
279 assert_eq!(composed.dims(), [8, 8]);
280 assert_eq!(composed.into_data().shape, original.into_data().shape);
281 }
282
283 #[test]
284 fn param_group_restricts_adapter_to_matching_parameters() {
285 use crate as burn;
286
287 #[derive(Module, Debug)]
288 struct TwoWeights {
289 a: Param<Tensor<2>>,
290 b: Param<Tensor<2>>,
291 }
292
293 let device = test_device();
294 let model = TwoWeights {
295 a: Param::from_tensor(Tensor::random(
296 [4, 4],
297 burn_tensor::Distribution::Default,
298 &device,
299 )),
300 b: Param::from_tensor(Tensor::random(
301 [4, 4],
302 burn_tensor::Distribution::Default,
303 &device,
304 )),
305 };
306
307 let group = ParamGroup::from_predicate("a");
308 let lora = Lora::new(2, 4.0).set_param_group(group);
309 let model = model.apply_lora(lora);
310
311 assert!(
313 model.a.adapter().is_some(),
314 "parameter in the group should get a LoRA adapter"
315 );
316 assert!(
317 model.b.adapter().is_none(),
318 "parameter outside the group should not get a LoRA adapter"
319 );
320
321 assert!(!model.a.base().is_require_grad());
323 assert!(!model.b.val().is_require_grad());
324 }
325
326 #[cfg(feature = "autodiff")]
327 #[test]
328 fn lora_valid_folds_adapter_for_inference() {
329 let device = test_device().autodiff();
330 let lora = Lora::new(2, 4.0);
331 let model = SimpleLinear::new(4, 6, &device).apply_lora(lora);
332
333 let inference = model.valid();
334 assert!(inference.weight.adapter().is_none());
336 inference.weight.val().into_data().assert_approx_eq::<f32>(
337 &model.weight.val().inner().into_data(),
338 Tolerance::default(),
339 );
340 }
341}