1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4
5use burn_pack::Tensor as PackTensor;
6
7use burn_core::tensor::kind::Basic;
8use burn_core::tensor::{Bool, Int, Tensor};
9
10use crate::bridge;
11use crate::{ModuleAdapter, ModuleContext, PathFilter};
12use burn_core::module::{ModuleVisitor, Param, ParamId, Parameter};
13
14pub struct Collector {
50 pub tensors: Vec<PackTensor>,
52 path_stack: Vec<String>,
53 container_stack: Vec<String>,
54 filter: Option<PathFilter>,
55 adapter: Option<Box<dyn ModuleAdapter>>,
56 skip_enum_variants: bool,
59}
60
61impl Default for Collector {
62 fn default() -> Self {
63 Self::new(None, None, false)
64 }
65}
66
67impl Collector {
68 pub fn new(
98 filter: Option<PathFilter>,
99 adapter: Option<Box<dyn ModuleAdapter>>,
100 skip_enum_variants: bool,
101 ) -> Self {
102 Self {
103 tensors: Vec::new(),
104 path_stack: Vec::new(),
105 container_stack: Vec::new(),
106 filter,
107 adapter,
108 skip_enum_variants,
109 }
110 }
111
112 pub fn into_tensors(self) -> Vec<PackTensor> {
114 self.tensors
115 }
116
117 fn should_collect(&self, path: &[String], container_stack: &[String]) -> bool {
118 match &self.filter {
120 None => true,
121 Some(f) => f.matches_with_container_path(path, container_stack),
122 }
123 }
124
125 fn collect_param<const D: usize, K: Basic + 'static>(&mut self, param: &Param<Tensor<D, K>>)
130 where
131 Tensor<D, K>: Parameter,
132 {
133 if !self.should_collect(&self.path_stack, &self.container_stack) {
134 return;
135 }
136
137 let tensor = param.transform_for_save().val();
140 let tensor = bridge::from_tensor(&tensor, self.path_stack.join("."), Some(param.id.val()));
141
142 let tensor = adapt(self.adapter.as_deref(), tensor, &self.container_stack);
143 self.tensors.push(tensor);
144 }
145
146 fn collect_at<const D: usize, K: Basic + 'static>(
148 &mut self,
149 path: &[String],
150 id: ParamId,
151 tensor: &Tensor<D, K>,
152 ) {
153 if !self.should_collect(path, &self.container_stack) {
155 return;
156 }
157
158 let tensor = bridge::from_tensor(tensor, path.join("."), Some(id.val()));
159
160 let tensor = adapt(self.adapter.as_deref(), tensor, &self.container_stack);
161 self.tensors.push(tensor);
162 }
163}
164
165fn adapt(
174 adapter: Option<&dyn ModuleAdapter>,
175 tensor: PackTensor,
176 containers: &[String],
177) -> PackTensor {
178 match adapter {
179 Some(adapter) => adapter.adapt(tensor, ModuleContext::new(containers)),
180 None => tensor,
181 }
182}
183
184impl ModuleVisitor for Collector {
185 fn enter_module(&mut self, name: &str, container_type: &str) {
186 self.container_stack.push(container_type.to_string());
188
189 if !self.skip_enum_variants || !container_type.starts_with("Enum:") {
192 self.path_stack.push(name.to_string());
193 }
194 }
195
196 fn exit_module(&mut self, _name: &str, container_type: &str) {
197 self.container_stack.pop();
198
199 if !self.skip_enum_variants || !container_type.starts_with("Enum:") {
201 self.path_stack.pop();
202 }
203 }
204
205 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
206 self.collect_param(param);
207 }
208
209 fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {
210 self.collect_param(param);
211 }
212
213 fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {
214 self.collect_param(param);
215 }
216
217 fn visit_float_with_path<const D: usize>(
218 &mut self,
219 path: &[String],
220 id: ParamId,
221 tensor: &Tensor<D>,
222 ) {
223 self.collect_at(path, id, tensor);
224 }
225
226 fn visit_int_with_path<const D: usize>(
227 &mut self,
228 path: &[String],
229 id: ParamId,
230 tensor: &Tensor<D, Int>,
231 ) {
232 self.collect_at(path, id, tensor);
233 }
234
235 fn visit_bool_with_path<const D: usize>(
236 &mut self,
237 path: &[String],
238 id: ParamId,
239 tensor: &Tensor<D, Bool>,
240 ) {
241 self.collect_at(path, id, tensor);
242 }
243}
244
245#[cfg(all(test, feature = "std"))]
246mod tests {
247 use super::*;
248
249 use burn_core as burn;
250
251 use alloc::collections::BTreeMap;
252 use alloc::string::String;
253 use burn_core::module::{Module, Param};
254 use burn_core::tensor::{Device, shape};
255 use burn_nn::LinearConfig;
256
257 #[test]
258 fn collector_collects_a_tensor() {
259 let device = Default::default();
260 let tensor = Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device);
261
262 let mut collector = Collector::new(None, None, false);
263 let id = ParamId::new();
264
265 collector.visit_float_with_path(&["model".to_string(), "weight".to_string()], id, &tensor);
267
268 assert_eq!(collector.tensors.len(), 1);
269 assert_eq!(collector.tensors[0].name, "model.weight");
270
271 let view = &collector.tensors[0];
273 let data = bridge::to_data(view).unwrap();
274 assert_eq!(*data.shape(), shape![2, 2]);
275 }
276
277 #[test]
278 fn root_level_parameters() {
279 use burn_core::module::ModuleVisitor;
280
281 let device = Default::default();
282
283 let weight = Param::<Tensor<2>>::from_data([[1.0, 2.0], [3.0, 4.0]], &device);
285 let bias = Param::<Tensor<1>>::from_data([5.0, 6.0], &device);
286
287 let mut collector = Collector::new(None, None, false);
288
289 ModuleVisitor::enter_module(&mut collector, "weight", "");
292 ModuleVisitor::visit_float(&mut collector, &weight);
293 ModuleVisitor::exit_module(&mut collector, "weight", "");
294
295 ModuleVisitor::enter_module(&mut collector, "bias", "");
297 ModuleVisitor::visit_float(&mut collector, &bias);
298 ModuleVisitor::exit_module(&mut collector, "bias", "");
299
300 assert_eq!(collector.tensors.len(), 2);
302
303 assert_eq!(collector.tensors[0].name, "weight");
305 assert_eq!(collector.tensors[1].name, "bias");
306
307 let weight_data = bridge::to_data(&collector.tensors[0])
309 .unwrap()
310 .try_into_vec::<f32>()
311 .unwrap();
312 let bias_data = bridge::to_data(&collector.tensors[1])
313 .unwrap()
314 .try_into_vec::<f32>()
315 .unwrap();
316
317 assert_eq!(weight_data, vec![1.0, 2.0, 3.0, 4.0]);
318 assert_eq!(bias_data, vec![5.0, 6.0]);
319 }
320
321 #[test]
322 #[cfg(target_has_atomic = "ptr")]
323 fn collector_with_filter() {
324 let device = Default::default();
325 let tensor = Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device);
326
327 let filter = PathFilter::new().with_regex(r"^encoder\..*");
328 let mut collector = Collector::new(Some(filter), None, false);
329 let id = ParamId::new();
330
331 collector.visit_float_with_path(
333 &["encoder".to_string(), "weight".to_string()],
334 id,
335 &tensor,
336 );
337 collector.visit_float_with_path(
339 &["decoder".to_string(), "weight".to_string()],
340 id,
341 &tensor,
342 );
343
344 assert_eq!(collector.tensors.len(), 1);
345 assert_eq!(collector.tensors[0].name, "encoder.weight");
346 }
347
348 #[test]
349 #[cfg(target_has_atomic = "ptr")]
350 fn collector_with_multiple_filters() {
351 let device = Default::default();
352 let tensor = Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device);
353
354 let filter = PathFilter::new()
356 .with_regex(r"^encoder\..*") .with_regex(r".*\.bias$"); let mut collector = Collector::new(Some(filter), None, false);
359 let id = ParamId::new();
360
361 collector.visit_float_with_path(
363 &["encoder".to_string(), "weight".to_string()],
364 id,
365 &tensor,
366 ); collector.visit_float_with_path(&["decoder".to_string(), "bias".to_string()], id, &tensor); collector.visit_float_with_path(&["encoder".to_string(), "bias".to_string()], id, &tensor); collector.visit_float_with_path(
372 &["decoder".to_string(), "weight".to_string()],
373 id,
374 &tensor,
375 ); assert_eq!(collector.tensors.len(), 3);
378 let paths: Vec<String> = collector.tensors.iter().map(|v| v.name.clone()).collect();
379 assert!(paths.contains(&"encoder.weight".to_string()));
380 assert!(paths.contains(&"decoder.bias".to_string()));
381 assert!(paths.contains(&"encoder.bias".to_string()));
382 assert!(!paths.contains(&"decoder.weight".to_string()));
383 }
384
385 #[test]
386 fn collector_with_predicate() {
387 let device = Default::default();
388 let tensor = Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device);
389
390 fn filter_fn(path: &str, _container_path: &str) -> bool {
392 path.starts_with("encoder.") || path == "decoder.bias"
393 }
394 let filter = PathFilter::new().with_predicate(filter_fn);
395 let mut collector = Collector::new(Some(filter), None, false);
396 let id = ParamId::new();
397
398 collector.visit_float_with_path(
400 &["encoder".to_string(), "weight".to_string()],
401 id,
402 &tensor,
403 );
404 collector.visit_float_with_path(&["encoder".to_string(), "bias".to_string()], id, &tensor);
405 collector.visit_float_with_path(&["decoder".to_string(), "bias".to_string()], id, &tensor);
406
407 collector.visit_float_with_path(
409 &["decoder".to_string(), "weight".to_string()],
410 id,
411 &tensor,
412 );
413 collector.visit_float_with_path(&["other".to_string(), "tensor".to_string()], id, &tensor);
414
415 assert_eq!(collector.tensors.len(), 3);
416 let paths: Vec<String> = collector.tensors.iter().map(|v| v.name.clone()).collect();
417 assert!(paths.contains(&"encoder.weight".to_string()));
418 assert!(paths.contains(&"encoder.bias".to_string()));
419 assert!(paths.contains(&"decoder.bias".to_string()));
420 assert!(!paths.contains(&"decoder.weight".to_string()));
421 assert!(!paths.contains(&"other.tensor".to_string()));
422 }
423
424 #[test]
425 fn collector_predicate_with_complex_logic() {
426 let device = Default::default();
427 let tensor = Tensor::<2>::from_data([[1.0, 2.0], [3.0, 4.0]], &device);
428
429 fn complex_filter(path: &str, _container_path: &str) -> bool {
431 let parts: Vec<&str> = path.split('.').collect();
432 if parts.len() != 3 {
433 return false;
434 }
435 (parts[1] == "layer1" || parts[1] == "layer2") && parts[2] == "weight"
437 }
438 let filter = PathFilter::new().with_predicate(complex_filter);
439 let mut collector = Collector::new(Some(filter), None, false);
440 let id = ParamId::new();
441
442 collector.visit_float_with_path(
444 &[
445 "model".to_string(),
446 "layer1".to_string(),
447 "weight".to_string(),
448 ],
449 id,
450 &tensor,
451 );
452 collector.visit_float_with_path(
453 &[
454 "model".to_string(),
455 "layer2".to_string(),
456 "weight".to_string(),
457 ],
458 id,
459 &tensor,
460 );
461
462 collector.visit_float_with_path(
464 &[
465 "model".to_string(),
466 "layer1".to_string(),
467 "bias".to_string(),
468 ],
469 id,
470 &tensor,
471 );
472 collector.visit_float_with_path(
473 &[
474 "model".to_string(),
475 "layer3".to_string(),
476 "weight".to_string(),
477 ],
478 id,
479 &tensor,
480 );
481 collector.visit_float_with_path(
482 &["encoder".to_string(), "weight".to_string()],
483 id,
484 &tensor,
485 ); assert_eq!(collector.tensors.len(), 2);
488 let paths: Vec<String> = collector.tensors.iter().map(|v| v.name.clone()).collect();
489 assert!(paths.contains(&"model.layer1.weight".to_string()));
490 assert!(paths.contains(&"model.layer2.weight".to_string()));
491 assert!(!paths.contains(&"model.layer1.bias".to_string()));
492 assert!(!paths.contains(&"model.layer3.weight".to_string()));
493 assert!(!paths.contains(&"encoder.weight".to_string()));
494 }
495
496 struct TensorPathCollector {
498 pub paths: BTreeMap<String, (ParamId, Vec<usize>)>,
499 path_stack: Vec<String>,
500 }
501
502 impl TensorPathCollector {
503 fn new() -> Self {
504 Self {
505 paths: BTreeMap::new(),
506 path_stack: Vec::new(),
507 }
508 }
509
510 fn current_path(&self) -> String {
511 self.path_stack.join(".")
512 }
513 }
514
515 impl ModuleVisitor for TensorPathCollector {
516 fn enter_module(&mut self, name: &str, _container_type: &str) {
517 self.path_stack.push(name.to_string());
518 }
519
520 fn exit_module(&mut self, _name: &str, _container_type: &str) {
521 self.path_stack.pop();
522 }
523
524 fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
525 let path = self.current_path();
526 if !path.is_empty() {
527 self.paths.insert(
528 path,
529 (param.id, param.transform_for_save().val().shape().to_vec()),
530 );
531 }
532 }
533
534 fn visit_int<const D: usize>(&mut self, param: &Param<Tensor<D, Int>>) {
535 let path = self.current_path();
536 if !path.is_empty() {
537 self.paths.insert(
538 path,
539 (param.id, param.transform_for_save().val().shape().to_vec()),
540 );
541 }
542 }
543
544 fn visit_bool<const D: usize>(&mut self, param: &Param<Tensor<D, Bool>>) {
545 let path = self.current_path();
546 if !path.is_empty() {
547 self.paths.insert(
548 path,
549 (param.id, param.transform_for_save().val().shape().to_vec()),
550 );
551 }
552 }
553 }
554
555 #[derive(Module, Debug)]
557 struct InnerModule {
558 weight: Param<Tensor<2>>,
559 bias: Param<Tensor<1>>,
560 }
561
562 #[derive(Module, Debug)]
563 struct OuterModule {
564 layer1: InnerModule,
565 layer2: InnerModule,
566 }
567
568 impl InnerModule {
569 fn new(device: &Device) -> Self {
570 Self {
571 weight: Param::from_data([[1.0, 2.0], [3.0, 4.0]], device),
572 bias: Param::from_data([5.0, 6.0], device),
573 }
574 }
575 }
576
577 impl OuterModule {
578 fn new(device: &Device) -> Self {
579 Self {
580 layer1: InnerModule::new(device),
581 layer2: InnerModule::new(device),
582 }
583 }
584 }
585
586 #[test]
587 fn nested_module_path_tracking() {
588 let device = Default::default();
589 let module = OuterModule::new(&device);
590
591 let mut collector = TensorPathCollector::new();
592 module.visit(&mut collector);
593
594 let paths = collector.paths;
595
596 assert!(paths.contains_key("layer1.weight"), "Missing layer1.weight");
599 assert!(paths.contains_key("layer1.bias"), "Missing layer1.bias");
600 assert!(paths.contains_key("layer2.weight"), "Missing layer2.weight");
601 assert!(paths.contains_key("layer2.bias"), "Missing layer2.bias");
602
603 assert_eq!(paths.get("layer1.weight").unwrap().1, vec![2, 2]);
605 assert_eq!(paths.get("layer1.bias").unwrap().1, vec![2]);
606 assert_eq!(paths.get("layer2.weight").unwrap().1, vec![2, 2]);
607 assert_eq!(paths.get("layer2.bias").unwrap().1, vec![2]);
608 }
609
610 #[test]
611 fn linear_module_paths() {
612 let device = Default::default();
613 let config = LinearConfig::new(10, 20).with_bias(true);
614 let linear = config.init(&device);
615
616 let mut collector = TensorPathCollector::new();
617 linear.visit(&mut collector);
618
619 let paths = collector.paths;
620
621 assert!(paths.contains_key("weight"));
623 assert!(paths.contains_key("bias"));
624
625 assert_eq!(paths.get("weight").unwrap().1, vec![10, 20]);
627 assert_eq!(paths.get("bias").unwrap().1, vec![20]);
628 }
629
630 #[derive(Module, Debug)]
632 struct Level4Module {
633 weight: Param<Tensor<2>>,
634 bias: Param<Tensor<1>>,
635 }
636
637 #[derive(Module, Debug)]
638 struct Level3Module {
639 layer: Level4Module,
640 extra: Level4Module,
641 }
642
643 #[derive(Module, Debug)]
644 struct Level2Module {
645 block1: Level3Module,
646 block2: Level3Module,
647 }
648
649 #[derive(Module, Debug)]
650 struct Level1Module {
651 encoder: Level2Module,
652 decoder: Level2Module,
653 }
654
655 #[derive(Module, Debug)]
656 struct DeepModel {
657 backbone: Level1Module,
658 head: Level4Module,
659 }
660
661 impl Level4Module {
662 fn new(device: &Device) -> Self {
663 Self {
664 weight: Param::from_data([[1.0, 2.0], [3.0, 4.0]], device),
665 bias: Param::from_data([5.0, 6.0], device),
666 }
667 }
668 }
669
670 impl Level3Module {
671 fn new(device: &Device) -> Self {
672 Self {
673 layer: Level4Module::new(device),
674 extra: Level4Module::new(device),
675 }
676 }
677 }
678
679 impl Level2Module {
680 fn new(device: &Device) -> Self {
681 Self {
682 block1: Level3Module::new(device),
683 block2: Level3Module::new(device),
684 }
685 }
686 }
687
688 impl Level1Module {
689 fn new(device: &Device) -> Self {
690 Self {
691 encoder: Level2Module::new(device),
692 decoder: Level2Module::new(device),
693 }
694 }
695 }
696
697 impl DeepModel {
698 fn new(device: &Device) -> Self {
699 Self {
700 backbone: Level1Module::new(device),
701 head: Level4Module::new(device),
702 }
703 }
704 }
705
706 #[test]
707 fn deep_module_path_tracking() {
708 let device = Default::default();
709 let model = DeepModel::new(&device);
710
711 let mut collector = Collector::new(None, None, false);
712 model.visit(&mut collector);
713
714 let views = collector.tensors;
715 let paths: Vec<String> = views.iter().map(|v| v.name.clone()).collect();
716
717 assert!(paths.contains(&"backbone.encoder.block1.layer.weight".to_string()));
719 assert!(paths.contains(&"backbone.encoder.block1.layer.bias".to_string()));
720 assert!(paths.contains(&"backbone.encoder.block1.extra.weight".to_string()));
721 assert!(paths.contains(&"backbone.encoder.block1.extra.bias".to_string()));
722
723 assert!(paths.contains(&"backbone.encoder.block2.layer.weight".to_string()));
724 assert!(paths.contains(&"backbone.encoder.block2.layer.bias".to_string()));
725 assert!(paths.contains(&"backbone.encoder.block2.extra.weight".to_string()));
726 assert!(paths.contains(&"backbone.encoder.block2.extra.bias".to_string()));
727
728 assert!(paths.contains(&"backbone.decoder.block1.layer.weight".to_string()));
729 assert!(paths.contains(&"backbone.decoder.block1.layer.bias".to_string()));
730 assert!(paths.contains(&"backbone.decoder.block1.extra.weight".to_string()));
731 assert!(paths.contains(&"backbone.decoder.block1.extra.bias".to_string()));
732
733 assert!(paths.contains(&"backbone.decoder.block2.layer.weight".to_string()));
734 assert!(paths.contains(&"backbone.decoder.block2.layer.bias".to_string()));
735 assert!(paths.contains(&"backbone.decoder.block2.extra.weight".to_string()));
736 assert!(paths.contains(&"backbone.decoder.block2.extra.bias".to_string()));
737
738 assert!(paths.contains(&"head.weight".to_string()));
740 assert!(paths.contains(&"head.bias".to_string()));
741
742 assert_eq!(views.len(), 18);
744
745 let view = views
747 .iter()
748 .find(|v| v.name == "backbone.encoder.block1.layer.weight")
749 .unwrap();
750 let data = bridge::to_data(view).unwrap();
751 assert_eq!(*data.shape(), shape![2, 2]);
752 }
753
754 #[test]
755 fn deep_module_filtered_export() {
756 let device = Default::default();
757 let model = DeepModel::new(&device);
758
759 #[cfg(target_has_atomic = "ptr")]
761 {
762 let filter = PathFilter::new().with_regex(r"^backbone\.encoder\..*");
763 let mut collector = Collector::new(Some(filter), None, false);
764 model.visit(&mut collector);
765 assert_eq!(collector.tensors.len(), 8); }
767
768 #[cfg(target_has_atomic = "ptr")]
770 {
771 let filter = PathFilter::new().with_regex(r".*\.block1\..*");
772 let mut collector = Collector::new(Some(filter), None, false);
773 model.visit(&mut collector);
774 assert_eq!(collector.tensors.len(), 8); }
776
777 #[cfg(target_has_atomic = "ptr")]
779 {
780 let filter = PathFilter::new().with_regex(r".*\.weight$");
781 let mut collector = Collector::new(Some(filter), None, false);
782 model.visit(&mut collector);
783 assert_eq!(collector.tensors.len(), 9); }
785
786 #[cfg(target_has_atomic = "ptr")]
788 {
789 let filter = PathFilter::new()
790 .with_regex(r"^backbone\.encoder\.block1\..*") .with_regex(r"^backbone\.decoder\..*\.bias$") .with_regex(r"^head\.weight$"); let mut collector = Collector::new(Some(filter), None, false);
794 model.visit(&mut collector);
795
796 assert_eq!(collector.tensors.len(), 9);
801
802 let paths: Vec<String> = collector.tensors.iter().map(|v| v.name.clone()).collect();
803 assert!(paths.contains(&"backbone.encoder.block1.layer.weight".to_string()));
804 assert!(paths.contains(&"backbone.decoder.block1.layer.bias".to_string()));
805 assert!(paths.contains(&"head.weight".to_string()));
806 assert!(!paths.contains(&"head.bias".to_string())); }
808 }
809
810 use crate::traits::ModuleSnapshot;
811 use burn_nn::{BatchNorm, BatchNormConfig, Linear};
812 use hashbrown::HashMap;
813 use std::sync::{Arc, Mutex};
814
815 #[derive(Module, Debug)]
817 struct OptionalFieldModule {
818 required: Param<Tensor<2>>,
819 optional: Option<Param<Tensor<1>>>,
820 }
821
822 impl OptionalFieldModule {
823 fn new_with_optional(device: &Device) -> Self {
824 Self {
825 required: Param::from_data([[1.0, 2.0], [3.0, 4.0]], device),
826 optional: Some(Param::from_data([5.0, 6.0], device)),
827 }
828 }
829
830 fn new_without_optional(device: &Device) -> Self {
831 Self {
832 required: Param::from_data([[1.0, 2.0], [3.0, 4.0]], device),
833 optional: None,
834 }
835 }
836 }
837
838 #[test]
839 fn optional_field_module_with_value() {
840 let device = Default::default();
841 let module = OptionalFieldModule::new_with_optional(&device);
842
843 let views: HashMap<String, PackTensor> = module
844 .collect(None, None, false)
845 .into_iter()
846 .map(|v| (v.name.clone(), v))
847 .collect();
848
849 assert_eq!(views.len(), 2);
850 assert!(views.contains_key("required"));
851 assert!(views.contains_key("optional"));
852 }
853
854 #[test]
855 fn optional_field_module_without_value() {
856 let device = Default::default();
857 let module = OptionalFieldModule::new_without_optional(&device);
858
859 let views: HashMap<String, PackTensor> = module
860 .collect(None, None, false)
861 .into_iter()
862 .map(|v| (v.name.clone(), v))
863 .collect();
864
865 assert_eq!(views.len(), 1);
866 assert!(views.contains_key("required"));
867 assert!(!views.contains_key("optional"));
868 }
869
870 #[derive(Module, Debug)]
872 struct VecModule {
873 layers: Vec<Linear>,
874 }
875
876 impl VecModule {
877 fn new(device: &Device, num_layers: usize) -> Self {
878 Self {
879 layers: (0..num_layers)
880 .map(|_| LinearConfig::new(10, 10).init(device))
881 .collect(),
882 }
883 }
884 }
885
886 #[derive(Module, Debug)]
888 struct TupleModule {
889 layers: (Linear, Linear, Linear),
890 }
891
892 impl TupleModule {
893 fn new(device: &Device) -> Self {
894 Self {
895 layers: (
896 LinearConfig::new(10, 10).init(device),
897 LinearConfig::new(10, 10).init(device),
898 LinearConfig::new(10, 10).init(device),
899 ),
900 }
901 }
902 }
903
904 #[test]
905 fn vec_module_collect() {
906 let device = Default::default();
907 let module = VecModule::new(&device, 3);
908
909 let views: HashMap<String, PackTensor> = module
910 .collect(None, None, false)
911 .into_iter()
912 .map(|v| (v.name.clone(), v))
913 .collect();
914
915 assert_eq!(views.len(), 6); assert!(views.contains_key("layers.0.weight"));
920 assert!(views.contains_key("layers.0.bias"));
921 assert!(views.contains_key("layers.1.weight"));
922 assert!(views.contains_key("layers.1.bias"));
923 assert!(views.contains_key("layers.2.weight"));
924 assert!(views.contains_key("layers.2.bias"));
925 }
926
927 #[test]
928 fn tuple_module_collect() {
929 let device = Default::default();
930 let module = TupleModule::new(&device);
931
932 let tensors = module.collect(None, None, false);
933 assert_eq!(tensors.len(), 6);
934
935 let views: HashMap<String, PackTensor> =
936 tensors.into_iter().map(|v| (v.name.clone(), v)).collect();
937
938 assert_eq!(views.len(), 6);
939
940 assert!(views.contains_key("layers.0.weight"));
941 assert!(views.contains_key("layers.0.bias"));
942 assert!(views.contains_key("layers.1.weight"));
943 assert!(views.contains_key("layers.1.bias"));
944 assert!(views.contains_key("layers.2.weight"));
945 assert!(views.contains_key("layers.2.bias"));
946 }
947
948 #[derive(Module, Debug)]
950 struct ArrayModule {
951 layers: [Linear; 3],
952 }
953
954 impl ArrayModule {
955 fn new(device: &Device) -> Self {
956 Self {
957 layers: [
958 LinearConfig::new(10, 10).init(device),
959 LinearConfig::new(10, 10).init(device),
960 LinearConfig::new(10, 10).init(device),
961 ],
962 }
963 }
964 }
965
966 #[test]
967 fn array_module_collect() {
968 let device = Default::default();
969 let module = ArrayModule::new(&device);
970
971 let views: HashMap<String, PackTensor> = module
972 .collect(None, None, false)
973 .into_iter()
974 .map(|v| (v.name.clone(), v))
975 .collect();
976
977 assert_eq!(views.len(), 6); for i in 0..3 {
982 assert!(views.contains_key(&format!("layers.{}.weight", i)));
983 assert!(views.contains_key(&format!("layers.{}.bias", i)));
984 }
985 }
986
987 #[derive(Module, Debug)]
989 enum EnumModule {
990 LayerA(Linear),
991 LayerB(Linear),
992 LayerC(Linear),
993 }
994
995 #[test]
996 fn enum_module_collect() {
997 let device = Default::default();
998
999 let module_a = EnumModule::LayerA(LinearConfig::new(10, 20).init(&device));
1001 let views_a: HashMap<String, PackTensor> = module_a
1002 .collect(None, None, false)
1003 .into_iter()
1004 .map(|v| (v.name.clone(), v))
1005 .collect();
1006
1007 assert_eq!(views_a.len(), 2);
1009 assert!(views_a.contains_key("LayerA.weight"));
1010 assert!(views_a.contains_key("LayerA.bias"));
1011
1012 let module_b = EnumModule::LayerB(LinearConfig::new(10, 20).init(&device));
1014 let views_b: HashMap<String, PackTensor> = module_b
1015 .collect(None, None, false)
1016 .into_iter()
1017 .map(|v| (v.name.clone(), v))
1018 .collect();
1019
1020 assert_eq!(views_b.len(), 2);
1021 assert!(views_b.contains_key("LayerB.weight"));
1022 assert!(views_b.contains_key("LayerB.bias"));
1023 }
1024 type Sighting = (String, Option<String>);
1026
1027 #[derive(Clone, Default)]
1034 struct RecordingAdapter {
1035 seen: Arc<Mutex<Vec<Sighting>>>,
1036 }
1037
1038 impl RecordingAdapter {
1039 fn seen(&self) -> Vec<Sighting> {
1041 let mut seen = self.seen.lock().unwrap().clone();
1042 seen.sort();
1043 seen
1044 }
1045 }
1046
1047 impl ModuleAdapter for RecordingAdapter {
1048 fn adapt(&self, tensor: PackTensor, ctx: ModuleContext<'_>) -> PackTensor {
1049 self.seen.lock().unwrap().push((
1050 tensor.name.clone(),
1051 ctx.module_type().map(|t| t.to_string()),
1052 ));
1053 tensor
1054 }
1055
1056 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
1057 Box::new(self.clone())
1058 }
1059 }
1060
1061 fn module(module: &str) -> Option<String> {
1063 Some(module.to_string())
1064 }
1065
1066 #[test]
1068 fn linear_container_type() {
1069 let device = Default::default();
1070
1071 #[derive(Module, Debug)]
1072 struct ModelWithLinear {
1073 linear: Linear,
1074 norm: BatchNorm,
1075 }
1076
1077 impl ModelWithLinear {
1078 fn new(device: &Device) -> Self {
1079 Self {
1080 linear: LinearConfig::new(10, 20).init(device),
1081 norm: BatchNormConfig::new(20).init(device),
1082 }
1083 }
1084 }
1085
1086 let model = ModelWithLinear::new(&device);
1087 let adapter = RecordingAdapter::default();
1088 model.collect(None, Some(Box::new(adapter.clone())), false);
1089
1090 assert_eq!(
1091 adapter.seen(),
1092 vec![
1093 ("linear.bias".to_string(), module("Struct:Linear")),
1094 ("linear.weight".to_string(), module("Struct:Linear")),
1095 ("norm.beta".to_string(), module("Struct:BatchNorm")),
1096 ("norm.gamma".to_string(), module("Struct:BatchNorm")),
1097 ("norm.running_mean".to_string(), module("Struct:BatchNorm")),
1098 ("norm.running_var".to_string(), module("Struct:BatchNorm")),
1099 ]
1100 );
1101 }
1102
1103 #[test]
1104 fn complex_model_container_types() {
1105 let device = Default::default();
1106
1107 #[derive(Module, Debug)]
1108 struct ComplexModel {
1109 linear_layers: [Linear; 2],
1110 vec_layers: Vec<Linear>,
1111 single_linear: Linear,
1112 }
1113
1114 impl ComplexModel {
1115 fn new(device: &Device) -> Self {
1116 Self {
1117 linear_layers: [
1118 LinearConfig::new(100, 50).init(device),
1119 LinearConfig::new(50, 10).init(device),
1120 ],
1121 vec_layers: vec![
1122 LinearConfig::new(10, 10).init(device),
1123 LinearConfig::new(10, 10).init(device),
1124 ],
1125 single_linear: LinearConfig::new(10, 1).init(device),
1126 }
1127 }
1128 }
1129
1130 let model = ComplexModel::new(&device);
1131 let adapter = RecordingAdapter::default();
1132 model.collect(None, Some(Box::new(adapter.clone())), false);
1133
1134 let seen = adapter.seen();
1136 assert_eq!(seen.len(), 10);
1137
1138 for (name, module_type) in seen {
1142 assert_eq!(
1143 module_type,
1144 module("Struct:Linear"),
1145 "'{name}' should have been seen as a Linear parameter"
1146 );
1147 }
1148 }
1149
1150 #[test]
1151 fn collect_with_container_filter() {
1152 let device = Default::default();
1153
1154 #[derive(Module, Debug)]
1155 struct FilterTestModel {
1156 layers: Vec<Linear>,
1157 norm: BatchNorm,
1158 }
1159
1160 impl FilterTestModel {
1161 fn new(device: &Device) -> Self {
1162 Self {
1163 layers: vec![
1164 LinearConfig::new(10, 10).init(device),
1165 LinearConfig::new(10, 10).init(device),
1166 ],
1167 norm: BatchNormConfig::new(10).init(device),
1168 }
1169 }
1170 }
1171
1172 let model = FilterTestModel::new(&device);
1173
1174 let filter = PathFilter::new().with_predicate(|_path, container_path| {
1176 container_path.split('.').next_back() == Some("Struct:Linear")
1177 });
1178
1179 let linear_views: Vec<PackTensor> = model.collect(Some(filter), None, false);
1180
1181 let mut paths: Vec<String> = linear_views.iter().map(|v| v.name.clone()).collect();
1183 paths.sort();
1184 assert_eq!(
1185 paths,
1186 vec![
1187 "layers.0.bias",
1188 "layers.0.weight",
1189 "layers.1.bias",
1190 "layers.1.weight"
1191 ]
1192 );
1193 }
1194}