1#![allow(clippy::excessive_precision)]
2
3use burn_core as burn;
4
5use super::{GradientsParams, ParameterContext};
6use crate::{LearningRate, OptimizerRecord, RecordTensor};
7use crate::{RecordState, StateSink, StateSource};
8use burn::config::Config;
9use burn::module::{Module, ModuleMapper, ModuleVisitor, Param};
10use burn::store::RecordError;
11use burn::tensor::{Bytes, Device, Tensor};
12use serde::{Deserialize, Serialize};
13
14use alloc::vec;
15use alloc::vec::Vec;
16use core::ops::Range;
17#[cfg(not(feature = "std"))]
18#[allow(unused_imports)]
19use num_traits::Float as _;
20
21fn cubic_interpolate(
26 x1: f64,
27 f1: f64,
28 g1: f64,
29 x2: f64,
30 f2: f64,
31 g2: f64,
32 bounds: Option<(f64, f64)>,
33) -> f64 {
34 let (min_bound, max_bound) = bounds.unwrap_or(if x1 <= x2 { (x1, x2) } else { (x2, x1) });
36 let d1 = g1 + g2 - 3.0 * (f1 - f2) / (x1 - x2);
44 let d2_square = d1 * d1 - g1 * g2;
45
46 if d2_square >= 0.0 {
47 let d2 = d2_square.sqrt();
48 let min_pos = if x1 <= x2 {
49 x2 - (x2 - x1) * ((g2 + d2 - d1) / (g2 - g1 + 2.0 * d2))
50 } else {
51 x1 - (x1 - x2) * ((g1 + d2 - d1) / (g1 - g2 + 2.0 * d2))
52 };
53 min_pos.max(min_bound).min(max_bound)
54 } else {
55 (min_bound + max_bound) / 2.0
56 }
57}
58struct LineSearchSample {
60 t: f64,
62 f: f64,
64 g: FlatGradients,
66 gtd: f64,
68}
69
70#[allow(clippy::too_many_arguments)]
71fn strong_wolfe<F>(
72 obj_func: &mut F,
74 x: &Tensor<1>,
75 mut t: f64,
77 d: &Tensor<1>,
78 f: f64,
79 g: FlatGradients,
80 gtd: f64,
81 c1: f64,
82 c2: f64,
83 tolerance_change: f64,
84 max_ls: usize,
85) -> (f64, FlatGradients, f64, usize)
86where
87 F: FnMut(&Tensor<1>, f64, &Tensor<1>) -> (f64, FlatGradients),
88{
89 let d_norm: f64 = d.clone().abs().max().into_scalar();
90
91 let (mut f_new, mut g_new) = obj_func(x, t, d);
93 let mut ls_func_evals = 1;
94 let mut gtd_new = g_new.tensor.clone().dot(d.clone()).into_scalar();
95
96 let (mut t_prev, mut f_prev, mut g_prev, mut gtd_prev) = (0.0, f, g.clone(), gtd);
98 let mut done = false;
99 let mut ls_iter = 0;
100
101 let mut bracket: Option<[LineSearchSample; 2]> = None;
103 let mut wolfe_bracket: Option<LineSearchSample> = None;
105 while ls_iter < max_ls {
106 if f_new > (f + c1 * t * gtd) || (ls_iter > 1 && f_new >= f_prev) {
111 bracket = Some([
112 LineSearchSample {
113 t: t_prev,
114 f: f_prev,
115 g: g_prev,
116 gtd: gtd_prev,
117 },
118 LineSearchSample {
119 t,
120 f: f_new,
121 g: g_new.clone(),
122 gtd: gtd_new,
123 },
124 ]);
125 break;
126 }
127
128 if gtd_new.abs() <= -c2 * gtd {
131 wolfe_bracket = Some(LineSearchSample {
132 t,
133 f: f_new,
134 g: g_new.clone(),
135 gtd: gtd_new,
136 });
137 done = true;
138 break;
139 }
140
141 if gtd_new >= 0.0 {
143 bracket = Some([
144 LineSearchSample {
145 t: t_prev,
146 f: f_prev,
147 g: g_prev,
148 gtd: gtd_prev,
149 },
150 LineSearchSample {
151 t,
152 f: f_new,
153 g: g_new.clone(),
154 gtd: gtd_new,
155 },
156 ]);
157 break;
158 }
159
160 let min_step = t + 0.01 * (t - t_prev);
162 let max_step = t * 10.0;
163 let t_next = cubic_interpolate(
164 t_prev,
165 f_prev,
166 gtd_prev,
167 t,
168 f_new,
169 gtd_new,
170 Some((min_step, max_step)),
171 );
172 t_prev = t;
173 f_prev = f_new;
174 g_prev = g_new;
175 gtd_prev = gtd_new;
176
177 t = t_next;
179 (f_new, g_new) = obj_func(x, t, d);
180 ls_func_evals += 1;
181 gtd_new = g_new.tensor.clone().dot(d.clone()).into_scalar();
182 ls_iter += 1;
183 }
184 if let Some(sample) = wolfe_bracket {
185 return (sample.f, sample.g, sample.t, ls_func_evals);
186 }
187
188 let mut bracket = bracket.unwrap_or_else(|| {
189 [
190 LineSearchSample {
191 t: 0.0,
192 f,
193 g: g.clone(),
194 gtd,
195 },
196 LineSearchSample {
197 t,
198 f: f_new,
199 g: g_new.clone(),
200 gtd: gtd_new,
201 },
202 ]
203 });
204
205 let mut insuf_progress = false;
207
208 let (mut low_idx, mut high_idx) = if bracket[0].f <= bracket[1].f {
210 (0, 1)
211 } else {
212 (1, 0)
213 };
214
215 while !done && ls_iter < max_ls {
216 let diff = (bracket[1].t - bracket[0].t).abs();
217 if diff * d_norm < tolerance_change {
219 break;
220 }
221
222 t = cubic_interpolate(
224 bracket[0].t,
225 bracket[0].f,
226 bracket[0].gtd,
227 bracket[1].t,
228 bracket[1].f,
229 bracket[1].gtd,
230 None,
231 );
232
233 let b_min = bracket[0].t.min(bracket[1].t);
234 let b_max = bracket[0].t.max(bracket[1].t);
235 let eps = 0.1 * (b_max - b_min);
236
237 if (b_max - t).min(t - b_min) < eps {
238 if insuf_progress || t >= b_max || t <= b_min {
240 t = if (t - b_max).abs() < (t - b_min).abs() {
241 b_max - eps
242 } else {
243 b_min + eps
244 };
245 insuf_progress = false;
246 } else {
247 insuf_progress = true;
248 }
249 } else {
250 insuf_progress = false;
251 }
252
253 (f_new, g_new) = obj_func(x, t, d);
255
256 ls_func_evals += 1;
257 gtd_new = g_new.tensor.clone().dot(d.clone()).into_scalar();
258 ls_iter += 1;
259
260 let armijo_holds = f_new <= (f + c1 * t * gtd) && f_new < bracket[low_idx].f;
261
262 if !armijo_holds {
263 bracket[high_idx] = LineSearchSample {
264 t,
265 f: f_new,
266 g: g_new,
267 gtd: gtd_new,
268 };
269 } else {
270 if gtd_new.abs() <= -c2 * gtd {
271 return (f_new, g_new, t, ls_func_evals);
272 }
273
274 if gtd_new * (bracket[high_idx].t - bracket[low_idx].t) >= 0.0 {
275 bracket[high_idx] = LineSearchSample {
276 t: bracket[low_idx].t,
277 f: bracket[low_idx].f,
278 g: bracket[low_idx].g.clone(),
279 gtd: bracket[low_idx].gtd,
280 };
281 }
282 bracket[low_idx] = LineSearchSample {
283 t,
284 f: f_new,
285 g: g_new,
286 gtd: gtd_new,
287 };
288 }
289
290 if bracket[0].f <= bracket[1].f {
291 low_idx = 0;
292 high_idx = 1;
293 } else {
294 low_idx = 1;
295 high_idx = 0;
296 }
297 }
298 (
300 bracket[low_idx].f,
301 bracket[low_idx].g.clone(),
302 bracket[low_idx].t,
303 ls_func_evals,
304 )
305}
306
307#[derive(Clone, Default, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
309pub enum LineSearchFn {
310 #[default]
312 None,
313 StrongWolfe,
317}
318
319#[derive(Config, Debug)]
321pub struct LBFGSConfig {
322 #[config(default = 20)]
324 pub max_iter: usize,
325 #[config(default = 100)]
327 pub history_size: usize,
328 #[config(default = 1e-7)]
330 pub tolerance_grad: f64,
331 #[config(default = 1e-9)]
333 pub tolerance_change: f64,
334 #[config(default = "None")]
336 pub max_eval: Option<usize>,
337 #[config(default = "LineSearchFn::None")]
339 pub line_search_fn: LineSearchFn,
340}
341
342impl LBFGSConfig {
343 pub fn init(&self) -> LBFGS {
349 let max_eval = self.max_eval.unwrap_or(self.max_iter * 5 / 4);
351 LBFGS {
352 config: LBFGSConfig {
353 max_iter: self.max_iter,
354 history_size: self.history_size,
355 tolerance_grad: self.tolerance_grad,
356 tolerance_change: self.tolerance_change,
357 max_eval: Some(max_eval),
358 line_search_fn: self.line_search_fn,
359 },
360 state: Default::default(),
361 }
362 }
363}
364
365#[derive(Clone)]
366struct FlatGradients {
367 tensor: Tensor<1>,
368 missing: Vec<Range<usize>>,
369}
370
371impl FlatGradients {
372 fn mask_direction(&self, mut direction: Tensor<1>) -> Tensor<1> {
373 assert_eq!(
374 direction.dims(),
375 self.tensor.dims(),
376 "L-BFGS search direction length must match the gradient length"
377 );
378 for range in &self.missing {
380 direction =
381 direction.slice_assign(range.clone(), self.tensor.clone().slice(range.clone()));
382 }
383 direction
384 }
385}
386
387struct FlattenGradsVisitorInner<'a> {
389 grads: &'a GradientsParams,
390 tensors: &'a mut Vec<Tensor<1>>,
391 missing: &'a mut Vec<Range<usize>>,
392 offset: usize,
393}
394
395impl ModuleVisitor for FlattenGradsVisitorInner<'_> {
396 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
397 let shape = param.lazy_shape();
398 let numel = shape.num_elements();
399 let grad = match self.grads.get::<D>(param.id) {
400 Some(grad) => {
401 assert_eq!(
402 grad.shape(),
403 shape,
404 "L-BFGS gradient shape must match parameter {}",
405 param.id
406 );
407 grad
408 }
409 None => {
410 if numel > 0 {
411 self.missing.push(self.offset..self.offset + numel);
412 }
413 param.val().inner().dequantize().zeros_like()
414 }
415 };
416 self.tensors.push(grad.reshape([numel]));
417 self.offset += numel;
418 }
419}
420
421fn flatten_params_inner<M: Module>(module: &M) -> Tensor<1> {
423 let mut tensors = Vec::new();
424 let mut visitor = FlattenParamsVisitorInner {
425 tensors: &mut tensors,
426 };
427 module.visit(&mut visitor);
428 if tensors.is_empty() {
429 return Tensor::empty([0], &module.devices()[0].clone().inner());
430 }
431 Tensor::cat(tensors, 0)
432}
433
434struct FlattenParamsVisitorInner<'a> {
435 tensors: &'a mut Vec<Tensor<1>>,
436}
437
438impl ModuleVisitor for FlattenParamsVisitorInner<'_> {
439 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
440 let t = param.val().inner();
441 let numel = t.shape().num_elements();
442 self.tensors.push(t.reshape([numel]));
443 }
444}
445
446fn flatten_grads_inner<M: Module>(module: &M, grads: &GradientsParams) -> FlatGradients {
448 let mut tensors = Vec::new();
449 let mut missing = Vec::new();
450 let mut visitor = FlattenGradsVisitorInner {
451 grads,
452 tensors: &mut tensors,
453 missing: &mut missing,
454 offset: 0,
455 };
456 module.visit(&mut visitor);
457 let tensor = if tensors.is_empty() {
458 Tensor::empty([0], &module.devices()[0].clone().inner())
459 } else {
460 Tensor::cat(tensors, 0)
461 };
462 FlatGradients { tensor, missing }
463}
464
465struct ParamsFromFlatMapperInner<'a> {
467 flat: &'a Tensor<1>,
468 offset: &'a mut usize,
469}
470
471impl ParamsFromFlatMapperInner<'_> {
472 fn take_slice(&mut self, numel: usize) -> Tensor<1> {
473 let start = *self.offset;
474 *self.offset += numel;
475 self.flat.clone().slice(start..*self.offset)
476 }
477}
478
479impl ModuleMapper for ParamsFromFlatMapperInner<'_> {
480 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
481 let (id, tensor, mapper) = param.consume();
482 let numel = tensor.shape().num_elements();
483 let slice_1d = self.take_slice(numel);
484 let new_inner = slice_1d.reshape(tensor.shape());
485 let new_tensor = ParameterContext::capture(&tensor).restore(new_inner, id);
486 Param::from_mapped_value(id, new_tensor, mapper)
487 }
488}
489
490fn set_params_from_flat_inner<M: Module>(module: M, flat: Tensor<1>) -> M {
492 assert_eq!(
493 flat.dims(),
494 [module.num_params()],
495 "L-BFGS flat parameter length must match the module parameter count"
496 );
497 let mut offset = 0;
498 let mut mapper = ParamsFromFlatMapperInner {
499 flat: &flat,
500 offset: &mut offset,
501 };
502 module.map(&mut mapper)
503}
504
505#[derive(Clone, RecordState)]
507pub struct LBFGSState {
508 pub history_s: Vec<Tensor<1>>,
510 pub history_y: Vec<Tensor<1>>,
512 pub d: Option<Tensor<1>>,
514 pub t: Option<f64>,
516 pub prev_flat_grad: Option<Tensor<1>>,
518 pub prev_loss: Option<f64>,
520 pub g_iter: usize,
522}
523
524impl LBFGSState {
525 fn check_num_params(&self, num_params: usize) {
526 for tensor in self
527 .history_s
528 .iter()
529 .chain(self.history_y.iter())
530 .chain(self.d.iter())
531 .chain(self.prev_flat_grad.iter())
532 {
533 assert_eq!(
534 tensor.dims(),
535 [num_params],
536 "L-BFGS state tensor length must match the module parameter count"
537 );
538 }
539 }
540
541 fn current_device(&self) -> Option<Device> {
543 self.prev_flat_grad
544 .as_ref()
545 .or(self.d.as_ref())
546 .or(self.history_s.first())
547 .map(|t| t.device())
548 }
549
550 pub fn to_device(self, device: &Device) -> Self {
552 Self {
553 history_s: self
554 .history_s
555 .into_iter()
556 .map(|t| t.to_device(device))
557 .collect(),
558 history_y: self
559 .history_y
560 .into_iter()
561 .map(|t| t.to_device(device))
562 .collect(),
563 d: self.d.map(|t| t.to_device(device)),
564 t: self.t,
565 prev_flat_grad: self.prev_flat_grad.map(|t| t.to_device(device)),
566 prev_loss: self.prev_loss,
567 g_iter: self.g_iter,
568 }
569 }
570}
571impl Default for LBFGSState {
572 fn default() -> Self {
573 Self {
574 history_s: Vec::new(),
575 history_y: Vec::new(),
576 d: None,
577 t: Some(1.0),
578 prev_flat_grad: None,
579 prev_loss: None,
580 g_iter: 0,
581 }
582 }
583}
584
585#[derive(Clone)]
595pub struct LBFGS {
596 config: LBFGSConfig,
597 state: LBFGSState,
598}
599
600impl LBFGS {
601 pub fn to_record(&self) -> OptimizerRecord {
606 let mut sink = StateSink::default();
607 RecordState::state_flatten(&self.state, "", &mut sink);
608
609 let tensors = sink
610 .tensors
611 .into_iter()
612 .map(|(name, data)| RecordTensor {
613 name,
614 param_id: None,
615 data,
616 })
617 .collect();
618 let scalars = sink.scalars.into_iter().collect();
619
620 OptimizerRecord {
621 tensors,
622 scalars,
623 paths: Default::default(),
624 }
625 }
626
627 pub fn load_record(mut self, record: OptimizerRecord) -> Self {
632 let device = Device::default();
633 let mut source = StateSource::new(record.scalars);
634 for tensor in record.tensors {
635 source.insert_tensor(tensor.name, tensor.data);
636 }
637 if let Some(state) = LBFGSState::state_unflatten("", &mut source, &device) {
638 self.state = state;
639 }
640 self
641 }
642
643 pub fn into_bytes(&self) -> Result<Bytes, RecordError> {
645 self.to_record().into_bytes()
646 }
647
648 pub fn from_bytes(self, bytes: Bytes) -> Result<Self, RecordError> {
650 Ok(self.load_record(OptimizerRecord::from_bytes(bytes)?))
651 }
652
653 #[cfg(feature = "std")]
655 pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), RecordError> {
656 self.to_record().save(path)
657 }
658
659 #[cfg(feature = "std")]
661 pub fn load<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self, RecordError> {
662 Ok(self.load_record(OptimizerRecord::load(path)?))
663 }
664
665 pub fn step<M, F>(&mut self, lr: LearningRate, mut module: M, mut closure: F) -> (M, f64)
675 where
676 M: Module + Clone,
677 F: FnMut(M) -> (f64, GradientsParams),
678 {
679 let (mut loss, grads) = closure(module.clone());
681 let mut current_evals = 1;
682
683 let mut flat_grad = flatten_grads_inner::<M>(&module, &grads);
684 let mut x_flat = flatten_params_inner::<M>(&module);
685 assert_eq!(
686 x_flat.dims(),
687 flat_grad.tensor.dims(),
688 "L-BFGS flat parameter and gradient lengths must match"
689 );
690 self.state.check_num_params(x_flat.dims()[0]);
691
692 if x_flat.dims()[0] == 0 {
693 return (module, loss);
694 }
695
696 let device = flat_grad.tensor.device();
699 if self.state.current_device().is_some_and(|d| d != device) {
700 self.state = core::mem::take(&mut self.state).to_device(&device);
701 }
702
703 let opt_cond =
704 flat_grad.tensor.clone().abs().max().into_scalar::<f64>() <= self.config.tolerance_grad;
705 if opt_cond {
707 return (module, loss);
708 }
709
710 let mut d = self
712 .state
713 .d
714 .take()
715 .unwrap_or_else(|| flat_grad.tensor.clone().neg());
716 let mut t = self.state.t.unwrap_or(lr);
717 let mut prev_flat_grad = self.state.prev_flat_grad.take();
718
719 let mut n_iter = 0;
720
721 while n_iter < self.config.max_iter {
723 n_iter += 1;
725 self.state.g_iter += 1;
726
727 if self.state.g_iter == 1 {
729 d = flat_grad.tensor.clone().neg();
730 self.state.history_s.clear();
731 self.state.history_y.clear();
732 } else {
733 if let Some(pg) = prev_flat_grad.as_ref() {
735 let y = flat_grad.tensor.clone().sub(pg.clone());
736 let s = d.clone().mul_scalar(t);
737
738 let ys: f64 = y.clone().dot(s.clone()).into_scalar();
739
740 if ys > 1e-10 {
741 if self.state.history_s.len() >= self.config.history_size {
743 self.state.history_s.remove(0);
745 self.state.history_y.remove(0);
746 }
747 self.state.history_s.push(s);
748 self.state.history_y.push(y);
749 }
750 }
751
752 let num_old = self.state.history_s.len();
755 let mut q = flat_grad.tensor.clone().neg();
756 let mut alphas: Vec<Tensor<1>> =
757 vec![Tensor::zeros([1], &flat_grad.tensor.device().inner()); num_old];
758
759 if num_old > 0 {
760 for i in (0..num_old).rev() {
763 let s = &self.state.history_s[i];
764 let y = &self.state.history_y[i];
765 let rho = y.clone().dot(s.clone()).powf_scalar(-1.0);
766 let alpha = rho.clone().mul(s.clone().dot(q.clone()));
767 alphas[i] = alpha.clone();
768 q = q.sub(y.clone().mul(alpha));
769 }
770
771 let last_s = &self.state.history_s[num_old - 1];
772 let last_y = &self.state.history_y[num_old - 1];
773 let ys = last_y.clone().dot(last_s.clone());
774 let yy = last_y.clone().dot(last_y.clone());
775 let h_diag = ys.div(yy);
776
777 let mut r = q.mul(h_diag);
778
779 for ((s, y), alpha) in self
780 .state
781 .history_s
782 .iter()
783 .zip(self.state.history_y.iter())
784 .zip(alphas)
785 .take(num_old)
786 {
787 let rho = y.clone().dot(s.clone()).powf_scalar(-1.0);
788
789 let beta = rho.mul(y.clone().dot(r.clone()));
790
791 r = r.add(s.clone().mul(alpha.sub(beta)));
792 }
793 d = r;
794 } else {
795 d = q;
796 }
797 }
798
799 d = flat_grad.mask_direction(d);
800 prev_flat_grad = Some(flat_grad.tensor.clone());
801 let prev_loss_iter = loss;
802
803 if self.state.g_iter == 1 {
805 let grad_l1: f64 = flat_grad.tensor.clone().abs().sum().into_scalar();
806 t = (1.0f64 / grad_l1).min(1.0) * lr;
807 } else {
808 t = lr;
809 }
810
811 let gtd = flat_grad.tensor.clone().dot(d.clone()).into_scalar();
813
814 if gtd > -self.config.tolerance_change {
815 break;
816 }
817
818 let ls_func_evals;
819
820 if let LineSearchFn::StrongWolfe = self.config.line_search_fn {
821 let mut obj_func = |current_x: &Tensor<1>, step: f64, dir: &Tensor<1>| {
823 let update = dir.clone().mul_scalar(step);
824 let new_x = current_x.clone().add(update);
825 let tmp_module = set_params_from_flat_inner::<M>(module.clone(), new_x);
826 let (l, g) = closure(tmp_module);
827 (l, flatten_grads_inner::<M>(&module, &g))
828 };
829
830 let (ls_f, ls_g, ls_t, evals) = strong_wolfe(
831 &mut obj_func,
832 &x_flat,
833 t,
834 &d,
835 loss,
836 flat_grad.clone(),
837 gtd,
838 1e-4,
839 0.9,
840 self.config.tolerance_change,
841 self.config.max_eval.unwrap() - current_evals,
842 );
843
844 loss = ls_f;
845 flat_grad = ls_g;
846 t = ls_t;
847 ls_func_evals = evals;
848
849 x_flat = x_flat.add(d.clone().mul_scalar(t));
850 module = set_params_from_flat_inner::<M>(module, x_flat.clone());
851 } else {
852 let step_vec = d.clone().mul_scalar(t);
854 x_flat = x_flat.add(step_vec);
855 module = set_params_from_flat_inner::<M>(module, x_flat.clone());
856 let (new_loss, new_grads) = closure(module.clone());
860 loss = new_loss;
861 flat_grad = flatten_grads_inner::<M>(&module, &new_grads);
862 ls_func_evals = 1;
863 }
864
865 current_evals += ls_func_evals;
867
868 if current_evals >= self.config.max_eval.unwrap() {
871 break;
872 }
873
874 if flat_grad.tensor.clone().abs().max().into_scalar::<f64>()
875 <= self.config.tolerance_grad
876 {
877 break;
878 }
879
880 if d.clone().mul_scalar(t).abs().max().into_scalar::<f64>()
881 <= self.config.tolerance_change
882 {
883 break;
884 }
885
886 if (loss - prev_loss_iter).abs() < self.config.tolerance_change {
887 break;
888 }
889 }
890 self.state.d = Some(d);
891 self.state.t = Some(t);
892 self.state.prev_flat_grad = prev_flat_grad;
893 self.state.prev_loss = Some(loss);
894 (module, loss)
895 }
896 pub fn to_device(self, device: &Device) -> Self {
898 Self {
899 config: self.config,
900 state: self.state.to_device(device),
902 }
903 }
904}
905
906#[cfg(test)]
907mod tests {
908
909 use super::*;
910 use crate::GradientsParams;
911 use burn::module::Param;
912 use burn::tensor::{Tensor, TensorData};
913 use burn_nn::Linear;
914
915 fn given_linear_layer(weight: TensorData, bias: TensorData, device: &Device) -> Linear {
916 Linear {
917 weight: Param::from_data(weight, device),
918 bias: Some(Param::from_data(bias, device)),
919 }
920 }
921 #[test]
922 fn test_cubic_interpolate() {
923 let tolerance = 1e-8;
924
925 let (x1, f1, g1, x2, f2, g2) = (-1.0, 1.0, -2.0, 1.0, 1.0, 2.0);
927 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
928 assert!(
929 (result - 0.00000).abs() < tolerance,
930 "Basic: Result {} should be close to 0.0",
931 result
932 );
933
934 let (x1, f1, g1, x2, f2, g2) = (0.0, 0.25, -1.0, 1.0, 0.25, 1.0);
936 let bounds = Some((0.6, 1.0));
937 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, bounds);
938 assert!(
939 (result - 0.6000000000).abs() < tolerance,
940 "Bound: Result {} should be clamped to 0.6",
941 result
942 );
943
944 let (x1, f1, g1, x2, f2, g2) = (0.0, 0.0, 10.0, 1.0, 5.0, 10.0);
946 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, Some((0.0, 1.0)));
947 assert!(
948 (result - 0.5000000).abs() < tolerance,
949 "Fallback: Result {} should be midpoint 0.5",
950 result
951 );
952
953 let (x1, f1, g1, x2, f2, g2) = (0.0, 1.0, -5.0, 1.0, 0.5, 1.0);
955 let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
956 assert!(
957 (result - 0.4606553370833684).abs() < tolerance,
958 "Asymmetric: Result {} should be 0.4606553370833684",
959 result
960 );
961
962 let (x1, f1, g1, x2, f2, g2) = (
964 1.231232145,
965 -0.12567458754,
966 9.1231243007,
967 8.239105015,
968 -100.9012398021,
969 123201321.0293982,
970 );
971 let result_1 = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
972 let result_2 = cubic_interpolate(x1, f1, g1, x2, f2, g2, Some((-4.4, 4.4)));
973 assert!(
974 (result_1 - 5.9031480234724434).abs() < tolerance,
975 "not good value 1: Result {} should be 5.9031480234724434",
976 result
977 );
978 assert!(
979 (result_2 - 4.4000000000000004).abs() < tolerance,
980 "not good value 2: Result {} should be 4.4000000000000004",
981 result
982 );
983 }
984 #[test]
985 fn test_strong_wolfe_direct_comparison() {
986 let device = Device::default().autodiff();
987 let tol = 1e-6;
988
989 {
990 let x = Tensor::<1>::from_floats([2.1321912957_f64], &device);
991 let d = Tensor::<1>::from_floats([0.91312321_f64], &device);
992 let t_initial = 1.213132_f64;
993 fn func(x_base: &Tensor<1>, t_val: f64, d_vec: &Tensor<1>) -> (f64, FlatGradients) {
994 let curr_x = x_base.clone().add(d_vec.clone().mul_scalar(t_val));
995 let x2 = curr_x.clone().mul(curr_x.clone());
996 let x3 = x2.clone().mul(curr_x.clone());
997 let x4 = x2.clone().mul(x2.clone());
998
999 let f_elements = x4 - x2.mul_scalar(2.0) + curr_x.clone();
1001
1002 let f_val = f_elements.sum().into_scalar();
1003
1004 let g = x3.mul_scalar(4.0) - curr_x.clone().mul_scalar(4.0)
1006 + Tensor::ones_like(&curr_x);
1007
1008 (
1009 f_val,
1010 FlatGradients {
1011 tensor: g,
1012 missing: Vec::new(),
1013 },
1014 )
1015 }
1016 let (f_init, g_init) = func(&x, 0.0, &d);
1017 let gtd_init = g_init.tensor.clone().dot(d.clone()).into_scalar::<f64>();
1018 println!("Initial State: f={},gtd = {}", f_init, gtd_init);
1019 assert!((f_init - 13.7080059052).abs() < tol);
1020 assert!((gtd_init - 28.5305728912).abs() < tol);
1021 let mut obj_func = |xb: &Tensor<1>, tv: f64, dv: &Tensor<1>| func(xb, tv, dv);
1022
1023 let (f_final, _g_final, t_final, evals) = strong_wolfe(
1024 &mut obj_func,
1025 &x,
1026 t_initial,
1027 &d,
1028 f_init,
1029 g_init,
1030 gtd_init,
1031 1e-4, 0.9, 1e-9, 10, );
1036 let g_f = _g_final.tensor.into_scalar::<f64>();
1037 println!(
1038 "f_final:{:?},_g_final:{:?},t_final:{:?},evals:{:?}",
1039 f_final, g_f, t_final, evals
1040 );
1041 assert!((f_final - 13.708005905151367).abs() < tol);
1042 assert!((g_f - 31.2450428009).abs() < tol);
1043 assert!((t_final - 0.0).abs() < tol);
1044 assert!((evals == 11));
1045 }
1046 }
1047 #[test]
1048 fn test_lbfgs_strong_wolfe_comparison() {
1049 let device = Device::default().autodiff();
1050 let tol = 1e-5;
1051 let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1052 let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1053 let weight = TensorData::from([[0.5f64]]);
1054 let bias = TensorData::from([0.1f64]);
1055 let module = given_linear_layer(weight, bias, &device);
1056
1057 let mut optimizer = LBFGSConfig::new()
1058 .with_line_search_fn(LineSearchFn::StrongWolfe)
1059 .init();
1060 let mut closure = |mod_in: Linear| {
1061 let output = mod_in.forward(x_data.clone());
1062 let loss = burn_nn::loss::MseLoss::new().forward(
1063 output,
1064 y_true.clone(),
1065 burn_nn::loss::Reduction::Sum,
1066 );
1067
1068 let grads = loss.backward();
1069 let grads_params = GradientsParams::from_grads(grads, &mod_in);
1070
1071 (loss.into_scalar::<f64>(), grads_params)
1072 };
1073 let initial_loss = closure(module.clone()).0;
1074 assert!((initial_loss - 50.1300048828).abs() < tol);
1075 let (updated_module, final_loss) = optimizer.step(0.001, module, &mut closure);
1076 assert!((final_loss - 0.0234732367).abs() < tol);
1077 let optimized_data: f64 = updated_module.weight.val().into_scalar();
1078 let optimized_bias: f64 = updated_module.bias.as_ref().unwrap().val().into_scalar();
1079 assert!((optimized_data - 2.0570652485).abs() < tol);
1080 assert!((optimized_bias - 0.8106800914).abs() < tol);
1081 }
1082
1083 #[test]
1086 fn test_lbfgs_burnpack_round_trip() {
1087 let device = Device::default().autodiff();
1088 let tol = 1e-6;
1089 let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1090 let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1091 let module = given_linear_layer(
1092 TensorData::from([[0.5f64]]),
1093 TensorData::from([0.1f64]),
1094 &device,
1095 );
1096
1097 let make_closure = || {
1098 let x = x_data.clone();
1099 let y = y_true.clone();
1100 move |mod_in: Linear| {
1101 let output = mod_in.forward(x.clone());
1102 let loss = burn_nn::loss::MseLoss::new().forward(
1103 output,
1104 y.clone(),
1105 burn_nn::loss::Reduction::Sum,
1106 );
1107 let grads = loss.backward();
1108 let grads_params = GradientsParams::from_grads(grads, &mod_in);
1109 (loss.into_scalar::<f64>(), grads_params)
1110 }
1111 };
1112
1113 let mut optimizer = LBFGSConfig::new()
1114 .with_line_search_fn(LineSearchFn::StrongWolfe)
1115 .init();
1116 let (module, _) = optimizer.step(0.001, module, &mut make_closure());
1117
1118 let bytes = optimizer.into_bytes().unwrap();
1120 let mut reloaded = LBFGSConfig::new()
1121 .with_line_search_fn(LineSearchFn::StrongWolfe)
1122 .init()
1123 .from_bytes(bytes)
1124 .unwrap();
1125
1126 let (_, loss_original) = optimizer.step(0.001, module.clone(), &mut make_closure());
1128 let (_, loss_reloaded) = reloaded.step(0.001, module, &mut make_closure());
1129 assert!(
1130 (loss_original - loss_reloaded).abs() < tol,
1131 "losses differ after burnpack round-trip: {loss_original} vs {loss_reloaded}"
1132 );
1133 }
1134
1135 #[test]
1136 fn test_lbfgs_no_strong_wolfe_comparison() {
1137 let device = Device::default().autodiff();
1138 let tol = 1e-5;
1139 let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1140 let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1141 let weight = TensorData::from([[0.5f64]]);
1142 let bias = TensorData::from([0.1f64]);
1143 let module = given_linear_layer(weight, bias, &device);
1144
1145 let mut optimizer = LBFGSConfig::new()
1146 .with_line_search_fn(LineSearchFn::None)
1147 .init();
1148 let mut closure = |mod_in: Linear| {
1149 let output = mod_in.forward(x_data.clone());
1150 let loss = burn_nn::loss::MseLoss::new().forward(
1151 output,
1152 y_true.clone(),
1153 burn_nn::loss::Reduction::Sum,
1154 );
1155
1156 let grads = loss.backward();
1157 let grads_params = GradientsParams::from_grads(grads, &mod_in);
1158
1159 (loss.into_scalar::<f64>(), grads_params)
1160 };
1161 let initial_loss = closure(module.clone()).0;
1162 assert!((initial_loss - 50.1300048828).abs() < tol);
1163 let (updated_module, final_loss) = optimizer.step(0.001, module, &mut closure);
1164 assert!((final_loss - 48.2181930542).abs() < tol);
1165 let optimized_data: f64 = updated_module.weight.val().into_scalar();
1166 let optimized_bias: f64 = updated_module.bias.as_ref().unwrap().val().into_scalar();
1167
1168 assert!((optimized_data - 0.5302446192).abs() < tol);
1169 assert!((optimized_bias - 0.1142520783).abs() < tol);
1170 }
1171
1172 #[test]
1173 fn test_lbfgs_missing_gradients_preserve_parameter_positions() {
1174 let device = Device::default().autodiff();
1175 let values = [[1.0f32, 2.0], [3.0, 4.0], [5.0, 6.0]];
1176
1177 for line_search in [LineSearchFn::None, LineSearchFn::StrongWolfe] {
1178 for frozen in [false, true] {
1179 for missing in 0..values.len() {
1180 let module: Vec<Param<Tensor<1>>> = values
1181 .iter()
1182 .enumerate()
1183 .map(|(i, value)| {
1184 let param = Param::from_tensor(Tensor::from_floats(*value, &device));
1185 if frozen && i == missing {
1186 param.freeze()
1187 } else {
1188 param
1189 }
1190 })
1191 .collect();
1192 let mut optimizer = LBFGSConfig::new()
1193 .with_max_iter(1)
1194 .with_line_search_fn(line_search)
1195 .init();
1196 let (module, _) = optimizer.step(1.0, module, |module| {
1197 let loss = module
1198 .iter()
1199 .enumerate()
1200 .filter(|(i, _)| frozen || *i != missing)
1201 .map(|(_, param)| param.val().powf_scalar(2.0).sum())
1202 .reduce(|a, b| a + b)
1203 .unwrap();
1204 let grads = GradientsParams::from_grads(loss.backward(), &module);
1205 (loss.into_scalar::<f64>(), grads)
1206 });
1207
1208 let active_sum: f32 = values
1209 .iter()
1210 .enumerate()
1211 .filter(|(i, _)| *i != missing)
1212 .flat_map(|(_, value)| value)
1213 .sum();
1214 for (i, param) in module.iter().enumerate() {
1215 let expected = values[i].map(|value| {
1216 if i == missing {
1217 value
1218 } else {
1219 value - value / active_sum
1220 }
1221 });
1222 param.val().into_data().assert_approx_eq::<f32>(
1223 &TensorData::from(expected),
1224 Default::default(),
1225 );
1226 assert_eq!(param.is_require_grad(), !frozen || i != missing);
1227 }
1228 }
1229 }
1230 }
1231 }
1232
1233 #[test]
1234 fn test_lbfgs_missing_scalar_gradient_does_not_broadcast() {
1235 let device = Device::default().autodiff();
1236 for line_search in [LineSearchFn::None, LineSearchFn::StrongWolfe] {
1237 let module = vec![
1238 Param::from_tensor(Tensor::<1>::from_floats([10.0], &device)).freeze(),
1239 Param::from_tensor(Tensor::<1>::from_floats([20.0], &device)),
1240 ];
1241 let mut optimizer = LBFGSConfig::new()
1242 .with_max_iter(1)
1243 .with_line_search_fn(line_search)
1244 .init();
1245 let (module, _) = optimizer.step(1.0, module, |module| {
1246 let loss = module[1].val().powf_scalar(2.0).sum();
1247 let grads = GradientsParams::from_grads(loss.backward(), &module);
1248 (loss.into_scalar::<f64>(), grads)
1249 });
1250
1251 assert_eq!(module[0].val().into_scalar::<f32>(), 10.0);
1252 assert_eq!(module[1].val().into_scalar::<f32>(), 19.0);
1253 }
1254 }
1255
1256 #[test]
1257 fn test_lbfgs_history_does_not_move_parameters_without_gradients() {
1258 let device = Device::default().autodiff();
1259 let coupled_loss = |module: Vec<Param<Tensor<1>>>| {
1260 let loss = (module[0].val() + module[1].val()).powf_scalar(2.0)
1261 + module[1].val().powf_scalar(2.0);
1262 let grads = GradientsParams::from_grads(loss.backward(), &module);
1263 (loss.into_scalar::<f64>(), grads)
1264 };
1265
1266 for line_search in [LineSearchFn::None, LineSearchFn::StrongWolfe] {
1267 for frozen in [false, true] {
1268 let mut module = vec![
1269 Param::from_tensor(Tensor::<1>::from_floats([1.0], &device)),
1270 Param::from_tensor(Tensor::<1>::from_floats([2.0], &device)),
1271 ];
1272 let mut optimizer = LBFGSConfig::new()
1273 .with_max_iter(1)
1274 .with_max_eval(Some(20))
1275 .with_line_search_fn(line_search)
1276 .init();
1277 (module, _) = optimizer.step(0.1, module, coupled_loss);
1278 (module, _) = optimizer.step(0.1, module, coupled_loss);
1279 assert!(!optimizer.state.history_s.is_empty());
1280
1281 if frozen {
1282 module[0] = module[0].clone().freeze();
1283 }
1284 let unchanged = module[0].val().into_scalar::<f32>();
1285 let mut closure = |module: Vec<Param<Tensor<1>>>| {
1286 if frozen {
1287 coupled_loss(module)
1288 } else {
1289 let loss = module[1].val().powf_scalar(2.0);
1290 let grads = GradientsParams::from_grads(loss.backward(), &module);
1291 (loss.into_scalar::<f64>(), grads)
1292 }
1293 };
1294 let initial_loss = closure(module.clone()).0;
1295 let (module, final_loss) = optimizer.step(0.1, module, &mut closure);
1296
1297 assert_eq!(module[0].val().into_scalar::<f32>(), unchanged);
1298 assert!(final_loss < initial_loss);
1299 }
1300 }
1301 }
1302
1303 #[test]
1304 fn test_lbfgs_no_gradients_leaves_parameters_unchanged() {
1305 let device = Device::default().autodiff();
1306 let module = Param::from_tensor(
1307 Tensor::<1>::from_floats([1.0, 2.0], &device).cast(burn::tensor::DType::F64),
1308 )
1309 .freeze();
1310 let mut optimizer = LBFGSConfig::new().init();
1311 let (module, _) = optimizer.step(1.0, module, |_| (0.0, GradientsParams::new()));
1312
1313 assert_eq!(optimizer.state.g_iter, 0);
1314 assert_eq!(module.val().dtype(), burn::tensor::DType::F64);
1315 module
1316 .val()
1317 .into_data()
1318 .assert_eq(&TensorData::from([1.0f64, 2.0]), false);
1319 }
1320
1321 #[test]
1322 #[should_panic(expected = "L-BFGS gradient shape must match parameter")]
1323 fn test_lbfgs_rejects_incorrect_gradient_shape() {
1324 let device = Device::default().autodiff();
1325 let module = Param::from_tensor(Tensor::<1>::from_floats([1.0, 2.0], &device));
1326 let mut optimizer = LBFGSConfig::new().init();
1327 optimizer.step(1.0, module, |module| {
1328 let mut grads = GradientsParams::new();
1329 grads.register(module.id, Tensor::<1>::ones([1], &device.clone().inner()));
1330 (1.0, grads)
1331 });
1332 }
1333
1334 #[test]
1335 #[should_panic(expected = "L-BFGS state tensor length must match the module parameter count")]
1336 fn test_lbfgs_rejects_incorrect_state_length() {
1337 let device = Device::default().autodiff();
1338 let module = Param::from_tensor(Tensor::<1>::from_floats([1.0, 2.0], &device));
1339 let mut optimizer = LBFGSConfig::new().init();
1340 optimizer.state.d = Some(Tensor::ones([1], &device.clone().inner()));
1341 optimizer.step(1.0, module, |module| {
1342 let loss = module.val().powf_scalar(2.0).sum();
1343 let grads = GradientsParams::from_grads(loss.backward(), &module);
1344 (loss.into_scalar::<f64>(), grads)
1345 });
1346 }
1347}