burn_core/module/base.rs
1use crate::module::{Lora, ParamGroup, QLora};
2
3use super::{ApplyReparameterization, Param, ParamId, Quantizer, Reparameterizer};
4use alloc::{
5 string::{String, ToString},
6 vec::Vec,
7};
8pub use burn_derive::Module;
9use burn_tensor::{Bool, Device, Int, Tensor};
10
11/// Type alias to `Vec<Device>` which supports `no_std` environments, but automatically using
12/// the `alloc` crate.
13pub type Devices = Vec<Device>;
14
15// At the moment, our plan is to continue experimenting with the macro internally and monitor its development.
16// We may consider making it public in the future.
17macro_rules! module {
18 (map=$module:ident, ops=$item:expr) => {{
19 struct Mapper;
20 impl ModuleMapper for Mapper {
21 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
22 let (id, tensor, mapper) = param.consume();
23 let func = $item;
24 let tensor = func(tensor);
25 Param::from_mapped_value(id, tensor, mapper)
26 }
27 }
28 let mut mapper = Mapper;
29 $module.map(&mut mapper)
30 }};
31 (map=$module:ident, ops=$item:expr, group=$group:ident) => {{
32 struct Mapper {
33 pub path: Vec<String>,
34 pub group: ParamGroup,
35 }
36 impl ModuleMapper for Mapper {
37 fn enter_module(&mut self, name: &str, _container_type: &str) {
38 self.path.push(name.to_string());
39 }
40
41 fn exit_module(&mut self, _name: &str, _container_type: &str) {
42 self.path.pop();
43 }
44
45 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
46 let (id, tensor, mapper) = param.consume();
47 let path = self.path.join(".");
48 if self.group.matches(&id, Some(&path)) {
49 let func = $item;
50 let tensor = func(tensor);
51 return Param::from_mapped_value(id, tensor, mapper);
52 }
53 Param::from_mapped_value(id, tensor, mapper)
54 }
55 }
56 let mut mapper = Mapper {
57 path: alloc::vec![],
58 group: $group,
59 };
60 $module.map(&mut mapper)
61 }};
62 (visit_float=$module:ident, ops=$item:expr, state=$state_ty:ty, init=$init:expr) => {{
63 struct Visitor<'a> {
64 state: &'a mut $state_ty,
65 }
66 impl<'a> ModuleVisitor for Visitor<'a> {
67 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
68 let func = $item;
69 func(¶m.val(), &mut self.state)
70 }
71 }
72 #[allow(clippy::redundant_closure_call)]
73 let mut state = $init();
74 let mut visitor = Visitor { state: &mut state };
75 $module.visit(&mut visitor);
76 state
77 }};
78}
79
80/// Trait for all neural network modules.
81///
82/// Modules should be created using the [derive](burn_derive::Module) attribute.
83/// This will make your module trainable, savable and loadable via
84/// `state` and `load`.
85///
86/// # Example
87///
88/// ```rust, ignore
89/// // Not necessary when using the burn crate directly.
90/// use burn_core as burn;
91///
92/// use burn::{
93/// module::Module,
94/// nn::Linear,
95/// tensor::Tensor,
96/// };
97///
98/// #[derive(Module, Debug)]
99/// struct MyModule {
100/// my_param: Linear,
101/// my_other_field: usize,
102/// }
103/// ```
104pub trait Module: Clone + Send + core::fmt::Debug {
105 /// Return all the devices found in the underneath module tree added to the given vector
106 /// without duplicates.
107 fn collect_devices(&self, devices: Devices) -> Devices;
108
109 /// Return all the devices found in the underneath module tree without duplicates.
110 fn devices(&self) -> Devices {
111 self.collect_devices(Devices::new())
112 }
113
114 /// Fork the module and all of its sub-modules to the given device.
115 ///
116 /// # Notes
117 ///
118 /// This is similar to [to_device](Module::to_device), but it ensures the output module on the
119 /// new device will have its own autodiff graph.
120 fn fork(self, device: &Device) -> Self;
121
122 /// Move the module and all of its sub-modules to the given device.
123 ///
124 /// # Warnings
125 ///
126 /// The operation supports autodiff and it will be registered when activated. However, this may
127 /// not be what you want. The output model will be an intermediary model, meaning that you
128 /// can't optimize it with gradient descent. If you want to optimize the output network on the
129 /// target device, use [fork](Module::fork) instead.
130 fn to_device(self, device: &Device) -> Self;
131
132 /// Each tensor in the module tree will not require grad.
133 ///
134 /// # Warnings
135 ///
136 /// This should not be used for inference, use [valid](AutodiffModule::valid) when using
137 /// AD modules. This is mostly useful when performing partial finetuning, which is updating only
138 /// a small fraction of the parameters instead of finetuning all of them.
139 fn no_grad(self) -> Self {
140 module!(
141 map = self,
142 ops = |tensor: Tensor<D>| tensor.set_require_grad(false)
143 )
144 }
145
146 /// Set `require_grad` to `false` for every parameter in the given group, leaving the rest
147 /// of the module untouched.
148 ///
149 /// This is the group-scoped counterpart to [no_grad](Module::no_grad): where `no_grad` freezes
150 /// the whole module tree, `freeze_group` freezes only the parameters matched by `group`.
151 ///
152 /// # Warnings
153 ///
154 /// Like [no_grad](Module::no_grad), this should not be used for inference; use
155 /// [valid](AutodiffModule::valid) with AD modules instead.
156 fn freeze_group(self, group: ParamGroup) -> Self {
157 module!(
158 map = self,
159 ops = |tensor: Tensor<D>| tensor.set_require_grad(false),
160 group = group
161 )
162 }
163
164 /// Set `require_grad` to `true` for every parameter in the given group, leaving the rest
165 /// of the module untouched.
166 ///
167 /// The inverse of [freeze_group](Module::freeze_group): it re-enables gradient tracking for the
168 /// parameters matched by `group`, e.g. to unfreeze a previously frozen module.
169 fn unfreeze_group(self, group: ParamGroup) -> Self {
170 module!(
171 map = self,
172 ops = |tensor: Tensor<D>| tensor.set_require_grad(true),
173 group = group
174 )
175 }
176
177 /// Move the module and all of its sub-modules to the autodiff backend.
178 ///
179 /// # Notes
180 ///
181 /// * Only plain modules (not already on an autodiff backend) can be moved.
182 /// * Calling `train()` on a module that is already on an autodiff backend
183 /// will result in a type error, because the module's inner backend does not match.
184 fn train(self) -> Self
185 where
186 Self: AutodiffModule,
187 {
188 AutodiffModule::from_inner(self)
189 }
190
191 /// Get the number of parameters the module has, including all of its sub-modules.
192 fn num_params(&self) -> usize {
193 module!(
194 visit_float = self,
195 ops = |tensor: &Tensor<D>, state: &mut usize| {
196 *state += tensor.shape().num_elements();
197 },
198 state = usize,
199 init = || 0
200 )
201 }
202 /// Visit each tensor parameter in the module with a [visitor](ModuleVisitor).
203 fn visit<Visitor: ModuleVisitor>(&self, visitor: &mut Visitor);
204
205 /// Map each tensor parameter in the module with a [mapper](ModuleMapper).
206 fn map<Mapper: ModuleMapper>(self, mapper: &mut Mapper) -> Self;
207
208 /// Quantize the weights of the module.
209 fn quantize_weights(self, quantizer: &mut Quantizer) -> Self {
210 self.map(quantizer)
211 }
212
213 /// Quantize the weights of the given parameter group.
214 fn quantize_weights_group(self, quantizer: &mut Quantizer, group: ParamGroup) -> Self {
215 quantizer.set_param_group(group);
216 self.map(quantizer)
217 }
218
219 /// Attach reparameterizations using the given [`Reparameterizer`].
220 ///
221 /// Every floating-point parameter is passed to the reparameterizer along with its module path.
222 /// The reparameterizer prepares its structural base and optionally creates the state used by
223 /// [`Param::val`]. [`Lora`] is a built-in example.
224 ///
225 /// # Limitations
226 ///
227 /// Nested reparameterizations are not supported. This method should only be called on modules
228 /// that don't already contain reparameterized parameters.
229 fn apply_reparameterization<R>(self, reparameterizer: R) -> Self
230 where
231 Self: Sized,
232 R: Reparameterizer,
233 {
234 self.map(&mut ApplyReparameterization::new(reparameterizer))
235 }
236
237 /// Attach LoRA adapters to the module's 2-D weights, freezing the base weights.
238 ///
239 /// The same module keeps working without any code changes; adapted weights now produce
240 /// `base + scale * (a @ b)`, and only the adapter factors are trainable.
241 fn apply_lora(self, lora: Lora) -> Self
242 where
243 Self: Sized,
244 {
245 self.apply_reparameterization(lora)
246 }
247
248 /// Apply QLoRA to the module: quantize the (frozen) base weights and attach trainable LoRA
249 /// adapters to 2-D weights.
250 fn apply_qlora(self, qlora: QLora) -> Self
251 where
252 Self: Sized,
253 {
254 self.apply_reparameterization(qlora)
255 }
256
257 /// Collect this module's parameters into a [`ModuleRecord`](crate::store::ModuleRecord).
258 ///
259 /// The record can be saved to a burnpack file or byte buffer and applied back with
260 /// [`load_record`](Module::load_record).
261 fn into_record(self) -> crate::store::ModuleRecord
262 where
263 Self: Sized,
264 {
265 crate::store::ModuleRecord::from_module(self, None)
266 }
267
268 /// Collect the parameters `group` names into a [`ModuleRecord`](crate::store::ModuleRecord).
269 ///
270 /// The record of a part of the module rather than all of it — what a run that trained a
271 /// group writes when the rest of the module is the checkpoint it started from, and what
272 /// [`load_record`](Module::load_record) applies back over that checkpoint (with
273 /// [`allow_partial`](crate::store::ModuleRecord::allow_partial), since the record holds
274 /// nothing for the parameters outside the group).
275 ///
276 /// A parameter the group does not match is skipped before its data is read, so this never
277 /// materializes the rest of the module.
278 fn into_record_group(self, group: ParamGroup) -> crate::store::ModuleRecord
279 where
280 Self: Sized,
281 {
282 crate::store::ModuleRecord::from_module(self, Some(group))
283 }
284
285 /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, returning the loaded
286 /// module.
287 ///
288 /// Honors the record's [`DTypePolicy`](crate::store::DTypePolicy), `validate`, and
289 /// `allow_partial` settings.
290 fn try_load_record(
291 self,
292 record: crate::store::ModuleRecord,
293 ) -> Result<Self, crate::store::RecordError>
294 where
295 Self: Sized,
296 {
297 record.apply(self)
298 }
299
300 /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, consuming and returning
301 /// it.
302 ///
303 /// Panics if validation fails; use [`try_load_record`](Module::try_load_record) for the
304 /// fallible variant.
305 fn load_record(self, record: crate::store::ModuleRecord) -> Self
306 where
307 Self: Sized,
308 {
309 self.try_load_record(record).expect("Failed to load record")
310 }
311
312 /// Save this module's parameters to a burnpack file on disk.
313 ///
314 /// Convenience for [`into_record`](Module::into_record) followed by
315 /// [`ModuleRecord::save`](crate::store::ModuleRecord::save). For non-default load behavior
316 /// (dtype policy, partial loading, validation), go through the record directly.
317 #[cfg(feature = "std")]
318 fn save_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), crate::store::RecordError>
319 where
320 Self: Sized,
321 {
322 self.into_record().save(path)
323 }
324
325 /// Load this module's parameters from a burnpack file on disk, returning the loaded module.
326 ///
327 /// Uses the default load behavior. Panics on I/O or validation errors; use
328 /// [`try_load_file`](Module::try_load_file) for the fallible variant, or go through
329 /// [`ModuleRecord`](crate::store::ModuleRecord) to configure dtype policy, partial loading or
330 /// validation.
331 #[cfg(feature = "std")]
332 fn load_file<P: AsRef<std::path::Path>>(self, path: P) -> Self
333 where
334 Self: Sized,
335 {
336 self.try_load_file(path)
337 .expect("Failed to load module from file")
338 }
339
340 /// Fallible variant of [`load_file`](Module::load_file).
341 ///
342 /// Reads the record from `path` with [`ModuleRecord::load`](crate::store::ModuleRecord::load)
343 /// and applies it through [`try_load_record`](Module::try_load_record).
344 #[cfg(feature = "std")]
345 fn try_load_file<P: AsRef<std::path::Path>>(
346 self,
347 path: P,
348 ) -> Result<Self, crate::store::RecordError>
349 where
350 Self: Sized,
351 {
352 let record = crate::store::ModuleRecord::load(path)?;
353 self.try_load_record(record)
354 }
355}
356
357/// Module visitor trait for traversing and inspecting module parameters.
358pub trait ModuleVisitor {
359 /// Visit a float parameter in the module.
360 ///
361 /// # Parameters
362 /// - `param`: The float parameter to visit
363 #[allow(unused_variables)]
364 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {}
365
366 /// Visit an int parameter in the module.
367 ///
368 /// # Parameters
369 /// - `param`: The integer parameter to visit
370 #[allow(unused_variables)]
371 fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {}
372
373 /// Visit a bool parameter in the module.
374 ///
375 /// # Parameters
376 /// - `param`: The boolean parameter to visit
377 #[allow(unused_variables)]
378 fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {}
379
380 /// Called when entering a submodule.
381 ///
382 /// # Parameters
383 /// - `name`: The name of the submodule being entered
384 /// - `container_type`: The type of the container with format:
385 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
386 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
387 /// - For Vec containers: "Vec" (name is the index)
388 /// - For Tuple containers: "Tuple" (name is the index)
389 /// - For Array containers: "Array" (name is the index)
390 ///
391 /// Note: Option containers do not call enter_module/exit_module to preserve
392 /// the field name in the path (e.g., "bias" instead of "bias.Some")
393 #[allow(unused_variables)]
394 fn enter_module(&mut self, name: &str, container_type: &str) {}
395
396 /// Called when exiting a submodule.
397 ///
398 /// # Parameters
399 /// - `name`: The name of the submodule being exited
400 /// - `container_type`: The type of the container with format:
401 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
402 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
403 /// - For Vec containers: "Vec" (name is the index)
404 /// - For Tuple containers: "Tuple" (name is the index)
405 /// - For Array containers: "Array" (name is the index)
406 ///
407 /// Note: Option containers do not call enter_module/exit_module to preserve
408 /// the field name in the path (e.g., "bias" instead of "bias.Some")
409 #[allow(unused_variables)]
410 fn exit_module(&mut self, name: &str, container_type: &str) {}
411
412 /// Visit a float tensor with its full module path.
413 ///
414 /// # Parameters
415 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
416 /// Each element represents a module name in the hierarchy, with the final element
417 /// being the parameter name. This allows efficient reuse of the path stack.
418 /// - `id`: The unique identifier of the parameter
419 /// - `tensor`: The float tensor to visit
420 #[allow(unused_variables)]
421 fn visit_float_with_path<const D: usize>(
422 &mut self,
423 path: &[String],
424 id: ParamId,
425 tensor: &Tensor<D>,
426 ) {
427 }
428
429 /// Visit an int tensor with its full module path.
430 ///
431 /// # Parameters
432 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
433 /// Each element represents a module name in the hierarchy, with the final element
434 /// being the parameter name. This allows efficient reuse of the path stack.
435 /// - `id`: The unique identifier of the parameter
436 /// - `tensor`: The integer tensor to visit
437 #[allow(unused_variables)]
438 fn visit_int_with_path<const D: usize>(
439 &mut self,
440 path: &[String],
441 id: ParamId,
442 tensor: &Tensor<D, Int>,
443 ) {
444 }
445
446 /// Visit a bool tensor with its full module path.
447 ///
448 /// # Parameters
449 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
450 /// Each element represents a module name in the hierarchy, with the final element
451 /// being the parameter name. This allows efficient reuse of the path stack.
452 /// - `id`: The unique identifier of the parameter
453 /// - `tensor`: The boolean tensor to visit
454 #[allow(unused_variables)]
455 fn visit_bool_with_path<const D: usize>(
456 &mut self,
457 path: &[String],
458 id: ParamId,
459 tensor: &Tensor<D, Bool>,
460 ) {
461 }
462}
463
464/// Module mapper trait for transforming module parameters.
465pub trait ModuleMapper {
466 /// Called when entering a submodule.
467 ///
468 /// # Parameters
469 /// - `name`: The name of the submodule being entered
470 /// - `container_type`: The type of the container with format:
471 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
472 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
473 /// - For Vec containers: "Vec" (name is the index)
474 /// - For Tuple containers: "Tuple" (name is the index)
475 /// - For Array containers: "Array" (name is the index)
476 ///
477 /// Note: Option containers do not call enter_module/exit_module to preserve
478 /// the field name in the path (e.g., "bias" instead of "bias.Some")
479 #[allow(unused_variables)]
480 fn enter_module(&mut self, name: &str, container_type: &str) {}
481
482 /// Called when exiting a submodule.
483 ///
484 /// # Parameters
485 /// - `name`: The name of the submodule being exited
486 /// - `container_type`: The type of the container with format:
487 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
488 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
489 /// - For Vec containers: "Vec" (name is the index)
490 /// - For Tuple containers: "Tuple" (name is the index)
491 /// - For Array containers: "Array" (name is the index)
492 ///
493 /// Note: Option containers do not call enter_module/exit_module to preserve
494 /// the field name in the path (e.g., "bias" instead of "bias.Some")
495 #[allow(unused_variables)]
496 fn exit_module(&mut self, name: &str, container_type: &str) {}
497
498 /// Map a float parameter in the module.
499 ///
500 /// # Parameters
501 /// - `param`: The float parameter to transform
502 ///
503 /// # Returns
504 /// The transformed parameter
505 #[allow(unused_variables)]
506 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
507 let (id, tensor, mapper) = param.consume();
508 Param::from_mapped_value(id, tensor, mapper)
509 }
510
511 /// Map an int parameter in the module.
512 ///
513 /// # Parameters
514 /// - `param`: The integer parameter to transform
515 ///
516 /// # Returns
517 /// The transformed parameter
518 #[allow(unused_variables)]
519 fn map_int<const D: usize>(&mut self, param: Param<Tensor<D, Int>>) -> Param<Tensor<D, Int>> {
520 let (id, tensor, mapper) = param.consume();
521 Param::from_mapped_value(id, tensor, mapper)
522 }
523
524 /// Map a bool parameter in the module.
525 ///
526 /// # Parameters
527 /// - `param`: The boolean parameter to transform
528 ///
529 /// # Returns
530 /// The transformed parameter
531 #[allow(unused_variables)]
532 fn map_bool<const D: usize>(
533 &mut self,
534 param: Param<Tensor<D, Bool>>,
535 ) -> Param<Tensor<D, Bool>> {
536 let (id, tensor, mapper) = param.consume();
537 Param::from_mapped_value(id, tensor, mapper)
538 }
539}
540
541/// Module with auto-differentiation backend.
542pub trait AutodiffModule: Module + Send + core::fmt::Debug {
543 /// Returns the same module, but on the inner backend without auto-differentiation.
544 fn valid(&self) -> Self;
545
546 /// Wraps an inner module back into an auto-diff module.
547 fn from_inner(module: Self) -> Self;
548}
549
550#[cfg(all(test, feature = "autodiff"))]
551mod tests {
552 use super::*;
553
554 use crate::module::ParamGroup;
555 use crate::{test_device, test_utils::SimpleLinear};
556
557 #[test]
558 fn test_module_val_train_stateful() {
559 let device = test_device().autodiff();
560 let module = SimpleLinear::new(4, 4, &device);
561
562 assert!(module.weight.is_require_grad());
563 assert!(module.weight.require_grad);
564
565 let module = module.valid();
566 assert!(!module.weight.is_require_grad());
567 assert!(module.weight.require_grad); // stateful
568
569 // Without `HasAutodiffModule`, we would need to specify the module type as well, which would be annoying
570 // let module: SimpleLinear<TestAutodiffBackend> = module.train();
571 let module = module.train();
572 assert!(module.weight.is_require_grad());
573 assert!(module.weight.require_grad); // stateful
574
575 let module = module.no_grad();
576 assert!(!module.weight.is_require_grad());
577 assert!(!module.weight.require_grad); // stateful
578
579 let module = module.valid();
580 assert!(!module.weight.is_require_grad()); // always
581 assert!(!module.weight.require_grad); // stateful
582
583 let module = module.train();
584 assert!(!module.weight.is_require_grad());
585 assert!(!module.weight.require_grad); // stateful
586 }
587
588 #[test]
589 fn freeze_group_freezes_only_selected_params() {
590 let device = test_device().autodiff();
591 let module = SimpleLinear::new(4, 4, &device);
592
593 assert!(module.weight.is_require_grad());
594 assert!(module.bias.as_ref().unwrap().is_require_grad());
595
596 let module = module.freeze_group(ParamGroup::from_path("weight"));
597
598 assert!(!module.weight.is_require_grad());
599 assert!(!module.weight.require_grad);
600
601 let bias = module.bias.as_ref().unwrap();
602 assert!(bias.is_require_grad());
603 assert!(bias.require_grad);
604 }
605
606 #[test]
607 fn unfreeze_group_only_thaws_selected_params() {
608 let device = test_device().autodiff();
609 let module = SimpleLinear::new(4, 4, &device);
610
611 let module = module.no_grad();
612 assert!(!module.weight.is_require_grad());
613 assert!(!module.bias.as_ref().unwrap().is_require_grad());
614
615 let module = module.unfreeze_group(ParamGroup::from_path("weight"));
616
617 assert!(module.weight.is_require_grad());
618 assert!(module.weight.require_grad);
619 assert!(!module.bias.as_ref().unwrap().is_require_grad());
620 assert!(!module.bias.as_ref().unwrap().require_grad);
621 }
622}