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