1use burn_core as burn;
2
3use crate::{LearningRate, RecordState, grad_clipping::GradientClippingConfig};
4use burn::config::Config;
5use burn::tensor::{Device, Tensor};
6
7use super::{Optimizer, module_optimizer::ModuleOptimizer};
8
9#[derive(Config, Debug)]
11pub struct LionConfig {
12 #[config(default = 0.9)]
14 beta_1: f32,
15 #[config(default = 0.99)]
17 beta_2: f32,
18 #[config(default = 0.0)]
20 weight_decay: f32,
21 grad_clipping: Option<GradientClippingConfig>,
23}
24
25#[derive(Clone)]
36pub struct Lion {
37 beta_1: f32,
38 beta_2: f32,
39 weight_decay: f32,
40}
41
42#[derive(RecordState, Clone, new)]
44pub struct LionState<const D: usize> {
45 pub momentum: Tensor<D>,
47}
48
49impl LionConfig {
50 pub fn build(&self) -> Lion {
57 Lion {
58 beta_1: self.beta_1,
59 beta_2: self.beta_2,
60 weight_decay: self.weight_decay,
61 }
62 }
63
64 pub fn init(&self) -> ModuleOptimizer {
66 let mut optim = ModuleOptimizer::from(self.build());
67 if let Some(config) = &self.grad_clipping {
68 optim = optim.with_grad_clipping(config.init());
69 }
70 optim
71 }
72}
73
74impl Optimizer for Lion {
75 type State<const D: usize> = LionState<D>;
76
77 fn step<const D: usize>(
78 &self,
79 lr: LearningRate,
80 tensor: Tensor<D>,
81 grad: Tensor<D>,
82 state: Option<Self::State<D>>,
83 ) -> (Tensor<D>, Option<Self::State<D>>) {
84 let (update, momentum) = match state {
85 Some(state) => {
86 let update = state
87 .momentum
88 .clone()
89 .mul_scalar(self.beta_1)
90 .add(grad.clone().mul_scalar(1.0 - self.beta_1))
91 .sign();
92 let momentum = state
93 .momentum
94 .mul_scalar(self.beta_2)
95 .add(grad.mul_scalar(1.0 - self.beta_2));
96
97 (update, momentum)
98 }
99 None => {
100 let update = grad.clone().mul_scalar(1.0 - self.beta_1).sign();
101 let momentum = grad.mul_scalar(1.0 - self.beta_2);
102
103 (update, momentum)
104 }
105 };
106
107 let decay = 1.0 - lr * self.weight_decay as f64;
108 let tensor = if decay == 1.0 {
109 tensor
110 } else {
111 tensor.mul_scalar(decay)
112 };
113 let tensor = tensor - update.mul_scalar(lr);
114
115 (tensor, Some(LionState::new(momentum)))
116 }
117
118 fn to_device<const D: usize>(mut state: Self::State<D>, device: &Device) -> Self::State<D> {
119 state.momentum = state.momentum.to_device(device);
120 state
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::optim::test_utils::assert_optimizer_resume;
128 use crate::{AdamWConfig, GradientsParams, ModuleOptimizer};
129 use burn::module::Param;
130 use burn::tensor::{TensorData, Tolerance};
131 use burn_nn::loss::{MseLoss, Reduction};
132 use burn_nn::{Linear, LinearConfig};
133
134 #[test]
135 fn test_lion_two_steps_with_weight_decay() {
136 let device = Device::default();
137 let optimizer = LionConfig::new()
138 .with_beta_1(0.9)
139 .with_beta_2(0.99)
140 .with_weight_decay(0.1)
141 .build();
142 let tensor = Tensor::<1>::from_floats([1.0, -2.0, 3.0], &device);
143 let grad = Tensor::<1>::from_floats([0.5, -0.25, 0.0], &device);
144
145 let (tensor, state) = optimizer.step(0.1, tensor, grad, None);
146 tensor.clone().into_data().assert_approx_eq::<f32>(
147 &TensorData::from([0.89, -1.88, 2.97]),
148 Tolerance::absolute(1e-6),
149 );
150 let state = state.unwrap();
151 state.momentum.clone().into_data().assert_approx_eq::<f32>(
152 &TensorData::from([0.005, -0.0025, 0.0]),
153 Tolerance::absolute(1e-6),
154 );
155
156 let grad = Tensor::<1>::from_floats([-0.1, 0.5, -2.0], &device);
157 let (tensor, state) = optimizer.step(0.1, tensor, grad, Some(state));
158 tensor.into_data().assert_approx_eq::<f32>(
159 &TensorData::from([0.9811, -1.9612, 3.0403]),
160 Tolerance::absolute(1e-6),
161 );
162 state.unwrap().momentum.into_data().assert_approx_eq::<f32>(
163 &TensorData::from([0.00395, 0.002525, -0.02]),
164 Tolerance::absolute(1e-6),
165 );
166 }
167
168 #[test]
169 fn test_lion_optimizer_save_load_state() {
170 let device = Device::default().autodiff();
171 let linear = LinearConfig::new(4, 3).init(&device);
172 assert_optimizer_resume(|| LionConfig::new().init(), linear, 1e-4);
173 }
174
175 #[test]
179 fn test_paper_smoke_lion_trains_tiny_regression() {
180 let device = Device::default().autodiff();
181 let lion_model = tiny_linear_model(&device);
182 let adamw_model = tiny_linear_model(&device);
183
184 let (lion_initial, lion_final) =
185 train_tiny_regression(lion_model, LionConfig::new().init(), 0.01, &device);
186 let (adamw_initial, adamw_final) = train_tiny_regression(
187 adamw_model,
188 AdamWConfig::new().with_weight_decay(0.0).init(),
189 0.05,
190 &device,
191 );
192
193 println!(
194 "tiny regression: Lion {lion_initial:.6e} -> {lion_final:.6e}; \
195 AdamW {adamw_initial:.6e} -> {adamw_final:.6e}"
196 );
197 assert!(
198 lion_final < lion_initial * 1e-3,
199 "Lion loss did not decrease enough: {lion_initial} -> {lion_final}"
200 );
201 assert!(adamw_final < adamw_initial * 1e-3);
202 }
203
204 #[test]
208 fn test_paper_smoke_lion_state_is_smaller_than_adamw() {
209 let device = Device::default().autodiff();
210 let model = LinearConfig::new(64, 64).with_bias(false).init(&device);
211 let input = Tensor::<2>::ones([2, 64], &device);
212
213 let mut lion = LionConfig::new().init();
214 let grads = GradientsParams::from_grads(
215 model.clone().forward(input.clone()).sum().backward(),
216 &model,
217 );
218 let _model = lion.step(1e-4, model, grads);
219 let lion_state_bytes = lion.into_bytes().unwrap().len();
220
221 let model = LinearConfig::new(64, 64).with_bias(false).init(&device);
222 let mut adamw = AdamWConfig::new().with_weight_decay(0.0).init();
223 let grads =
224 GradientsParams::from_grads(model.clone().forward(input).sum().backward(), &model);
225 let _model = adamw.step(1e-3, model, grads);
226 let adamw_state_bytes = adamw.into_bytes().unwrap().len();
227
228 println!(
229 "serialized optimizer state: Lion {lion_state_bytes} bytes; \
230 AdamW {adamw_state_bytes} bytes"
231 );
232 assert!(
233 lion_state_bytes < adamw_state_bytes,
234 "expected Lion state ({lion_state_bytes} bytes) to be smaller than AdamW state \
235 ({adamw_state_bytes} bytes)"
236 );
237 }
238
239 fn tiny_linear_model(device: &Device) -> Linear {
240 Linear {
241 weight: Param::from_data(TensorData::from([[0.0], [0.0]]), device),
242 bias: None,
243 }
244 }
245
246 fn train_tiny_regression(
247 mut model: Linear,
248 mut optimizer: ModuleOptimizer,
249 peak_lr: LearningRate,
250 device: &Device,
251 ) -> (f32, f32) {
252 let input =
253 Tensor::<2>::from_floats([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [-1.0, 1.0]], device);
254 let target = Tensor::<2>::from_floats([[0.5], [-0.75], [-0.25], [-1.25]], device);
255 let loss = MseLoss::new();
256 let initial = loss
257 .forward(
258 model.forward(input.clone()),
259 target.clone(),
260 Reduction::Mean,
261 )
262 .into_scalar::<f32>();
263 let steps = 600;
264
265 for step in 0..steps {
266 let output = model.forward(input.clone());
267 let value = loss.forward(output, target.clone(), Reduction::Mean);
268 let grads = GradientsParams::from_grads(value.backward(), &model);
269 let lr = peak_lr * (1.0 - step as f64 / steps as f64);
270 model = optimizer.step(lr, model, grads);
271 }
272
273 let final_loss = loss
274 .forward(model.forward(input), target, Reduction::Mean)
275 .into_scalar::<f32>();
276 (initial, final_loss)
277 }
278}