burn_core/module/base.rs
1use crate::module::{Lora, ParamGroup, QLora};
2
3use super::{ApplyReparameterization, Flag, 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, state=$state:ident:$state_ty:ty) => {{
19 struct Mapper {
20 $state: $state_ty,
21 }
22 impl ModuleMapper for Mapper {
23 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
24 let func = $item;
25 func(param, &self.$state)
26 }
27 }
28 let mut mapper = Mapper { $state };
29 $module.map(&mut mapper)
30 }};
31 (map=$module:ident, ops=$item:expr, training=$training:expr) => {{
32 struct Mapper;
33 impl ModuleMapper for Mapper {
34 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
35 let func = $item;
36 func(param)
37 }
38
39 fn map_flag(&mut self, flag: Param<Flag>) -> Param<Flag> {
40 flag.with_value($training)
41 }
42 }
43 let mut mapper = Mapper;
44 $module.map(&mut mapper)
45 }};
46 (map=$module:ident, ops=$item:expr, group=$group:ident, state=$state:ident:$state_ty:ty) => {{
47 struct Mapper {
48 pub path: Vec<String>,
49 pub group: ParamGroup,
50 $state: $state_ty,
51 }
52 impl ModuleMapper for Mapper {
53 fn enter_module(&mut self, name: &str, _container_type: &str) {
54 self.path.push(name.to_string());
55 }
56
57 fn exit_module(&mut self, _name: &str, _container_type: &str) {
58 self.path.pop();
59 }
60
61 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
62 let path = self.path.join(".");
63 if self.group.matches(¶m.id, Some(&path)) {
64 let func = $item;
65 return func(param, &self.$state);
66 }
67 param
68 }
69 }
70 let mut mapper = Mapper {
71 path: alloc::vec![],
72 group: $group,
73 $state,
74 };
75 $module.map(&mut mapper)
76 }};
77 (map=$module:ident, ops=$item:expr, group=$group:ident, training=$training:expr) => {{
78 struct Mapper {
79 pub path: Vec<String>,
80 pub group: ParamGroup,
81 }
82 impl ModuleMapper for Mapper {
83 fn enter_module(&mut self, name: &str, _container_type: &str) {
84 self.path.push(name.to_string());
85 }
86
87 fn exit_module(&mut self, _name: &str, _container_type: &str) {
88 self.path.pop();
89 }
90
91 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
92 let path = self.path.join(".");
93 if self.group.matches(¶m.id, Some(&path)) {
94 let func = $item;
95 return func(param);
96 }
97 param
98 }
99
100 fn map_flag(&mut self, flag: Param<Flag>) -> Param<Flag> {
101 let path = self.path.join(".");
102 match self.group.matches(&flag.id, Some(&path)) {
103 true => flag.with_value($training),
104 false => flag,
105 }
106 }
107 }
108 let mut mapper = Mapper {
109 path: alloc::vec![],
110 group: $group,
111 };
112 $module.map(&mut mapper)
113 }};
114 (visit_float=$module:ident, ops=$item:expr, state=$state_ty:ty, init=$init:expr) => {{
115 struct Visitor<'a> {
116 state: &'a mut $state_ty,
117 }
118 impl<'a> ModuleVisitor for Visitor<'a> {
119 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
120 let func = $item;
121 func(param, &mut self.state)
122 }
123 }
124 #[allow(clippy::redundant_closure_call)]
125 let mut state = $init();
126 let mut visitor = Visitor { state: &mut state };
127 $module.visit(&mut visitor);
128 state
129 }};
130}
131
132/// Trait for all neural network modules.
133///
134/// Modules should be created using the [derive](burn_derive::Module) attribute.
135/// This will make your module trainable, savable and loadable via
136/// [`into_record`](Module::into_record) and [`load_record`](Module::load_record).
137///
138/// # Example
139///
140/// ```rust, ignore
141/// // Not necessary when using the burn crate directly.
142/// use burn_core as burn;
143///
144/// use burn::{
145/// module::Module,
146/// nn::Linear,
147/// tensor::Tensor,
148/// };
149///
150/// #[derive(Module, Debug)]
151/// struct MyModule {
152/// my_param: Linear,
153/// my_other_field: usize,
154/// }
155/// ```
156/// Training and validation use the same module type. This trait provides state transitions,
157/// not a guarantee that a value currently has autodiff enabled. Inspect individual parameter
158/// tensors with [`Tensor::is_autodiff`] and [`Tensor::is_require_grad`]; contexts can be mixed.
159pub trait Module: Clone + Send + core::fmt::Debug {
160 /// Return all the devices found in the underneath module tree added to the given vector
161 /// without duplicates.
162 fn collect_devices(&self, devices: Devices) -> Devices;
163
164 /// Return all the devices found in the underneath module tree without duplicates.
165 fn devices(&self) -> Devices {
166 self.collect_devices(Devices::new())
167 }
168
169 /// Fork the module and all of its sub-modules to the given device.
170 ///
171 /// # Notes
172 ///
173 /// This is similar to [to_device](Module::to_device), but it ensures the output module on the
174 /// new device will have its own autodiff graph.
175 /// Both transfers preserve the source tensors' autodiff association and checkpointing
176 /// strategy. The destination's autodiff defaults do not enable training; use
177 /// [`train`](Module::train) first when starting from a validation module.
178 ///
179 /// A parameter not initialized yet initializes on the destination, unless a clone shares
180 /// it, in which case it initializes where it is and is then copied.
181 fn fork(self, device: &Device) -> Self;
182
183 /// Move the module and all of its sub-modules to the given device.
184 ///
185 /// # Warnings
186 ///
187 /// The operation supports autodiff and it will be registered when activated. However, this may
188 /// not be what you want. The output model will be an intermediary model, meaning that you
189 /// can't optimize it with gradient descent. If you want to optimize the output network on the
190 /// target device, use [fork](Module::fork) instead.
191 ///
192 /// A parameter not initialized yet initializes on the destination, unless a clone shares
193 /// it, in which case it initializes where it is and is then moved.
194 fn to_device(self, device: &Device) -> Self;
195
196 /// Set whether every floating-point tensor parameter in the module tree requires gradients.
197 ///
198 /// Module-owned control flags are left unchanged. Use [`freeze`](Module::freeze) to disable
199 /// both gradients and training behavior.
200 /// On a backend without autodiff, the enabled setting is preserved and takes effect when
201 /// [`train`](Module::train) transitions the module back to training.
202 /// Setting this to `true` overrides selective gradient configurations such as the frozen dense
203 /// base weights established by [`apply_lora`](Module::apply_lora).
204 fn set_require_grad(self, require_grad: bool) -> Self {
205 module!(
206 map = self,
207 ops = |param: Param<Tensor<D>>, require_grad: &bool| param
208 .set_require_grad(*require_grad),
209 state = require_grad: bool
210 )
211 }
212
213 /// Set whether matched floating-point tensor parameters require gradients.
214 ///
215 /// Only floating-point tensor parameters within `group` are affected. Other parameter values
216 /// matched by the group, including module-owned control flags, are ignored; unmatched tensor
217 /// parameters are also left unchanged. This means a group created with
218 /// [`ParamGroup::ids_from_module`] can select an entire subtree while this method changes only
219 /// its tensor gradient state.
220 /// On a backend without autodiff, the enabled setting is preserved and takes effect through
221 /// [`train`](Module::train).
222 ///
223 /// Use [`freeze_group`](Module::freeze_group) or
224 /// [`unfreeze_group`](Module::unfreeze_group) when matched training flags should be changed
225 /// together with tensor gradients.
226 fn set_require_grad_group(self, group: ParamGroup, require_grad: bool) -> Self {
227 module!(
228 map = self,
229 ops = |param: Param<Tensor<D>>, require_grad: &bool| param
230 .set_require_grad(*require_grad),
231 group = group,
232 state = require_grad: bool
233 )
234 }
235
236 /// Disable gradients for every floating-point tensor parameter in the module tree.
237 ///
238 /// This is equivalent to `set_require_grad(false)`. Module-owned control flags are left
239 /// unchanged, so dropout, noise, randomized activations and batch-normalization running
240 /// statistics retain their current behavior. Use [`freeze`](Module::freeze) to disable both
241 /// gradients and training behavior.
242 ///
243 /// # Gradient and training state
244 ///
245 /// This persistently disables gradient tracking, including on a backend without autodiff. A
246 /// later [`train`](Module::train) keeps gradients disabled; it does not undo `no_grad`.
247 ///
248 /// # Warnings
249 ///
250 /// Use [valid](Module::valid) for inference. `no_grad` is useful for partial fine-tuning
251 /// when tensor gradients should be disabled
252 /// while layer training behavior remains active. For example, matched batch-normalization
253 /// layers continue updating their running statistics, and dropout remains enabled. Use
254 /// [`freeze`](Module::freeze) when that module-owned training behavior should also stop.
255 fn no_grad(self) -> Self {
256 self.set_require_grad(false)
257 }
258
259 /// Freeze the whole module tree.
260 ///
261 /// Every floating-point tensor parameter stops requiring gradients, and every module-owned
262 /// training flag is disabled. This applies both to parameterless stochastic layers such as
263 /// dropout, noise and randomized activations, and to stateful layers such as batch
264 /// normalization.
265 ///
266 /// # Gradient and training state
267 ///
268 /// Gradient tracking and training flags are persistently disabled. On a backend without
269 /// autodiff, flags are disabled immediately, and [`train`](Module::train) keeps tensors
270 /// untracked rather than undoing the freeze.
271 ///
272 /// # Warnings
273 ///
274 /// Use [`valid`](Module::valid) for inference. `freeze` is intended for partial finetuning
275 /// where a module remains on the training
276 /// device but should not participate in training.
277 fn freeze(self) -> Self {
278 module!(
279 map = self,
280 ops = |param: Param<Tensor<D>>| param.set_require_grad(false),
281 training = false
282 )
283 }
284
285 /// Unfreeze the whole module tree.
286 ///
287 /// Every floating-point tensor parameter is configured to require gradients, and every
288 /// module-owned training flag is set to enabled rather than restored to a prior value. This
289 /// overwrites selective gradient configurations, including frozen dense base weights
290 /// established by [`apply_lora`](Module::apply_lora).
291 ///
292 /// # Gradient and training state
293 ///
294 /// On a backend without autodiff, training flags are enabled immediately while gradient
295 /// tracking takes effect when [`train`](Module::train) transitions the module back to the
296 /// autodiff backend.
297 fn unfreeze(self) -> Self {
298 module!(
299 map = self,
300 ops = |param: Param<Tensor<D>>| param.set_require_grad(true),
301 training = true
302 )
303 }
304
305 /// Freeze every module-owned value in the given group, leaving the rest of the module
306 /// untouched.
307 ///
308 /// This is the group-scoped counterpart to [`freeze`](Module::freeze): where `freeze` freezes
309 /// the whole module tree, `freeze_group` clears gradient requirements on matched tensor
310 /// parameters and disables matched training flags.
311 ///
312 /// # Gradient and training state
313 ///
314 /// Gradient tracking and training flags are persistently disabled for matched values. On a
315 /// backend without autodiff, matched flags are disabled immediately, and
316 /// [`train`](Module::train) keeps matched tensors untracked rather than undoing the group
317 /// freeze.
318 ///
319 /// # Warnings
320 ///
321 /// Like [`freeze`](Module::freeze), this should not be used for inference; use
322 /// [valid](Module::valid) instead.
323 fn freeze_group(self, group: ParamGroup) -> Self {
324 module!(
325 map = self,
326 ops = |param: Param<Tensor<D>>| param.set_require_grad(false),
327 group = group,
328 training = false
329 )
330 }
331
332 /// Unfreeze every module-owned value in the given group, leaving the rest of the module
333 /// untouched.
334 ///
335 /// The inverse of [freeze_group](Module::freeze_group): matched tensor parameters are configured
336 /// to require gradients, and matched training flags are set to enabled rather than restored to
337 /// prior values.
338 ///
339 /// # Gradient and training state
340 ///
341 /// On a backend without autodiff, matched training flags are enabled immediately while gradient
342 /// tracking for matched tensors takes effect when [`train`](Module::train) transitions the
343 /// module back to the autodiff backend.
344 fn unfreeze_group(self, group: ParamGroup) -> Self {
345 module!(
346 map = self,
347 ops = |param: Param<Tensor<D>>| param.set_require_grad(true),
348 group = group,
349 training = true
350 )
351 }
352
353 /// Enables autodiff and restores configured training state throughout the module.
354 ///
355 /// # Gradient and training state
356 ///
357 /// This is the supported transition back to training after [`valid`](Module::valid).
358 /// It applies the latest gradient-tracking settings and training-flag values. Mappings, device
359 /// moves, forks and explicit state changes on a validation module preserve or update what this
360 /// method applies.
361 ///
362 /// Tensor-bearing modules require the `autodiff` feature to enable autodiff. This does not
363 /// undo explicit freezing or reconstruct state discarded by [`valid`](Module::valid).
364 fn train(self) -> Self;
365
366 /// Returns a validation snapshot without autodiff or active training flags.
367 ///
368 /// # Gradient and training state
369 ///
370 /// Tensor gradient requirements and module-owned training flags are disabled in the returned
371 /// value while their configured state is retained internally. Use [`Module::train`] to apply
372 /// that state. Mappings, device moves and forks preserve both the disabled effective validation
373 /// state and what `train` will restore. Calling `valid` when the module's tensors are already on
374 /// a plain device still disables its training flags, so the operation is idempotent but not
375 /// necessarily a structural no-op.
376 ///
377 /// The returned value is an inference snapshot: parameter reparameterizations are folded into
378 /// their values, and tensor checkpointing strategies are removed with autodiff. Calling
379 /// [`Module::train`] on that snapshot does not reconstruct those reparameterizations or restore
380 /// the previous checkpointing strategies. Keep the original module when continuing training
381 /// after validation.
382 fn valid(&self) -> Self;
383
384 /// Get the number of parameters the module has, including all of its sub-modules, without
385 /// initializing the ones not initialized yet.
386 fn num_params(&self) -> usize {
387 module!(
388 visit_float = self,
389 ops = |param: &Param<Tensor<D>>, state: &mut usize| {
390 *state += param.lazy_shape().num_elements();
391 },
392 state = usize,
393 init = || 0
394 )
395 }
396 /// Visit each parameter and module-owned control value with a [visitor](ModuleVisitor).
397 fn visit<Visitor: ModuleVisitor>(&self, visitor: &mut Visitor);
398
399 /// Map each parameter and module-owned control value with a [mapper](ModuleMapper).
400 fn map<Mapper: ModuleMapper>(self, mapper: &mut Mapper) -> Self;
401
402 /// Quantize the weights of the module.
403 fn quantize_weights(self, quantizer: &mut Quantizer) -> Self {
404 self.map(quantizer)
405 }
406
407 /// Quantize the weights of the given parameter group.
408 fn quantize_weights_group(self, quantizer: &mut Quantizer, group: ParamGroup) -> Self {
409 quantizer.set_param_group(group);
410 self.map(quantizer)
411 }
412
413 /// Attach reparameterizations using the given [`Reparameterizer`].
414 ///
415 /// Every floating-point parameter is passed to the reparameterizer along with its module path.
416 /// The reparameterizer prepares its structural base and optionally creates the state used by
417 /// [`Param::val`]. [`Lora`] is a built-in example.
418 ///
419 /// # Limitations
420 ///
421 /// Nested reparameterizations are not supported. This method should only be called on modules
422 /// that don't already contain reparameterized parameters.
423 fn apply_reparameterization<R>(self, reparameterizer: R) -> Self
424 where
425 Self: Sized,
426 R: Reparameterizer,
427 {
428 self.map(&mut ApplyReparameterization::new(reparameterizer))
429 }
430
431 /// Attach LoRA adapters to the module's 2-D weights, freezing all base tensor parameters.
432 ///
433 /// The same module keeps working without any code changes; adapted weights now produce
434 /// `base + scale * (a @ b)`, and only the adapter factors are trainable.
435 /// Module-owned control flags are preserved, so dropout and batch normalization keep their
436 /// current training behavior. Call [`freeze`](Module::freeze) before this method if the base
437 /// module's control behavior and running statistics should also be frozen.
438 fn apply_lora(self, lora: Lora) -> Self
439 where
440 Self: Sized,
441 {
442 self.apply_reparameterization(lora)
443 }
444
445 /// Apply QLoRA to the module: quantize the (frozen) base tensor parameters and attach trainable
446 /// LoRA adapters to 2-D weights.
447 ///
448 /// Module-owned control flags are preserved. Call [`freeze`](Module::freeze) before this
449 /// method if the base module's control behavior and running statistics should also be frozen.
450 fn apply_qlora(self, qlora: QLora) -> Self
451 where
452 Self: Sized,
453 {
454 self.apply_reparameterization(qlora)
455 }
456
457 /// Collect this module's tensor parameters into a [`ModuleRecord`](crate::store::ModuleRecord).
458 ///
459 /// The record can be saved to a burnpack file or byte buffer and applied back with
460 /// [`load_record`](Module::load_record).
461 /// Module-owned control values such as [`Flag`] are runtime configuration and are not
462 /// recorded; loading preserves their state and identity from the destination module.
463 fn into_record(self) -> crate::store::ModuleRecord
464 where
465 Self: Sized,
466 {
467 crate::store::ModuleRecord::from_module(self, None)
468 }
469
470 /// Collect the tensor parameters `group` names into a [`ModuleRecord`](crate::store::ModuleRecord).
471 ///
472 /// The record of a part of the module rather than all of it — what a run that trained a
473 /// group writes when the rest of the module is the checkpoint it started from, and what
474 /// [`load_record`](Module::load_record) applies back over that checkpoint (with
475 /// [`allow_partial`](crate::store::ModuleRecord::allow_partial), since the record holds
476 /// nothing for the parameters outside the group).
477 ///
478 /// A parameter the group does not match is skipped before its data is read, so this never
479 /// materializes the rest of the module.
480 fn into_record_group(self, group: ParamGroup) -> crate::store::ModuleRecord
481 where
482 Self: Sized,
483 {
484 crate::store::ModuleRecord::from_module(self, Some(group))
485 }
486
487 /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, returning the loaded
488 /// module.
489 ///
490 /// Honors the record's [`DTypePolicy`](crate::store::DTypePolicy), `validate`, and
491 /// `allow_partial` settings.
492 fn try_load_record(
493 self,
494 record: crate::store::ModuleRecord,
495 ) -> Result<Self, crate::store::RecordError>
496 where
497 Self: Sized,
498 {
499 record.apply(self)
500 }
501
502 /// Apply a [`ModuleRecord`](crate::store::ModuleRecord) to this module, consuming and returning
503 /// it.
504 ///
505 /// Panics if validation fails; use [`try_load_record`](Module::try_load_record) for the
506 /// fallible variant.
507 fn load_record(self, record: crate::store::ModuleRecord) -> Self
508 where
509 Self: Sized,
510 {
511 self.try_load_record(record).expect("Failed to load record")
512 }
513
514 /// Save this module's parameters to a burnpack file on disk.
515 ///
516 /// Convenience for [`into_record`](Module::into_record) followed by
517 /// [`ModuleRecord::save`](crate::store::ModuleRecord::save). For non-default load behavior
518 /// (dtype policy, partial loading, validation), go through the record directly.
519 #[cfg(feature = "std")]
520 fn save_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), crate::store::RecordError>
521 where
522 Self: Sized,
523 {
524 self.into_record().save(path)
525 }
526
527 /// Load this module's parameters from a burnpack file on disk, returning the loaded module.
528 ///
529 /// Uses the default load behavior. Panics on I/O or validation errors; use
530 /// [`try_load_file`](Module::try_load_file) for the fallible variant, or go through
531 /// [`ModuleRecord`](crate::store::ModuleRecord) to configure dtype policy, partial loading or
532 /// validation.
533 #[cfg(feature = "std")]
534 fn load_file<P: AsRef<std::path::Path>>(self, path: P) -> Self
535 where
536 Self: Sized,
537 {
538 self.try_load_file(path)
539 .expect("Failed to load module from file")
540 }
541
542 /// Fallible variant of [`load_file`](Module::load_file).
543 ///
544 /// Reads the record from `path` with [`ModuleRecord::load`](crate::store::ModuleRecord::load)
545 /// and applies it through [`try_load_record`](Module::try_load_record).
546 #[cfg(feature = "std")]
547 fn try_load_file<P: AsRef<std::path::Path>>(
548 self,
549 path: P,
550 ) -> Result<Self, crate::store::RecordError>
551 where
552 Self: Sized,
553 {
554 let record = crate::store::ModuleRecord::load(path)?;
555 self.try_load_record(record)
556 }
557}
558
559/// Module visitor trait for traversing and inspecting module parameters.
560pub trait ModuleVisitor {
561 /// Visit a float parameter in the module.
562 ///
563 /// # Parameters
564 /// - `param`: The float parameter to visit
565 #[allow(unused_variables)]
566 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {}
567
568 /// Visit an int parameter in the module.
569 ///
570 /// # Parameters
571 /// - `param`: The integer parameter to visit
572 #[allow(unused_variables)]
573 fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {}
574
575 /// Visit a bool parameter in the module.
576 ///
577 /// # Parameters
578 /// - `param`: The boolean parameter to visit
579 #[allow(unused_variables)]
580 fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {}
581
582 /// Visit a [`Param<Flag>`] in the module.
583 ///
584 /// An identified boolean value owned by a module, so traversals can reason about control state
585 /// as well as tensor parameters.
586 ///
587 /// # Parameters
588 /// - `flag`: The flag to visit
589 #[allow(unused_variables)]
590 fn visit_flag(&mut self, flag: &Param<Flag>) {}
591
592 /// Called when entering a submodule.
593 ///
594 /// # Parameters
595 /// - `name`: The name of the submodule being entered
596 /// - `container_type`: The type of the container with format:
597 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
598 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
599 /// - For Vec containers: "Vec" (name is the index)
600 /// - For Tuple containers: "Tuple" (name is the index)
601 /// - For Array containers: "Array" (name is the index)
602 ///
603 /// Note: Option containers do not call enter_module/exit_module to preserve
604 /// the field name in the path (e.g., "bias" instead of "bias.Some")
605 #[allow(unused_variables)]
606 fn enter_module(&mut self, name: &str, container_type: &str) {}
607
608 /// Called when exiting a submodule.
609 ///
610 /// # Parameters
611 /// - `name`: The name of the submodule being exited
612 /// - `container_type`: The type of the container with format:
613 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
614 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
615 /// - For Vec containers: "Vec" (name is the index)
616 /// - For Tuple containers: "Tuple" (name is the index)
617 /// - For Array containers: "Array" (name is the index)
618 ///
619 /// Note: Option containers do not call enter_module/exit_module to preserve
620 /// the field name in the path (e.g., "bias" instead of "bias.Some")
621 #[allow(unused_variables)]
622 fn exit_module(&mut self, name: &str, container_type: &str) {}
623
624 /// Visit a float tensor with its full module path.
625 ///
626 /// # Parameters
627 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
628 /// Each element represents a module name in the hierarchy, with the final element
629 /// being the parameter name. This allows efficient reuse of the path stack.
630 /// - `id`: The unique identifier of the parameter
631 /// - `tensor`: The float tensor to visit
632 #[allow(unused_variables)]
633 fn visit_float_with_path<const D: usize>(
634 &mut self,
635 path: &[String],
636 id: ParamId,
637 tensor: &Tensor<D>,
638 ) {
639 }
640
641 /// Visit an int tensor with its full module path.
642 ///
643 /// # Parameters
644 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
645 /// Each element represents a module name in the hierarchy, with the final element
646 /// being the parameter name. This allows efficient reuse of the path stack.
647 /// - `id`: The unique identifier of the parameter
648 /// - `tensor`: The integer tensor to visit
649 #[allow(unused_variables)]
650 fn visit_int_with_path<const D: usize>(
651 &mut self,
652 path: &[String],
653 id: ParamId,
654 tensor: &Tensor<D, Int>,
655 ) {
656 }
657
658 /// Visit a bool tensor with its full module path.
659 ///
660 /// # Parameters
661 /// - `path`: The path components to the tensor as a slice (e.g., &["encoder", "layer1", "weight"]).
662 /// Each element represents a module name in the hierarchy, with the final element
663 /// being the parameter name. This allows efficient reuse of the path stack.
664 /// - `id`: The unique identifier of the parameter
665 /// - `tensor`: The boolean tensor to visit
666 #[allow(unused_variables)]
667 fn visit_bool_with_path<const D: usize>(
668 &mut self,
669 path: &[String],
670 id: ParamId,
671 tensor: &Tensor<D, Bool>,
672 ) {
673 }
674}
675
676/// Module mapper trait for transforming module parameters.
677pub trait ModuleMapper {
678 /// Called when entering a submodule.
679 ///
680 /// # Parameters
681 /// - `name`: The name of the submodule being entered
682 /// - `container_type`: The type of the container with format:
683 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
684 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
685 /// - For Vec containers: "Vec" (name is the index)
686 /// - For Tuple containers: "Tuple" (name is the index)
687 /// - For Array containers: "Array" (name is the index)
688 ///
689 /// Note: Option containers do not call enter_module/exit_module to preserve
690 /// the field name in the path (e.g., "bias" instead of "bias.Some")
691 #[allow(unused_variables)]
692 fn enter_module(&mut self, name: &str, container_type: &str) {}
693
694 /// Called when exiting a submodule.
695 ///
696 /// # Parameters
697 /// - `name`: The name of the submodule being exited
698 /// - `container_type`: The type of the container with format:
699 /// - For user-defined structs: "Struct:TypeName" (e.g., "Struct:Linear")
700 /// - For user-defined enums: "Enum:TypeName" (e.g., "Enum:MyEnum")
701 /// - For Vec containers: "Vec" (name is the index)
702 /// - For Tuple containers: "Tuple" (name is the index)
703 /// - For Array containers: "Array" (name is the index)
704 ///
705 /// Note: Option containers do not call enter_module/exit_module to preserve
706 /// the field name in the path (e.g., "bias" instead of "bias.Some")
707 #[allow(unused_variables)]
708 fn exit_module(&mut self, name: &str, container_type: &str) {}
709
710 /// Map a float parameter in the module.
711 ///
712 /// # Parameters
713 /// - `param`: The float parameter to transform
714 ///
715 /// # Returns
716 /// The transformed parameter
717 #[allow(unused_variables)]
718 fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
719 param
720 }
721
722 /// Map an int parameter in the module.
723 ///
724 /// # Parameters
725 /// - `param`: The integer parameter to transform
726 ///
727 /// # Returns
728 /// The transformed parameter
729 #[allow(unused_variables)]
730 fn map_int<const D: usize>(&mut self, param: Param<Tensor<D, Int>>) -> Param<Tensor<D, Int>> {
731 param
732 }
733
734 /// Map a bool parameter in the module.
735 ///
736 /// # Parameters
737 /// - `param`: The boolean parameter to transform
738 ///
739 /// # Returns
740 /// The transformed parameter
741 #[allow(unused_variables)]
742 fn map_bool<const D: usize>(
743 &mut self,
744 param: Param<Tensor<D, Bool>>,
745 ) -> Param<Tensor<D, Bool>> {
746 param
747 }
748
749 /// Map a [`Param<Flag>`] in the module.
750 ///
751 /// The default is the identity, so a mapper with no opinion about control state (for example,
752 /// a record, a quantizer or a module sharder) leaves it untouched. Device moves are handled
753 /// directly by [`Module::to_device`] and do not go through a [`ModuleMapper`].
754 ///
755 /// # Parameters
756 /// - `flag`: The flag to map
757 ///
758 /// # Returns
759 /// The mapped flag
760 fn map_flag(&mut self, flag: Param<Flag>) -> Param<Flag> {
761 flag
762 }
763}
764
765#[cfg(all(test, feature = "autodiff"))]
766mod tests {
767 use super::*;
768
769 use crate::module::ParamGroup;
770 use crate::{test_device, test_utils::SimpleLinear};
771
772 fn stateful_module(device: &Device) -> (SimpleLinear, Param<Flag>) {
773 (
774 SimpleLinear::new(4, 4, device),
775 Param::<Flag>::from_bool(true),
776 )
777 }
778
779 #[test]
780 fn test_module_val_train_stateful() {
781 let device = test_device().autodiff();
782 let module = SimpleLinear::new(4, 4, &device);
783
784 assert!(module.weight.is_require_grad());
785 assert!(module.weight.is_active);
786
787 let module = module.valid();
788 assert!(!module.weight.is_require_grad());
789 assert!(module.weight.is_active); // stateful
790
791 let module = module.train();
792 assert!(module.weight.is_require_grad());
793 assert!(module.weight.is_active); // stateful
794
795 let module = module.no_grad();
796 assert!(!module.weight.is_require_grad());
797 assert!(!module.weight.is_active); // stateful
798
799 let module = module.valid();
800 assert!(!module.weight.is_require_grad()); // always
801 assert!(!module.weight.is_active); // stateful
802
803 let module = module.train();
804 assert!(!module.weight.is_require_grad());
805 assert!(!module.weight.is_active); // stateful
806 }
807
808 /// `valid` on a module already on the inner backend returns it unchanged rather than
809 /// panicking.
810 #[test]
811 fn valid_is_idempotent() {
812 let device = test_device().autodiff();
813 let module = SimpleLinear::new(4, 4, &device).valid();
814
815 let module = module.valid();
816
817 assert!(!module.weight.is_require_grad());
818 assert!(!module.weight.val().device().is_autodiff());
819 }
820
821 #[test]
822 fn valid_on_a_plain_device_keeps_tensors_plain_and_disables_flags() {
823 let module = stateful_module(&test_device());
824
825 let module = module.valid();
826
827 assert!(!module.0.weight.val().device().is_autodiff());
828 assert!(!module.1.is_enabled());
829 }
830
831 #[test]
832 fn unfreeze_during_validation_is_fully_applied_by_train() {
833 let device = test_device().autodiff();
834 let module = stateful_module(&device).valid().unfreeze();
835
836 // The plain validation tensor can't require gradients yet, but the setting is preserved.
837 assert!(!module.0.weight.is_require_grad());
838 assert!(module.0.weight.is_active);
839 assert!(module.1.is_enabled());
840
841 let module = module.train();
842
843 assert!(module.0.weight.is_require_grad());
844 assert!(module.0.bias.as_ref().unwrap().is_require_grad());
845 assert!(module.1.is_enabled());
846 }
847
848 #[test]
849 fn set_require_grad_during_validation_is_fully_applied_by_train() {
850 let device = test_device().autodiff();
851 let module = stateful_module(&device)
852 .freeze()
853 .valid()
854 .set_require_grad(true);
855
856 assert!(!module.0.weight.is_require_grad());
857 assert!(module.0.weight.is_active);
858 assert!(!module.1.is_enabled());
859
860 let module = module.train();
861
862 assert!(module.0.weight.is_require_grad());
863 assert!(module.0.bias.as_ref().unwrap().is_require_grad());
864 assert!(!module.1.is_enabled());
865 }
866
867 #[test]
868 fn freeze_during_validation_survives_train() {
869 let device = test_device().autodiff();
870 let module = stateful_module(&device).valid().freeze().train();
871
872 assert!(!module.0.weight.is_require_grad());
873 assert!(!module.0.bias.as_ref().unwrap().is_require_grad());
874 assert!(!module.1.is_enabled());
875 }
876
877 #[test]
878 fn group_activation_settings_during_validation_are_applied_by_train() {
879 let device = test_device().autodiff();
880 let module = stateful_module(&device).freeze().valid();
881 let group = ParamGroup::from_ids(vec![module.0.weight.id, module.1.id]);
882
883 let module = module.unfreeze_group(group).train();
884
885 assert!(module.0.weight.is_require_grad());
886 assert!(!module.0.bias.as_ref().unwrap().is_require_grad());
887 assert!(module.1.is_enabled());
888 }
889
890 #[test]
891 fn group_gradient_settings_during_validation_ignore_flags_and_restore_tensors() {
892 let device = test_device().autodiff();
893 let module = stateful_module(&device).freeze().valid();
894 let group = ParamGroup::from_ids(vec![module.0.weight.id, module.1.id]);
895
896 let module = module.set_require_grad_group(group, true).train();
897
898 assert!(module.0.weight.is_require_grad());
899 assert!(!module.0.bias.as_ref().unwrap().is_require_grad());
900 assert!(!module.1.is_enabled());
901 }
902
903 #[test]
904 fn validation_state_survives_fork_until_train() {
905 let device = test_device().autodiff();
906 let module = stateful_module(&device).valid().fork(&device);
907
908 assert!(!module.0.weight.is_require_grad());
909 assert!(module.0.weight.is_active);
910 assert!(!module.1.is_enabled());
911
912 let module = module.train();
913
914 assert!(module.0.weight.is_require_grad());
915 assert!(module.0.bias.as_ref().unwrap().is_require_grad());
916 assert!(module.1.is_enabled());
917 }
918
919 #[test]
920 fn trained_validation_module_remains_trainable_after_fork() {
921 let device = test_device().autodiff();
922 let module = stateful_module(&device).valid().train().fork(&device);
923
924 assert!(module.0.weight.is_require_grad());
925 assert!(module.0.bias.as_ref().unwrap().is_require_grad());
926 assert!(module.1.is_enabled());
927 }
928
929 #[test]
930 fn validation_state_survives_device_move_until_train() {
931 let device = test_device().autodiff();
932 let target = test_device();
933 let module = stateful_module(&device).valid().to_device(&target);
934
935 assert!(!module.0.weight.is_require_grad());
936 assert!(module.0.weight.is_active);
937 assert!(!module.1.is_enabled());
938
939 let module = module.train();
940
941 assert!(module.0.weight.is_require_grad());
942 assert!(module.0.bias.as_ref().unwrap().is_require_grad());
943 assert!(module.1.is_enabled());
944 }
945
946 #[test]
947 fn freeze_group_freezes_only_selected_params() {
948 let device = test_device().autodiff();
949 let module = SimpleLinear::new(4, 4, &device);
950
951 assert!(module.weight.is_require_grad());
952 assert!(module.bias.as_ref().unwrap().is_require_grad());
953
954 let module = module.freeze_group(ParamGroup::from_path("weight"));
955
956 assert!(!module.weight.is_require_grad());
957 assert!(!module.weight.is_active);
958
959 let bias = module.bias.as_ref().unwrap();
960 assert!(bias.is_require_grad());
961 assert!(bias.is_active);
962 }
963
964 #[test]
965 fn unfreeze_group_only_thaws_selected_params() {
966 let device = test_device().autodiff();
967 let module = SimpleLinear::new(4, 4, &device);
968
969 let module = module.no_grad();
970 assert!(!module.weight.is_require_grad());
971 assert!(!module.bias.as_ref().unwrap().is_require_grad());
972
973 let module = module.unfreeze_group(ParamGroup::from_path("weight"));
974
975 assert!(module.weight.is_require_grad());
976 assert!(module.weight.is_active);
977 assert!(!module.bias.as_ref().unwrap().is_require_grad());
978 assert!(!module.bias.as_ref().unwrap().is_active);
979 }
980}