1use crate::bridge;
9
10use alloc::boxed::Box;
11use alloc::format;
12use alloc::string::String;
13use alloc::string::ToString;
14use alloc::vec;
15
16use burn_pack::Tensor as PackTensor;
17
18use burn_core::tensor::shape;
19use burn_core::tensor::{DType, TensorData};
20use hashbrown::HashSet;
21
22mod module_names {
26 pub const LINEAR: &str = "Struct:Linear";
28 pub const BATCH_NORM: &str = "Struct:BatchNorm";
29 pub const LAYER_NORM: &str = "Struct:LayerNorm";
30 pub const GROUP_NORM: &str = "Struct:GroupNorm";
31 pub const EMBEDDING: &str = "Struct:Embedding";
32 pub const CONV1D: &str = "Struct:Conv1d";
33 pub const CONV2D: &str = "Struct:Conv2d";
34 pub const CONV3D: &str = "Struct:Conv3d";
35 pub const CONV_TRANSPOSE1D: &str = "Struct:ConvTranspose1d";
36 pub const CONV_TRANSPOSE2D: &str = "Struct:ConvTranspose2d";
37 pub const CONV_TRANSPOSE3D: &str = "Struct:ConvTranspose3d";
38 pub const DEFORM_CONV2D: &str = "Struct:DeformConv2d";
39 pub const INSTANCE_NORM: &str = "Struct:InstanceNorm";
40 pub const RMS_NORM: &str = "Struct:RmsNorm";
41 pub const PRELU: &str = "Struct:PRelu";
42}
43
44#[derive(Debug, Clone, Copy)]
60pub struct ModuleContext<'a> {
61 container_stack: &'a [String],
62}
63
64impl<'a> ModuleContext<'a> {
65 pub fn new(container_stack: &'a [String]) -> Self {
71 Self { container_stack }
72 }
73
74 pub fn module_type(&self) -> Option<&'a str> {
84 self.container_stack
85 .iter()
86 .rev()
87 .find(|ct| ct.starts_with("Struct:") || ct.starts_with("Enum:"))
88 .map(|s| s.as_str())
89 }
90}
91
92pub trait ModuleAdapter: Send + Sync {
94 fn adapt(&self, tensor: PackTensor, ctx: ModuleContext<'_>) -> PackTensor;
107
108 fn get_alternative_param_name(&self, _param_name: &str, _module_type: &str) -> Option<String> {
121 None
122 }
123
124 fn clone_box(&self) -> Box<dyn ModuleAdapter>;
126
127 fn chain<A>(self, next: A) -> ChainAdapter
137 where
138 Self: Sized + 'static,
139 A: ModuleAdapter + 'static,
140 {
141 ChainAdapter::new(self, next)
142 }
143}
144
145impl Clone for Box<dyn ModuleAdapter> {
146 fn clone(&self) -> Self {
147 self.clone_box()
148 }
149}
150
151#[derive(Clone)]
155pub struct ChainAdapter {
156 first: Box<dyn ModuleAdapter>,
157 second: Box<dyn ModuleAdapter>,
158}
159
160impl ChainAdapter {
161 pub fn new<A, B>(first: A, second: B) -> Self
163 where
164 A: ModuleAdapter + 'static,
165 B: ModuleAdapter + 'static,
166 {
167 Self {
168 first: Box::new(first),
169 second: Box::new(second),
170 }
171 }
172}
173
174impl ModuleAdapter for ChainAdapter {
175 fn adapt(&self, tensor: PackTensor, ctx: ModuleContext<'_>) -> PackTensor {
176 self.second.adapt(self.first.adapt(tensor, ctx), ctx)
177 }
178
179 fn get_alternative_param_name(&self, param_name: &str, container_type: &str) -> Option<String> {
180 if let Some(name) = self
181 .first
182 .get_alternative_param_name(param_name, container_type)
183 {
184 self.second
185 .get_alternative_param_name(&name, container_type)
186 .or(Some(name))
187 } else {
188 self.second
189 .get_alternative_param_name(param_name, container_type)
190 }
191 }
192
193 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
194 Box::new(self.clone())
195 }
196}
197
198#[derive(Debug, Clone, Default)]
200pub struct IdentityAdapter;
201
202impl ModuleAdapter for IdentityAdapter {
203 fn adapt(&self, tensor: PackTensor, _ctx: ModuleContext<'_>) -> PackTensor {
204 tensor
205 }
206
207 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
208 Box::new(self.clone())
209 }
210}
211
212fn cast(tensor: PackTensor, target: DType) -> PackTensor {
214 let (name, shape) = (tensor.name.clone(), tensor.shape.clone());
215
216 bridge::map_data(tensor, name, target, shape, move |data| {
217 data.convert_dtype(target)
218 })
219}
220
221fn param_name(tensor: &PackTensor) -> &str {
223 tensor.name.rsplit('.').next().unwrap_or("")
224}
225
226fn rename_param(mut tensor: PackTensor, new_name: &str) -> PackTensor {
228 let keep = tensor.name.rfind('.').map(|i| i + 1).unwrap_or(0);
229 tensor.name.truncate(keep);
230 tensor.name.push_str(new_name);
231 tensor
232}
233
234fn default_half_precision_modules() -> HashSet<String> {
241 let modules = [
242 module_names::LINEAR,
243 module_names::EMBEDDING,
244 module_names::CONV1D,
245 module_names::CONV2D,
246 module_names::CONV3D,
247 module_names::CONV_TRANSPOSE1D,
248 module_names::CONV_TRANSPOSE2D,
249 module_names::CONV_TRANSPOSE3D,
250 module_names::DEFORM_CONV2D,
251 module_names::LAYER_NORM,
252 module_names::GROUP_NORM,
253 module_names::INSTANCE_NORM,
254 module_names::RMS_NORM,
255 module_names::PRELU,
256 ];
257 modules.iter().map(|s| s.to_string()).collect()
258}
259
260#[derive(Debug, Clone)]
296pub struct HalfPrecisionAdapter {
297 modules: HashSet<String>,
298}
299
300impl HalfPrecisionAdapter {
301 pub fn new() -> Self {
303 Self {
304 modules: default_half_precision_modules(),
305 }
306 }
307
308 pub fn with_module(mut self, module_type: impl Into<String>) -> Self {
314 let name = module_type.into();
315 if name.contains(':') {
316 self.modules.insert(name);
317 } else {
318 self.modules.insert(format!("Struct:{}", name));
319 }
320 self
321 }
322
323 pub fn without_module(mut self, module_type: impl Into<String>) -> Self {
325 let name = module_type.into();
326 let key = if name.contains(':') {
327 name
328 } else {
329 format!("Struct:{}", name)
330 };
331 assert!(
332 self.modules.contains(&key),
333 "without_module called with '{}' which is not in the module set",
334 key
335 );
336 self.modules.remove(&key);
337 self
338 }
339
340 fn should_convert(&self, ctx: ModuleContext<'_>) -> bool {
342 ctx.module_type()
343 .is_some_and(|mt| self.modules.contains(mt))
344 }
345}
346
347impl Default for HalfPrecisionAdapter {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353impl ModuleAdapter for HalfPrecisionAdapter {
354 fn adapt(&self, tensor: PackTensor, ctx: ModuleContext<'_>) -> PackTensor {
355 let target_dtype = match tensor.dtype {
357 DType::F32 => DType::F16,
358 DType::F16 => DType::F32,
359 _ => return tensor,
360 };
361
362 if !self.should_convert(ctx) {
363 return tensor;
364 }
365
366 cast(tensor, target_dtype)
367 }
368
369 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
370 Box::new(self.clone())
371 }
372}
373
374#[derive(Debug, Clone)]
404pub struct FloatCastAdapter {
405 target: DType,
407}
408
409impl FloatCastAdapter {
410 pub fn to(target: DType) -> Self {
416 assert!(
417 target.is_float(),
418 "FloatCastAdapter target must be a float dtype, got {target:?}"
419 );
420 Self { target }
421 }
422}
423
424impl ModuleAdapter for FloatCastAdapter {
425 fn adapt(&self, tensor: PackTensor, _ctx: ModuleContext<'_>) -> PackTensor {
426 if !tensor.dtype.is_float() || tensor.dtype == self.target {
427 return tensor;
428 }
429
430 cast(tensor, self.target)
431 }
432
433 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
434 Box::new(self.clone())
435 }
436}
437
438#[derive(Debug, Clone, Default)]
444pub struct PyTorchToBurnAdapter;
445
446impl ModuleAdapter for PyTorchToBurnAdapter {
447 fn adapt(&self, tensor: PackTensor, ctx: ModuleContext<'_>) -> PackTensor {
448 adapt_pytorch_tensor(tensor, ctx, PyTorchConversionDirection::PyTorchToBurn)
449 }
450
451 fn get_alternative_param_name(&self, param_name: &str, container_type: &str) -> Option<String> {
452 if is_normalization_layer(container_type) {
454 burn_norm_param_to_pytorch(param_name).map(|s| s.to_string())
455 } else {
456 None
457 }
458 }
459
460 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
461 Box::new(self.clone())
462 }
463}
464
465#[derive(Debug, Clone, Default)]
471pub struct BurnToPyTorchAdapter;
472
473impl ModuleAdapter for BurnToPyTorchAdapter {
474 fn adapt(&self, tensor: PackTensor, ctx: ModuleContext<'_>) -> PackTensor {
475 adapt_pytorch_tensor(tensor, ctx, PyTorchConversionDirection::BurnToPyTorch)
476 }
477
478 fn get_alternative_param_name(&self, param_name: &str, container_type: &str) -> Option<String> {
479 if is_normalization_layer(container_type) {
481 pytorch_norm_param_to_burn(param_name).map(|s| s.to_string())
482 } else {
483 None
484 }
485 }
486
487 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
488 Box::new(self.clone())
489 }
490}
491
492#[derive(Debug, Clone, Copy)]
494enum PyTorchConversionDirection {
495 PyTorchToBurn,
496 BurnToPyTorch,
497}
498
499fn is_normalization_layer(container_type: &str) -> bool {
501 matches!(
502 container_type,
503 module_names::BATCH_NORM
504 | module_names::LAYER_NORM
505 | module_names::GROUP_NORM
506 | module_names::RMS_NORM
507 )
508}
509
510fn pytorch_norm_param_to_burn(param_name: &str) -> Option<&'static str> {
512 match param_name {
513 "weight" => Some("gamma"),
514 "bias" => Some("beta"),
515 _ => None,
516 }
517}
518
519fn burn_norm_param_to_pytorch(param_name: &str) -> Option<&'static str> {
521 match param_name {
522 "gamma" => Some("weight"),
523 "beta" => Some("bias"),
524 _ => None,
525 }
526}
527
528fn adapt_pytorch_tensor(
530 tensor: PackTensor,
531 ctx: ModuleContext<'_>,
532 direction: PyTorchConversionDirection,
533) -> PackTensor {
534 let Some(module_type) = ctx.module_type() else {
536 return tensor; };
538 let param = param_name(&tensor);
539
540 if module_type == module_names::LINEAR && param == "weight" && tensor.shape.len() == 2 {
542 return transpose_2d_tensor(tensor);
543 }
544
545 if is_normalization_layer(module_type) {
547 let renamed = match direction {
548 PyTorchConversionDirection::PyTorchToBurn => pytorch_norm_param_to_burn(param),
549 PyTorchConversionDirection::BurnToPyTorch => burn_norm_param_to_pytorch(param),
550 };
551
552 if let Some(new_name) = renamed {
553 return rename_param(tensor, new_name);
554 }
555 }
556
557 tensor
558}
559
560fn transpose_2d_tensor(tensor: PackTensor) -> PackTensor {
562 if tensor.shape.len() != 2 {
563 return tensor;
564 }
565
566 if matches!(tensor.dtype, DType::QFloat(_)) {
571 return tensor;
572 }
573
574 let (name, dtype) = (tensor.name.clone(), tensor.dtype);
575 let transposed_shape = shape![tensor.shape[1], tensor.shape[0]];
576
577 bridge::map_data(tensor, name, dtype, transposed_shape, transpose_tensor_data)
579}
580
581fn transpose_tensor_data(data: TensorData) -> TensorData {
583 let shape = data.shape();
584 let rows = shape[0];
585 let cols = shape[1];
586 let transposed_shape = vec![cols, rows];
587
588 let bytes = data.as_bytes();
590 let element_size = data.dtype().size();
591
592 let mut transposed_bytes = vec![0u8; bytes.len()];
594
595 for i in 0..rows {
598 for j in 0..cols {
599 let src_idx = (i * cols + j) * element_size;
600 let dst_idx = (j * rows + i) * element_size;
601
602 transposed_bytes[dst_idx..dst_idx + element_size]
604 .copy_from_slice(&bytes[src_idx..src_idx + element_size]);
605 }
606 }
607
608 TensorData::from_bytes_vec(transposed_bytes, transposed_shape, data.dtype())
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use alloc::string::ToString;
616 use alloc::sync::Arc;
617 use alloc::vec::Vec;
618 use burn_core::tensor::{Bytes, DType, Shape, TensorData};
619 use core::sync::atomic::{AtomicUsize, Ordering};
620
621 #[test]
622 fn test_module_names_match_burn_nn() {
623 #[allow(unused_imports)]
625 use burn_nn::{
626 BatchNorm, Embedding, GroupNorm, InstanceNorm, LayerNorm, Linear, PRelu, RmsNorm,
627 conv::{
628 Conv1d, Conv2d, Conv3d, ConvTranspose1d, ConvTranspose2d, ConvTranspose3d,
629 DeformConv2d,
630 },
631 };
632
633 assert_eq!(module_names::LINEAR, "Struct:Linear");
634 assert_eq!(module_names::BATCH_NORM, "Struct:BatchNorm");
635 assert_eq!(module_names::LAYER_NORM, "Struct:LayerNorm");
636 assert_eq!(module_names::GROUP_NORM, "Struct:GroupNorm");
637 assert_eq!(module_names::EMBEDDING, "Struct:Embedding");
638 assert_eq!(module_names::CONV1D, "Struct:Conv1d");
639 assert_eq!(module_names::CONV2D, "Struct:Conv2d");
640 assert_eq!(module_names::CONV3D, "Struct:Conv3d");
641 assert_eq!(module_names::CONV_TRANSPOSE1D, "Struct:ConvTranspose1d");
642 assert_eq!(module_names::CONV_TRANSPOSE2D, "Struct:ConvTranspose2d");
643 assert_eq!(module_names::CONV_TRANSPOSE3D, "Struct:ConvTranspose3d");
644 assert_eq!(module_names::DEFORM_CONV2D, "Struct:DeformConv2d");
645 assert_eq!(module_names::INSTANCE_NORM, "Struct:InstanceNorm");
646 assert_eq!(module_names::RMS_NORM, "Struct:RmsNorm");
647 assert_eq!(module_names::PRELU, "Struct:PRelu");
648 }
649
650 fn tensor_with(name: &str, data: TensorData) -> PackTensor {
652 bridge::from_data(data, name.to_string(), None)
653 }
654
655 fn tensor(name: &str, shape: Shape) -> PackTensor {
657 let values = vec![1.0f32; shape.iter().product()];
658 tensor_with(name, TensorData::new(values, shape))
659 }
660
661 fn containers(container_type: &str) -> Vec<String> {
663 vec![container_type.to_string()]
664 }
665
666 fn adapt_in(
668 adapter: &dyn ModuleAdapter,
669 name: &str,
670 shape: Shape,
671 container_type: &str,
672 ) -> PackTensor {
673 let containers = containers(container_type);
674 adapter.adapt(tensor(name, shape), ModuleContext::new(&containers))
675 }
676
677 fn adapt_without_module(adapter: &dyn ModuleAdapter, tensor: PackTensor) -> PackTensor {
679 adapter.adapt(tensor, ModuleContext::new(&[]))
680 }
681
682 #[test]
683 fn test_pytorch_to_burn_linear_weight() {
684 let adapter = PyTorchToBurnAdapter;
685
686 let adapted = adapt_in(&adapter, "fc.weight", shape![10, 5], module_names::LINEAR);
688 assert_eq!(adapted.shape, shape![5, 10]);
689
690 let adapted = adapt_in(&adapter, "fc.bias", shape![10], module_names::LINEAR);
692 assert_eq!(adapted.shape, shape![10]);
693 }
694
695 #[test]
696 fn test_pytorch_to_burn_norm_params() {
697 let adapter = PyTorchToBurnAdapter;
698
699 let adapted = adapt_in(
701 &adapter,
702 "norm.weight",
703 shape![10],
704 module_names::BATCH_NORM,
705 );
706 assert_eq!(adapted.name, "norm.gamma");
707
708 let adapted = adapt_in(&adapter, "norm.bias", shape![10], module_names::BATCH_NORM);
710 assert_eq!(adapted.name, "norm.beta");
711 }
712
713 #[test]
714 fn test_burn_to_pytorch_linear_weight() {
715 let adapter = BurnToPyTorchAdapter;
716
717 let adapted = adapt_in(&adapter, "fc.weight", shape![5, 10], module_names::LINEAR);
719 assert_eq!(adapted.shape, shape![10, 5]);
720 }
721
722 #[test]
723 fn test_burn_to_pytorch_norm_params() {
724 let adapter = BurnToPyTorchAdapter;
725
726 let adapted = adapt_in(&adapter, "norm.gamma", shape![10], module_names::BATCH_NORM);
728 assert_eq!(adapted.name, "norm.weight");
729
730 let adapted = adapt_in(&adapter, "norm.beta", shape![10], module_names::BATCH_NORM);
732 assert_eq!(adapted.name, "norm.bias");
733 }
734
735 #[test]
738 fn rename_keeps_the_enclosing_path() {
739 let adapter = PyTorchToBurnAdapter;
740
741 let adapted = adapt_in(
742 &adapter,
743 "encoder.layers.0.norm.weight",
744 shape![10],
745 module_names::LAYER_NORM,
746 );
747 assert_eq!(adapted.name, "encoder.layers.0.norm.gamma");
748
749 let adapted = adapt_in(&adapter, "weight", shape![10], module_names::LAYER_NORM);
750 assert_eq!(adapted.name, "gamma");
751 }
752
753 #[test]
754 fn test_transpose_different_dtypes() {
755 let f32_data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]);
759 let transposed = transpose_tensor_data(f32_data);
760 assert_eq!(*transposed.shape(), shape![3, 2]);
761 let values = transposed.try_to_vec::<f32>().unwrap();
762 assert_eq!(values, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
763
764 let i32_data = TensorData::new(vec![1i32, 2, 3, 4, 5, 6], [2, 3]);
766 let transposed = transpose_tensor_data(i32_data);
767 assert_eq!(*transposed.shape(), shape![3, 2]);
768 let values = transposed.try_to_vec::<i32>().unwrap();
769 assert_eq!(values, vec![1, 4, 2, 5, 3, 6]);
770
771 let f64_data = TensorData::new(vec![1.0f64, 2.0, 3.0, 4.0], [2, 2]);
773 let transposed = transpose_tensor_data(f64_data);
774 assert_eq!(*transposed.shape(), shape![2, 2]);
775 let values = transposed.try_to_vec::<f64>().unwrap();
776 assert_eq!(values, vec![1.0, 3.0, 2.0, 4.0]);
777 }
778
779 #[test]
782 fn transpose_moves_the_data_not_just_the_shape() {
783 let adapter = PyTorchToBurnAdapter;
784 let data = TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]);
785 let containers = containers(module_names::LINEAR);
786
787 let adapted = adapter.adapt(
788 tensor_with("fc.weight", data),
789 ModuleContext::new(&containers),
790 );
791
792 assert_eq!(adapted.shape, shape![3, 2]);
793 let values = bridge::to_data(&adapted)
794 .unwrap()
795 .try_into_vec::<f32>()
796 .unwrap();
797 assert_eq!(values, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
798 }
799
800 #[test]
801 fn test_no_container_info() {
802 let adapter = PyTorchToBurnAdapter;
803
804 let adapted = adapt_without_module(&adapter, tensor("fc.weight", shape![10, 5]));
806 assert_eq!(adapted.shape, shape![10, 5]); let adapted = adapt_without_module(&adapter, tensor("other.weight", shape![10, 5]));
810 assert_eq!(adapted.shape, shape![10, 5]); }
812
813 #[test]
816 fn module_type_looks_past_collection_wrappers() {
817 let adapter = PyTorchToBurnAdapter;
818 let containers = vec![
819 "Struct:Model".to_string(),
820 "Vec".to_string(),
821 module_names::LINEAR.to_string(),
822 ];
823
824 let adapted = adapter.adapt(
825 tensor("layers.0.weight", shape![10, 5]),
826 ModuleContext::new(&containers),
827 );
828 assert_eq!(adapted.shape, shape![5, 10]);
829 }
830
831 #[derive(Clone)]
832 struct RenameParamAdapter {
833 from: &'static str,
834 to: &'static str,
835 called: Arc<AtomicUsize>,
836 }
837
838 impl ModuleAdapter for RenameParamAdapter {
839 fn adapt(&self, tensor: PackTensor, _ctx: ModuleContext<'_>) -> PackTensor {
840 self.called.fetch_add(1, Ordering::Relaxed);
841
842 if param_name(&tensor) != self.from {
843 return tensor;
844 }
845
846 rename_param(tensor, self.to)
847 }
848
849 fn get_alternative_param_name(
850 &self,
851 _param_name: &str,
852 _container_type: &str,
853 ) -> Option<String> {
854 None
855 }
856
857 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
858 Box::new(self.clone())
859 }
860 }
861
862 #[derive(Clone)]
863 struct AltNameAdapter {
864 from: &'static str,
865 to: &'static str,
866 called: Arc<AtomicUsize>,
867 }
868
869 impl ModuleAdapter for AltNameAdapter {
870 fn adapt(&self, tensor: PackTensor, _ctx: ModuleContext<'_>) -> PackTensor {
871 tensor
872 }
873
874 fn get_alternative_param_name(
875 &self,
876 param_name: &str,
877 _container_type: &str,
878 ) -> Option<String> {
879 self.called.fetch_add(1, Ordering::Relaxed);
880 if param_name == self.from {
881 Some(self.to.to_string())
882 } else {
883 None
884 }
885 }
886
887 fn clone_box(&self) -> Box<dyn ModuleAdapter> {
888 Box::new(self.clone())
889 }
890 }
891
892 #[test]
893 fn test_chain_adapter_pipes_adapt() {
894 let called1 = Arc::new(AtomicUsize::new(0));
895 let called2 = Arc::new(AtomicUsize::new(0));
896
897 let a = RenameParamAdapter {
898 from: "weight",
899 to: "a",
900 called: called1.clone(),
901 };
902 let b = RenameParamAdapter {
903 from: "a",
904 to: "b",
905 called: called2.clone(),
906 };
907
908 let chain = a.chain(b);
909 let adapted = adapt_in(&chain, "fc.weight", shape![2, 2], module_names::LINEAR);
910
911 assert_eq!(adapted.name, "fc.b");
912 assert_eq!(called1.load(Ordering::Relaxed), 1);
913 assert_eq!(called2.load(Ordering::Relaxed), 1);
914 }
915
916 #[test]
917 fn test_chain_adapter_alternative_name_pipes_and_fallbacks() {
918 let called1 = Arc::new(AtomicUsize::new(0));
919 let called2 = Arc::new(AtomicUsize::new(0));
920
921 let a = AltNameAdapter {
922 from: "gamma",
923 to: "weight",
924 called: called1.clone(),
925 };
926 let b = AltNameAdapter {
927 from: "weight",
928 to: "scale",
929 called: called2.clone(),
930 };
931
932 let chain = a.chain(b);
933 let alt = chain.get_alternative_param_name("gamma", module_names::LAYER_NORM);
934 assert_eq!(alt.as_deref(), Some("scale"));
935 assert_eq!(called1.load(Ordering::Relaxed), 1);
936 assert_eq!(called2.load(Ordering::Relaxed), 1);
937
938 let called1 = Arc::new(AtomicUsize::new(0));
941 let called2 = Arc::new(AtomicUsize::new(0));
942 let a = AltNameAdapter {
943 from: "gamma",
944 to: "weight",
945 called: called1.clone(),
946 };
947 let b = AltNameAdapter {
948 from: "something-else",
949 to: "unused",
950 called: called2.clone(),
951 };
952 let chain = a.chain(b);
953 let alt = chain.get_alternative_param_name("gamma", module_names::LAYER_NORM);
954 assert_eq!(alt.as_deref(), Some("weight"));
955 assert_eq!(called1.load(Ordering::Relaxed), 1);
956 assert_eq!(called2.load(Ordering::Relaxed), 1);
957
958 let called1 = Arc::new(AtomicUsize::new(0));
960 let called2 = Arc::new(AtomicUsize::new(0));
961 let a = AltNameAdapter {
962 from: "something-else",
963 to: "unused",
964 called: called1.clone(),
965 };
966 let b = AltNameAdapter {
967 from: "gamma",
968 to: "weight",
969 called: called2.clone(),
970 };
971 let chain = a.chain(b);
972 let alt = chain.get_alternative_param_name("gamma", module_names::LAYER_NORM);
973 assert_eq!(alt.as_deref(), Some("weight"));
974 assert_eq!(called1.load(Ordering::Relaxed), 1);
975 assert_eq!(called2.load(Ordering::Relaxed), 1);
976
977 let boxed = chain.clone_box();
979 let alt = boxed.get_alternative_param_name("gamma", module_names::LAYER_NORM);
980 assert_eq!(alt.as_deref(), Some("weight"));
981 }
982
983 #[test]
984 fn test_half_precision_f32_to_f16() {
985 let adapter = HalfPrecisionAdapter::new();
986
987 let adapted = adapt_in(&adapter, "fc.weight", shape![2, 3], module_names::LINEAR);
988 assert_eq!(adapted.dtype, DType::F16);
989 assert_eq!(adapted.shape, shape![2, 3]);
990
991 let data = bridge::to_data(&adapted).unwrap();
992 assert_eq!(data.dtype(), DType::F16);
993 }
994
995 #[test]
999 fn test_half_precision_updates_byte_len() {
1000 let adapter = HalfPrecisionAdapter::new();
1001
1002 let adapted = adapt_in(&adapter, "fc.weight", shape![2, 3], module_names::LINEAR);
1003 assert_eq!(adapted.byte_len(), 6 * 2);
1004 assert_eq!(bridge::to_data(&adapted).unwrap().bytes().len(), 6 * 2);
1005 }
1006
1007 #[test]
1008 fn test_half_precision_f16_to_f32() {
1009 let adapter = HalfPrecisionAdapter::new();
1010
1011 let data = TensorData::new(vec![1.0f32; 6], shape![2, 3]).convert_dtype(DType::F16);
1013 let containers = containers(module_names::LINEAR);
1014
1015 let adapted = adapter.adapt(
1016 tensor_with("fc.weight", data),
1017 ModuleContext::new(&containers),
1018 );
1019 assert_eq!(adapted.dtype, DType::F32);
1020 }
1021
1022 #[test]
1023 fn test_half_precision_skips_batch_norm() {
1024 let adapter = HalfPrecisionAdapter::new();
1025
1026 let adapted = adapt_in(
1028 &adapter,
1029 "norm.weight",
1030 shape![10],
1031 module_names::BATCH_NORM,
1032 );
1033 assert_eq!(adapted.dtype, DType::F32); }
1035
1036 #[test]
1037 fn test_half_precision_converts_default_modules() {
1038 let adapter = HalfPrecisionAdapter::new();
1039
1040 for (name, shape, module) in [
1041 ("fc.weight", shape![2, 3], module_names::LINEAR),
1042 ("emb.weight", shape![100, 64], module_names::EMBEDDING),
1043 ("conv.weight", shape![3, 3, 3, 3], module_names::CONV2D),
1044 ("norm.gamma", shape![10], module_names::LAYER_NORM),
1045 ("gn.gamma", shape![10], module_names::GROUP_NORM),
1046 ("rms.weight", shape![10], module_names::RMS_NORM),
1047 ] {
1048 assert_eq!(
1049 adapt_in(&adapter, name, shape, module).dtype,
1050 DType::F16,
1051 "{module} should be converted"
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn test_half_precision_without_module() {
1058 let adapter = HalfPrecisionAdapter::new().without_module("LayerNorm");
1059
1060 let adapted = adapt_in(&adapter, "norm.gamma", shape![10], module_names::LAYER_NORM);
1062 assert_eq!(adapted.dtype, DType::F32);
1063
1064 let adapted = adapt_in(&adapter, "fc.weight", shape![2, 3], module_names::LINEAR);
1066 assert_eq!(adapted.dtype, DType::F16);
1067 }
1068
1069 #[test]
1070 fn test_half_precision_with_module() {
1071 let adapter = HalfPrecisionAdapter::new().with_module("CustomLayer");
1072
1073 let adapted = adapt_in(&adapter, "custom.weight", shape![5], "Struct:CustomLayer");
1075 assert_eq!(adapted.dtype, DType::F16);
1076 }
1077
1078 #[test]
1079 fn test_half_precision_with_qualified_name() {
1080 let adapter = HalfPrecisionAdapter::new().with_module("Struct:CustomLayer");
1081
1082 let adapted = adapt_in(&adapter, "custom.weight", shape![5], "Struct:CustomLayer");
1083 assert_eq!(adapted.dtype, DType::F16);
1084 }
1085
1086 #[test]
1087 fn test_half_precision_chain() {
1088 let adapter = PyTorchToBurnAdapter.chain(HalfPrecisionAdapter::new());
1089
1090 let adapted = adapt_in(&adapter, "fc.weight", shape![10, 5], module_names::LINEAR);
1091
1092 assert_eq!(adapted.shape, shape![5, 10]);
1094 assert_eq!(adapted.dtype, DType::F16);
1095 }
1096
1097 #[test]
1098 fn test_half_precision_skips_no_container() {
1099 let adapter = HalfPrecisionAdapter::new();
1100
1101 let adapted = adapt_without_module(&adapter, tensor("fc.weight", shape![2, 3]));
1103 assert_eq!(adapted.dtype, DType::F32);
1104 }
1105
1106 #[test]
1107 fn test_half_precision_skips_non_float() {
1108 use burn_core::tensor::quantization::QuantScheme;
1109
1110 let adapter = HalfPrecisionAdapter::new();
1111
1112 let qfloat_dtype = DType::QFloat(QuantScheme::default());
1115 let shape = shape![2, 3];
1116 let bytes = Bytes::from_bytes_vec(vec![0u8; bridge::data_len(qfloat_dtype, &shape)]);
1117 let qfloat = PackTensor::new("fc.weight".to_string(), qfloat_dtype, shape, None, bytes);
1118
1119 let containers = containers(module_names::LINEAR);
1120 let adapted = adapter.adapt(qfloat, ModuleContext::new(&containers));
1121 assert_eq!(adapted.dtype, qfloat_dtype);
1122 }
1123
1124 #[test]
1125 fn test_half_precision_default_module_count() {
1126 let adapter = HalfPrecisionAdapter::new();
1127 assert_eq!(adapter.modules.len(), 14);
1130 }
1131
1132 #[test]
1133 fn test_half_precision_without_module_qualified() {
1134 let adapter = HalfPrecisionAdapter::new().without_module("Struct:LayerNorm");
1135
1136 let adapted = adapt_in(&adapter, "norm.gamma", shape![10], module_names::LAYER_NORM);
1137 assert_eq!(adapted.dtype, DType::F32);
1138 }
1139
1140 fn float_tensor(dtype: DType) -> PackTensor {
1143 let values = vec![1.0f32, -2.0, 0.5, 4.0, -0.25, 8.0];
1144 let data = TensorData::new(values, shape![2, 3]).convert_dtype(dtype);
1145 tensor_with("fc.weight", data)
1146 }
1147
1148 #[test]
1149 fn test_float_cast_bf16_to_f16() {
1150 let adapter = FloatCastAdapter::to(DType::F16);
1152 let containers = containers(module_names::LINEAR);
1153
1154 let adapted = adapter.adapt(float_tensor(DType::BF16), ModuleContext::new(&containers));
1155 assert_eq!(adapted.dtype, DType::F16);
1156 assert_eq!(adapted.shape, shape![2, 3]);
1157 assert_eq!(adapted.name, "fc.weight");
1158
1159 let data = bridge::to_data(&adapted).unwrap();
1160 assert_eq!(data.dtype(), DType::F16);
1161 let values = data
1162 .convert_dtype(DType::F32)
1163 .try_into_vec::<f32>()
1164 .unwrap();
1165 assert_eq!(values, vec![1.0, -2.0, 0.5, 4.0, -0.25, 8.0]);
1166 }
1167
1168 #[test]
1169 fn test_float_cast_converts_any_module_type() {
1170 let adapter = FloatCastAdapter::to(DType::F16);
1172 let containers = containers("Struct:CustomLayer");
1173 let adapted = adapter.adapt(float_tensor(DType::F32), ModuleContext::new(&containers));
1174 assert_eq!(adapted.dtype, DType::F16);
1175
1176 let adapted = adapt_without_module(&adapter, float_tensor(DType::F32));
1178 assert_eq!(adapted.dtype, DType::F16);
1179 }
1180
1181 #[test]
1182 fn test_float_cast_passthrough_on_target_dtype() {
1183 let adapter = FloatCastAdapter::to(DType::F16);
1184 let adapted = adapt_without_module(&adapter, float_tensor(DType::F16));
1185 assert_eq!(adapted.dtype, DType::F16);
1186 }
1187
1188 #[test]
1189 fn test_float_cast_skips_non_float() {
1190 let adapter = FloatCastAdapter::to(DType::F16);
1191 let containers = containers(module_names::EMBEDDING);
1192
1193 let data = TensorData::new(vec![1i64, 2, 3], shape![3]);
1194 let adapted = adapter.adapt(tensor_with("idx", data), ModuleContext::new(&containers));
1195 assert_eq!(adapted.dtype, DType::I64);
1196 }
1197
1198 #[test]
1203 fn a_quantized_linear_weight_is_not_transposed() {
1204 use burn_core::tensor::quantization::{QuantScheme, QuantValue};
1205 use burn_core::tensor::{Device, Distribution, Tensor};
1206
1207 let device = Device::default();
1208 let quantized = Tensor::<2>::random(shape![3, 8], Distribution::Default, &device)
1209 .quantize_dynamic(&QuantScheme::default().with_value(QuantValue::Q4S));
1210
1211 let source = bridge::from_tensor(&quantized, "fc.weight".to_string(), None);
1212 let (shape, bytes) = (
1213 source.shape.clone(),
1214 bridge::to_data(&source).unwrap().as_bytes().to_vec(),
1215 );
1216
1217 let containers = containers(module_names::LINEAR);
1218 let adapted = BurnToPyTorchAdapter.adapt(source, ModuleContext::new(&containers));
1219
1220 assert_eq!(
1221 adapted.shape, shape,
1222 "a quantized weight must pass through unchanged"
1223 );
1224 assert_eq!(
1225 bridge::to_data(&adapted).unwrap().as_bytes().to_vec(),
1226 bytes
1227 );
1228 }
1229
1230 #[test]
1232 fn module_type_is_none_without_a_user_defined_module() {
1233 let containers = vec!["Vec".to_string(), "Array".to_string()];
1234 assert_eq!(ModuleContext::new(&containers).module_type(), None);
1235 }
1236
1237 #[test]
1238 fn test_float_cast_chain_after_pytorch() {
1239 let adapter = PyTorchToBurnAdapter.chain(FloatCastAdapter::to(DType::F16));
1241
1242 let adapted = adapt_in(&adapter, "fc.weight", shape![10, 5], module_names::LINEAR);
1243 assert_eq!(adapted.shape, shape![5, 10]);
1244 assert_eq!(adapted.dtype, DType::F16);
1245 }
1246
1247 #[test]
1248 #[should_panic(expected = "must be a float dtype")]
1249 fn test_float_cast_rejects_non_float_target() {
1250 let _ = FloatCastAdapter::to(DType::I32);
1251 }
1252
1253 #[test]
1254 fn test_half_precision_with_module_batch_norm_opt_in() {
1255 let adapter = HalfPrecisionAdapter::new().with_module("BatchNorm");
1256
1257 let adapted = adapt_in(&adapter, "bn.weight", shape![10], module_names::BATCH_NORM);
1258 assert_eq!(adapted.dtype, DType::F16);
1259 }
1260}