burn_core/module/base.rs
1use crate::module::{LoraConfig, ParamGroup};
2
3use super::{LoraMapper, Param, ParamId, QLoraMapper, Quantizer};
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 LoRA adapters to the module's 2-D weights, freezing the base weights.
220 ///
221 /// The same module keeps working without any code changes; adapted weights now produce
222 /// `base + scale * (a @ b)`, and only the adapter factors are trainable.
223 fn apply_lora(self, config: LoraConfig) -> Self
224 where
225 Self: Sized,
226 {
227 let mut mapper = LoraMapper::new(config);
228 self.map(&mut mapper)
229 }
230
231 /// Apply QLoRA to the module: quantize the (frozen) base weights and attach trainable LoRA
232 /// adapters to 2-D weights.
233 fn apply_qlora(self, config: LoraConfig, quantizer: Quantizer) -> Self
234 where
235 Self: Sized,
236 {
237 let mut mapper = QLoraMapper::new(config, quantizer);
238 self.map(&mut mapper)
239 }
240
241 /// Collect this module's parameters into a [`ModuleRecord`](crate::store::ModuleRecord).
242 ///
243 /// The record can be saved to a burnpack file or byte buffer and applied back with
244 /// [`load_record`](Module::load_record).
245 fn into_record(self) -> crate::store::ModuleRecord
246 where
247 Self: Sized,
248 {
249 crate::store::ModuleRecord::from_module(self)
250 }
251
252 /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, returning the loaded
253 /// module.
254 ///
255 /// Honors the record's [`DTypePolicy`](crate::store::DTypePolicy), `validate`, and
256 /// `allow_partial` settings.
257 fn try_load_record(
258 self,
259 record: crate::store::ModuleRecord,
260 ) -> Result<Self, crate::store::RecordError>
261 where
262 Self: Sized,
263 {
264 record.apply(self)
265 }
266
267 /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, consuming and returning
268 /// it.
269 ///
270 /// Panics if validation fails; use [`try_load_record`](Module::try_load_record) for the
271 /// fallible variant.
272 fn load_record(self, record: crate::store::ModuleRecord) -> Self
273 where
274 Self: Sized,
275 {
276 self.try_load_record(record).expect("Failed to load record")
277 }
278
279 /// Save this module's parameters to a burnpack file on disk.
280 ///
281 /// Convenience for [`into_record`](Module::into_record) followed by
282 /// [`ModuleRecord::save`](crate::store::ModuleRecord::save). For non-default load behavior
283 /// (dtype policy, partial loading, validation), go through the record directly.
284 #[cfg(feature = "std")]
285 fn save_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), crate::store::RecordError>
286 where
287 Self: Sized,
288 {
289 self.into_record().save(path)
290 }
291
292 /// Load this module's parameters from a burnpack file on disk, returning the loaded module.
293 ///
294 /// Uses the default load behavior. Panics on I/O or validation errors; use
295 /// [`try_load_file`](Module::try_load_file) for the fallible variant, or go through
296 /// [`ModuleRecord`](crate::store::ModuleRecord) to configure dtype policy, partial loading or
297 /// validation.
298 #[cfg(feature = "std")]
299 fn load_file<P: AsRef<std::path::Path>>(self, path: P) -> Self
300 where
301 Self: Sized,
302 {
303 self.try_load_file(path)
304 .expect("Failed to load module from file")
305 }
306
307 /// Fallible variant of [`load_file`](Module::load_file).
308 ///
309 /// Reads the record from `path` with [`ModuleRecord::load`](crate::store::ModuleRecord::load)
310 /// and applies it through [`try_load_record`](Module::try_load_record).
311 #[cfg(feature = "std")]
312 fn try_load_file<P: AsRef<std::path::Path>>(
313 self,
314 path: P,
315 ) -> Result<Self, crate::store::RecordError>
316 where
317 Self: Sized,
318 {
319 let record = crate::store::ModuleRecord::load(path)?;
320 self.try_load_record(record)
321 }
322}
323
324/// Module visitor trait for traversing and inspecting module parameters.
325pub trait ModuleVisitor {
326 /// Visit a float parameter in the module.
327 ///
328 /// # Parameters
329 /// - `param`: The float parameter to visit
330 #[allow(unused_variables)]
331 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {}
332
333 /// Visit an int parameter in the module.
334 ///
335 /// # Parameters
336 /// - `param`: The integer parameter to visit
337 #[allow(unused_variables)]
338 fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {}
339
340 /// Visit a bool parameter in the module.
341 ///
342 /// # Parameters
343 /// - `param`: The boolean parameter to visit
344 #[allow(unused_variables)]
345 fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {}
346
347 /// Called when entering a submodule.
348 ///
349 /// # Parameters
350 /// - `name`: The name of the submodule being entered
351 /// - `container_type`: The type of the container with format:
352 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
353 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
354 /// - For Vec containers: "Vec" (name is the index)
355 /// - For Tuple containers: "Tuple" (name is the index)
356 /// - For Array containers: "Array" (name is the index)
357 ///
358 /// Note: Option containers do not call enter_module/exit_module to preserve
359 /// the field name in the path (e.g., "bias" instead of "bias.Some")
360 #[allow(unused_variables)]
361 fn enter_module(&mut self, name: &str, container_type: &str) {}
362
363 /// Called when exiting a submodule.
364 ///
365 /// # Parameters
366 /// - `name`: The name of the submodule being exited
367 /// - `container_type`: The type of the container with format:
368 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
369 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
370 /// - For Vec containers: "Vec" (name is the index)
371 /// - For Tuple containers: "Tuple" (name is the index)
372 /// - For Array containers: "Array" (name is the index)
373 ///
374 /// Note: Option containers do not call enter_module/exit_module to preserve
375 /// the field name in the path (e.g., "bias" instead of "bias.Some")
376 #[allow(unused_variables)]
377 fn exit_module(&mut self, name: &str, container_type: &str) {}
378
379 /// Visit a float tensor with its full module path.
380 ///
381 /// # Parameters
382 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
383 /// Each element represents a module name in the hierarchy, with the final element
384 /// being the parameter name. This allows efficient reuse of the path stack.
385 /// - `id`: The unique identifier of the parameter
386 /// - `tensor`: The float tensor to visit
387 #[allow(unused_variables)]
388 fn visit_float_with_path<const D: usize>(
389 &mut self,
390 path: &[String],
391 id: ParamId,
392 tensor: &Tensor<D>,
393 ) {
394 }
395
396 /// Visit an int tensor with its full module path.
397 ///
398 /// # Parameters
399 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
400 /// Each element represents a module name in the hierarchy, with the final element
401 /// being the parameter name. This allows efficient reuse of the path stack.
402 /// - `id`: The unique identifier of the parameter
403 /// - `tensor`: The integer tensor to visit
404 #[allow(unused_variables)]
405 fn visit_int_with_path<const D: usize>(
406 &mut self,
407 path: &[String],
408 id: ParamId,
409 tensor: &Tensor<D, Int>,
410 ) {
411 }
412
413 /// Visit a bool tensor with its full module path.
414 ///
415 /// # Parameters
416 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
417 /// Each element represents a module name in the hierarchy, with the final element
418 /// being the parameter name. This allows efficient reuse of the path stack.
419 /// - `id`: The unique identifier of the parameter
420 /// - `tensor`: The boolean tensor to visit
421 #[allow(unused_variables)]
422 fn visit_bool_with_path<const D: usize>(
423 &mut self,
424 path: &[String],
425 id: ParamId,
426 tensor: &Tensor<D, Bool>,
427 ) {
428 }
429}
430
431/// Module mapper trait for transforming module parameters.
432pub trait ModuleMapper {
433 /// Called when entering a submodule.
434 ///
435 /// # Parameters
436 /// - `name`: The name of the submodule being entered
437 /// - `container_type`: The type of the container with format:
438 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
439 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
440 /// - For Vec containers: "Vec" (name is the index)
441 /// - For Tuple containers: "Tuple" (name is the index)
442 /// - For Array containers: "Array" (name is the index)
443 ///
444 /// Note: Option containers do not call enter_module/exit_module to preserve
445 /// the field name in the path (e.g., "bias" instead of "bias.Some")
446 #[allow(unused_variables)]
447 fn enter_module(&mut self, name: &str, container_type: &str) {}
448
449 /// Called when exiting a submodule.
450 ///
451 /// # Parameters
452 /// - `name`: The name of the submodule being exited
453 /// - `container_type`: The type of the container with format:
454 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
455 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
456 /// - For Vec containers: "Vec" (name is the index)
457 /// - For Tuple containers: "Tuple" (name is the index)
458 /// - For Array containers: "Array" (name is the index)
459 ///
460 /// Note: Option containers do not call enter_module/exit_module to preserve
461 /// the field name in the path (e.g., "bias" instead of "bias.Some")
462 #[allow(unused_variables)]
463 fn exit_module(&mut self, name: &str, container_type: &str) {}
464
465 /// Map a float parameter in the module.
466 ///
467 /// # Parameters
468 /// - `param`: The float parameter to transform
469 ///
470 /// # Returns
471 /// The transformed parameter
472 #[allow(unused_variables)]
473 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
474 let (id, tensor, mapper) = param.consume();
475 Param::from_mapped_value(id, tensor, mapper)
476 }
477
478 /// Map an int parameter in the module.
479 ///
480 /// # Parameters
481 /// - `param`: The integer parameter to transform
482 ///
483 /// # Returns
484 /// The transformed parameter
485 #[allow(unused_variables)]
486 fn map_int<const D: usize>(&mut self, param: Param<Tensor<D, Int>>) -> Param<Tensor<D, Int>> {
487 let (id, tensor, mapper) = param.consume();
488 Param::from_mapped_value(id, tensor, mapper)
489 }
490
491 /// Map a bool parameter in the module.
492 ///
493 /// # Parameters
494 /// - `param`: The boolean parameter to transform
495 ///
496 /// # Returns
497 /// The transformed parameter
498 #[allow(unused_variables)]
499 fn map_bool<const D: usize>(
500 &mut self,
501 param: Param<Tensor<D, Bool>>,
502 ) -> Param<Tensor<D, Bool>> {
503 let (id, tensor, mapper) = param.consume();
504 Param::from_mapped_value(id, tensor, mapper)
505 }
506}
507
508/// Module with auto-differentiation backend.
509pub trait AutodiffModule: Module + Send + core::fmt::Debug {
510 /// Returns the same module, but on the inner backend without auto-differentiation.
511 fn valid(&self) -> Self;
512
513 /// Wraps an inner module back into an auto-diff module.
514 fn from_inner(module: Self) -> Self;
515}
516
517#[cfg(all(test, feature = "autodiff"))]
518mod tests {
519 use super::*;
520
521 use crate::module::ParamGroup;
522 use crate::{test_device, test_utils::SimpleLinear};
523
524 #[test]
525 fn test_module_val_train_stateful() {
526 let device = test_device().autodiff();
527 let module = SimpleLinear::new(4, 4, &device);
528
529 assert!(module.weight.is_require_grad());
530 assert!(module.weight.require_grad);
531
532 let module = module.valid();
533 assert!(!module.weight.is_require_grad());
534 assert!(module.weight.require_grad); // stateful
535
536 // Without `HasAutodiffModule`, we would need to specify the module type as well, which would be annoying
537 // let module: SimpleLinear<TestAutodiffBackend> = module.train();
538 let module = module.train();
539 assert!(module.weight.is_require_grad());
540 assert!(module.weight.require_grad); // stateful
541
542 let module = module.no_grad();
543 assert!(!module.weight.is_require_grad());
544 assert!(!module.weight.require_grad); // stateful
545
546 let module = module.valid();
547 assert!(!module.weight.is_require_grad()); // always
548 assert!(!module.weight.require_grad); // stateful
549
550 let module = module.train();
551 assert!(!module.weight.is_require_grad());
552 assert!(!module.weight.require_grad); // stateful
553 }
554
555 #[test]
556 fn freeze_group_freezes_only_selected_params() {
557 let device = test_device().autodiff();
558 let module = SimpleLinear::new(4, 4, &device);
559
560 assert!(module.weight.is_require_grad());
561 assert!(module.bias.as_ref().unwrap().is_require_grad());
562
563 let module = module.freeze_group(ParamGroup::from_path("weight"));
564
565 assert!(!module.weight.is_require_grad());
566 assert!(!module.weight.require_grad);
567
568 let bias = module.bias.as_ref().unwrap();
569 assert!(bias.is_require_grad());
570 assert!(bias.require_grad);
571 }
572
573 #[test]
574 fn unfreeze_group_only_thaws_selected_params() {
575 let device = test_device().autodiff();
576 let module = SimpleLinear::new(4, 4, &device);
577
578 let module = module.no_grad();
579 assert!(!module.weight.is_require_grad());
580 assert!(!module.bias.as_ref().unwrap().is_require_grad());
581
582 let module = module.unfreeze_group(ParamGroup::from_path("weight"));
583
584 assert!(module.weight.is_require_grad());
585 assert!(module.weight.require_grad);
586 assert!(!module.bias.as_ref().unwrap().is_require_grad());
587 assert!(!module.bias.as_ref().unwrap().require_grad);
588 }
589}