1use burn_core as burn;
2
3use super::Optimizer;
4use super::decay::{WeightDecay, WeightDecayConfig};
5use super::module_optimizer::ModuleOptimizer;
6use super::momentum::{Momentum, MomentumConfig, MomentumState};
7use crate::LearningRate;
8use crate::RecordState;
9use crate::grad_clipping::GradientClippingConfig;
10use burn::config::Config;
11use burn::tensor::Device;
12use burn::tensor::Tensor;
13
14#[derive(Config, Debug)]
16pub struct SgdConfig {
17 weight_decay: Option<WeightDecayConfig>,
19 momentum: Option<MomentumConfig>,
21 gradient_clipping: Option<GradientClippingConfig>,
23}
24
25#[derive(Clone)]
29pub struct Sgd {
30 momentum: Option<Momentum>,
31 weight_decay: Option<WeightDecay>,
32}
33
34#[derive(RecordState, Clone, new)]
36pub struct SgdState<const D: usize> {
37 pub momentum: Option<MomentumState<D>>,
39}
40
41impl SgdConfig {
42 pub(crate) fn build(&self) -> Sgd {
44 Sgd {
45 momentum: self.momentum.as_ref().map(Momentum::new),
46 weight_decay: self.weight_decay.as_ref().map(WeightDecay::new),
47 }
48 }
49
50 pub fn init(&self) -> ModuleOptimizer {
52 let mut optim = ModuleOptimizer::from(self.build());
53 if let Some(config) = &self.gradient_clipping {
54 optim = optim.with_grad_clipping(config.init());
55 }
56 optim
57 }
58}
59
60impl Optimizer for Sgd {
61 type State<const D: usize> = SgdState<D>;
62
63 fn step<const D: usize>(
64 &self,
65 lr: LearningRate,
66 tensor: Tensor<D>,
67 mut grad: Tensor<D>,
68 state: Option<Self::State<D>>,
69 ) -> (Tensor<D>, Option<Self::State<D>>) {
70 let mut state_momentum = None;
71
72 if let Some(state) = state {
73 state_momentum = state.momentum;
74 }
75
76 if let Some(weight_decay) = &self.weight_decay {
77 grad = weight_decay.transform(grad, tensor.clone());
78 }
79
80 if let Some(momentum) = &self.momentum {
81 let (grad_out, state) = momentum.transform(grad, state_momentum);
82 state_momentum = Some(state);
83 grad = grad_out;
84 }
85
86 let state = SgdState::new(state_momentum);
87 let delta = grad.mul_scalar(lr);
88
89 (tensor - delta, Some(state))
90 }
91
92 fn to_device<const D: usize>(mut state: Self::State<D>, device: &Device) -> Self::State<D> {
93 state.momentum = state.momentum.map(|state| state.to_device(device));
94 state
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::{grad_clipping::GradientClipping, optim::GradientsParams};
102 use burn::tensor::{Distribution, Shape};
103 use burn_nn::{Linear, LinearConfig};
104
105 const LEARNING_RATE: LearningRate = 0.02;
106
107 #[test]
108 fn with_updated_params_should_have_state() {
109 let device = Device::default().autodiff();
110 let layer = layer(&device);
111 let mut optim = sgd_with_all();
112 let loss = layer.forward(random_tensor(&device));
113 let grads = loss.backward();
114 let grads = GradientsParams::from_grads(grads, &layer);
115 let _layer = optim.step(LEARNING_RATE.into(), layer, grads);
116
117 let record = optim.to_record();
118
119 assert!(!record.is_empty());
120 }
121
122 #[test]
123 fn without_updated_params_should_not_have_state() {
124 let optim = sgd_with_all();
125 let record = optim.to_record();
126 assert!(record.is_empty());
127 }
128
129 #[test]
130 fn can_attach_gradient_clipping() {
131 let optim = sgd_with_all().with_grad_clipping(GradientClipping::Value(0.5));
132 assert!(optim.has_gradient_clipping());
133 }
134
135 #[test]
136 fn should_load_state() {
137 let device = Device::default().autodiff();
138 let layer = layer(&device);
139 let mut optim = sgd_with_all();
140 let loss = layer.forward(random_tensor(&device));
141 let grads = loss.backward();
142 let grads = GradientsParams::from_grads(grads, &layer);
143 let _layer = optim.step(LEARNING_RATE.into(), layer, grads);
144
145 let record = optim.to_record();
146 let bytes = optim.into_bytes().unwrap();
147 let optim_new = sgd_with_all();
148 let record_new = optim_new.to_record();
149 let optim_new = optim_new.from_bytes(bytes).unwrap();
150 let state_restored = optim_new.to_record();
151
152 assert_ne!(record.len(), record_new.len());
153 assert_eq!(record.len(), state_restored.len());
154 }
155
156 #[test]
157 fn lora_finetune_trains_adapter_and_freezes_base() {
158 use burn::module::{LoraConfig, Module};
159 use burn::tensor::Tolerance;
160
161 let device = Device::default().autodiff();
162 let linear = LinearConfig::new(8, 8).init(&device);
163
164 let base_before = linear.weight.val();
166
167 let mut model = linear.apply_lora(LoraConfig::new(4, 8.0));
169 assert!(model.weight.adapter().is_some());
170 let b_before = model.weight.adapter().unwrap().b.val();
171
172 let x = Tensor::<2>::random(Shape::new([16, 8]), Distribution::Default, &device);
173 let target = Tensor::<2>::random(Shape::new([16, 8]), Distribution::Default, &device);
174
175 let mut optim = SgdConfig::new().init();
176
177 let mut first_loss = None;
178 let mut last_loss = 0.0f32;
179 for _ in 0..30 {
180 let output = model.forward(x.clone());
181 let loss = (output - target.clone()).powf_scalar(2.0).mean();
182 last_loss = loss.clone().into_scalar::<f32>();
183 first_loss.get_or_insert(last_loss);
184
185 let grads = loss.backward();
186 let grads = GradientsParams::from_grads(grads, &model);
187 model = optim.step(0.5.into(), model, grads);
188 }
189
190 assert!(
192 last_loss < first_loss.unwrap(),
193 "expected loss to decrease ({} -> {})",
194 first_loss.unwrap(),
195 last_loss
196 );
197
198 assert!(model.weight.adapter().is_some());
200 let b_after = model.weight.adapter().unwrap().b.val();
201 let b_change = (b_after - b_before).abs().sum().into_scalar::<f32>();
202 assert!(
203 b_change > 0.0,
204 "adapter factor B should be updated by training"
205 );
206
207 model
209 .weight
210 .base()
211 .into_data()
212 .assert_approx_eq::<f32>(&base_before.into_data(), Tolerance::default());
213 }
214
215 fn random_tensor(device: &Device) -> Tensor<2> {
216 Tensor::<2>::random(Shape::new([2, 20]), Distribution::Default, device)
217 }
218
219 fn layer(device: &Device) -> Linear {
220 LinearConfig::new(20, 20).init(device)
221 }
222
223 fn sgd_with_all() -> ModuleOptimizer {
224 SgdConfig {
225 weight_decay: Some(WeightDecayConfig { penalty: 0.05 }),
226 momentum: Some(MomentumConfig {
227 momentum: 0.9,
228 dampening: 0.1,
229 nesterov: true,
230 }),
231 gradient_clipping: None,
232 }
233 .init()
234 }
235}