1use alloc::{
2 boxed::Box,
3 string::{String, ToString},
4 vec,
5 vec::Vec,
6};
7use burn_tensor::{Distribution, Tensor};
8
9use crate::module::{LoraAdapter, ModuleMapper, Param, ParamGroup, Quantizer};
10
11#[derive(Debug, Clone)]
13pub struct LoraConfig {
14 pub rank: usize,
16 pub alpha: f64,
18 pub init_std: Option<f64>,
20 pub param_group: ParamGroup,
22}
23
24impl LoraConfig {
25 pub fn new(rank: usize, alpha: f64) -> Self {
27 Self {
28 rank,
29 alpha,
30 init_std: None,
31 param_group: ParamGroup::all(),
32 }
33 }
34
35 pub fn set_param_group(mut self, group: ParamGroup) -> Self {
37 self.param_group = group;
38 self
39 }
40}
41
42#[derive(Debug, Clone)]
54pub struct LoraMapper {
55 config: LoraConfig,
56 path: Vec<String>,
57}
58
59impl LoraMapper {
60 pub fn new(config: LoraConfig) -> Self {
62 Self {
63 config,
64 path: vec![],
65 }
66 }
67
68 pub fn for_group(mut self, group: ParamGroup) -> Self {
70 self.config.param_group = group;
71 self
72 }
73}
74
75impl ModuleMapper for LoraMapper {
76 fn enter_module(&mut self, name: &str, _container_type: &str) {
77 self.path.push(name.to_string());
78 }
79
80 fn exit_module(&mut self, _name: &str, _container_type: &str) {
81 self.path.pop();
82 }
83
84 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
85 if D != 2 {
88 let (id, tensor, mapper) = param.consume();
89 return Param::from_mapped_value(id, tensor.set_require_grad(false), mapper);
90 }
91
92 let rank = self.config.rank;
93 let (id, tensor, mapper) = param.consume();
94 let device = tensor.device();
95 let dims = tensor.dims();
96 let (d_in, d_out) = (dims[0], dims[1]);
97
98 let base = Param::from_mapped_value(id, tensor.set_require_grad(false), mapper);
100
101 let path = self.path.join(".");
102 if self.config.param_group.matches(&id, Some(&path)) {
103 let std = self.config.init_std.unwrap_or(1.0 / rank as f64);
106 let a = Tensor::<2>::random([d_in, rank], Distribution::Normal(0.0, std), &device);
107 let b = Tensor::<2>::zeros([rank, d_out], &device);
108
109 let adapter = LoraAdapter {
110 a: Param::from_tensor(a),
111 b: Param::from_tensor(b),
112 scale: self.config.alpha / rank as f64,
113 };
114
115 return base.with_adapter(Some(Box::new(adapter)));
116 }
117
118 base
119 }
120}
121
122pub struct QLoraMapper {
128 lora: LoraMapper,
129 quantizer: Quantizer,
130}
131
132impl QLoraMapper {
133 pub fn new(config: LoraConfig, quantizer: Quantizer) -> Self {
135 Self {
136 lora: LoraMapper::new(config),
137 quantizer,
138 }
139 }
140}
141
142impl ModuleMapper for QLoraMapper {
143 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
144 let param = self.quantizer.map_float(param);
146 self.lora.map_float(param)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 #[cfg(feature = "autodiff")]
154 use crate::module::AutodiffModule;
155 use crate::module::{Module, ParamId};
156 use crate::test_device;
157 use crate::test_utils::SimpleLinear;
158 use burn_tensor::Tolerance;
159
160 fn lora_model(in_features: usize, out_features: usize) -> (SimpleLinear, super::LoraConfig) {
161 let device = test_device();
162 let config = LoraConfig::new(2, 4.0);
163 let model =
164 SimpleLinear::new(in_features, out_features, &device).apply_lora(config.clone());
165 (model, config)
166 }
167
168 #[test]
169 fn compose_lora_matches_base_plus_delta() {
170 let device = test_device();
171 let (model, config) = lora_model(4, 6);
172
173 let weight = &model.weight;
174 let adapter = weight.adapter().expect("adapter should be attached");
175
176 let expected = weight.base() + adapter.delta();
178 weight
179 .val()
180 .into_data()
181 .assert_approx_eq::<f32>(&expected.into_data(), Tolerance::default());
182
183 assert_eq!(adapter.scale, config.alpha / config.rank as f64);
185 let _ = device;
186 }
187
188 #[test]
189 fn lora_mapper_freezes_base_and_trains_adapter() {
190 let (model, _) = lora_model(4, 6);
191 let weight = &model.weight;
192 let adapter = weight.adapter().expect("adapter should be attached");
193
194 let ids = [weight.id, adapter.a.id, adapter.b.id];
196 assert_eq!(
197 ids.iter()
198 .collect::<alloc::collections::BTreeSet<&ParamId>>()
199 .len(),
200 3
201 );
202
203 assert_eq!(model.num_params(), 24 + 6 + 12 + 8);
206 }
207
208 #[test]
209 fn lora_b_is_zero_initialized_so_initial_delta_is_zero() {
210 let (model, _) = lora_model(4, 6);
211 let weight = &model.weight;
212 weight
214 .val()
215 .into_data()
216 .assert_approx_eq::<f32>(&weight.base().into_data(), Tolerance::default());
217 }
218
219 #[test]
220 fn lora_record_roundtrip_preserves_base_and_adapter() {
221 let (model, config) = lora_model(4, 6);
222
223 let device = test_device();
225 let target = SimpleLinear::new(4, 6, &device).apply_lora(config);
226
227 let record = model.clone().into_record();
228 let loaded = target.load_record(record);
229
230 loaded
232 .weight
233 .base()
234 .into_data()
235 .assert_eq(&model.weight.base().into_data(), true);
236 loaded
237 .weight
238 .adapter()
239 .unwrap()
240 .a
241 .val()
242 .into_data()
243 .assert_eq(&model.weight.adapter().unwrap().a.val().into_data(), true);
244 loaded
245 .weight
246 .val()
247 .into_data()
248 .assert_eq(&model.weight.val().into_data(), true);
249 }
250
251 #[cfg(feature = "autodiff")]
252 #[test]
253 fn lora_backward_grads_adapter_only() {
254 let device = test_device().autodiff();
255 let config = LoraConfig::new(2, 4.0);
256 let model = SimpleLinear::new(4, 6, &device).apply_lora(config);
257
258 let loss = model.weight.val().sum();
260 let grads = loss.backward();
261
262 let adapter = model.weight.adapter().unwrap();
263 assert!(adapter.a.val().grad(&grads).is_some());
265 assert!(adapter.b.val().grad(&grads).is_some());
266 assert!(model.weight.base().grad(&grads).is_none());
267 }
268
269 #[test]
271 fn qlora_quantizes_base_and_attaches_adapter() {
272 use crate::module::Quantizer;
273 use burn_tensor::quantization::{Calibration, QuantLevel, QuantParam, QuantValue};
274
275 let device = test_device();
276 let scheme = device
277 .settings()
278 .quantization
279 .scheme
280 .with_value(QuantValue::Q8S)
281 .with_level(QuantLevel::Tensor)
282 .with_param(QuantParam::F32);
283 let quantizer = Quantizer::new(Calibration::MinMax, scheme);
284
285 let original = SimpleLinear::new(8, 8, &device).weight.val();
286
287 let model =
288 SimpleLinear::new(8, 8, &device).apply_qlora(LoraConfig::new(2, 4.0), quantizer);
289
290 let weight = &model.weight;
291 assert!(weight.adapter().is_some());
292
293 let composed = weight.val();
296 assert_eq!(composed.dims(), [8, 8]);
297 assert_eq!(composed.into_data().shape, original.into_data().shape);
298 }
299
300 #[test]
301 fn param_group_restricts_adapter_to_matching_parameters() {
302 use crate as burn;
303
304 #[derive(Module, Debug)]
305 struct TwoWeights {
306 a: Param<Tensor<2>>,
307 b: Param<Tensor<2>>,
308 }
309
310 let device = test_device();
311 let model = TwoWeights {
312 a: Param::from_tensor(Tensor::random(
313 [4, 4],
314 burn_tensor::Distribution::Default,
315 &device,
316 )),
317 b: Param::from_tensor(Tensor::random(
318 [4, 4],
319 burn_tensor::Distribution::Default,
320 &device,
321 )),
322 };
323
324 let group = ParamGroup::from_predicate("a");
325 let config = LoraConfig::new(2, 4.0).set_param_group(group);
326 let model = model.apply_lora(config);
327
328 assert!(
330 model.a.adapter().is_some(),
331 "parameter in the group should get a LoRA adapter"
332 );
333 assert!(
334 model.b.adapter().is_none(),
335 "parameter outside the group should not get a LoRA adapter"
336 );
337
338 assert!(!model.a.base().is_require_grad());
340 assert!(!model.b.val().is_require_grad());
341 }
342
343 #[cfg(feature = "autodiff")]
344 #[test]
345 fn lora_valid_folds_adapter_for_inference() {
346 let device = test_device().autodiff();
347 let config = LoraConfig::new(2, 4.0);
348 let model = SimpleLinear::new(4, 6, &device).apply_lora(config);
349
350 let inference = model.valid();
351 assert!(inference.weight.adapter().is_none());
353 inference.weight.val().into_data().assert_approx_eq::<f32>(
354 &model.weight.val().inner().into_data(),
355 Tolerance::default(),
356 );
357 }
358}