Skip to main content

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(&param.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}