use std::marker::PhantomData;
use burn::module::{Module, ModuleMapper, ModuleVisitor, Param};
use burn::tensor::{Tensor, backend::Backend};
pub trait ParamReshaper<B: Backend>: Send + Sync {
type Module: Module<B>;
fn num_params(&self) -> usize;
fn flatten(&self, module: &Self::Module, device: &B::Device) -> Tensor<B, 1>;
fn unflatten(&self, flat: Tensor<B, 1>) -> Self::Module;
}
pub struct ModuleReshaper<B: Backend, M: Module<B>> {
template: M,
num_params: usize,
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend, M: Module<B>> std::fmt::Debug for ModuleReshaper<B, M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleReshaper")
.field("num_params", &self.num_params)
.finish_non_exhaustive()
}
}
impl<B: Backend, M: Module<B>> Clone for ModuleReshaper<B, M> {
fn clone(&self) -> Self {
Self {
template: self.template.clone(),
num_params: self.num_params,
_backend: PhantomData,
}
}
}
impl<B: Backend, M: Module<B>> ModuleReshaper<B, M> {
#[must_use]
pub fn new(template: M) -> Self {
let mut counter = CountVisitor { count: 0 };
template.visit(&mut counter);
Self {
template,
num_params: counter.count,
_backend: PhantomData,
}
}
#[must_use]
pub fn template(&self) -> &M {
&self.template
}
#[must_use]
pub fn num_params(&self) -> usize {
self.num_params
}
}
impl<B, M> ParamReshaper<B> for ModuleReshaper<B, M>
where
B: Backend,
M: Module<B> + Sync,
{
type Module = M;
fn num_params(&self) -> usize {
self.num_params
}
fn flatten(&self, module: &M, device: &B::Device) -> Tensor<B, 1> {
let mut visitor: FlattenVisitor<B> = FlattenVisitor { parts: Vec::new() };
module.visit(&mut visitor);
assert!(
!visitor.parts.is_empty(),
"module has no float parameters to flatten"
);
Tensor::cat(visitor.parts, 0).to_device(device)
}
fn unflatten(&self, flat: Tensor<B, 1>) -> M {
let len = flat.dims()[0];
assert_eq!(
len, self.num_params,
"flat length {len} does not match num_params {}",
self.num_params
);
let mut mapper: SlicingMapper<B> = SlicingMapper { flat, cursor: 0 };
self.template.clone().map(&mut mapper)
}
}
struct CountVisitor {
count: usize,
}
impl<B: Backend> ModuleVisitor<B> for CountVisitor {
fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<B, D>>) {
self.count += param.dims().iter().product::<usize>();
}
}
struct FlattenVisitor<B: Backend> {
parts: Vec<Tensor<B, 1>>,
}
impl<B: Backend> ModuleVisitor<B> for FlattenVisitor<B> {
fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<B, D>>) {
let value: Tensor<B, D> = param.val();
let n: usize = value.dims().iter().product();
self.parts.push(value.reshape([n]));
}
}
struct SlicingMapper<B: Backend> {
flat: Tensor<B, 1>,
cursor: usize,
}
impl<B: Backend> ModuleMapper<B> for SlicingMapper<B> {
fn map_float<const D: usize>(&mut self, param: Param<Tensor<B, D>>) -> Param<Tensor<B, D>> {
let dims: [usize; D] = param.dims();
let n: usize = dims.iter().product();
let start = self.cursor;
self.cursor += n;
let flat = self.flat.clone();
param.map(move |_old| {
#[allow(clippy::single_range_in_vec_init)]
let slice = flat.slice([start..start + n]);
slice.reshape(dims)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::Flex;
use burn::nn::{
BatchNorm, BatchNormConfig, Linear, LinearConfig, Relu,
conv::{Conv2d, Conv2dConfig},
};
use burn::tensor::TensorData;
type TestBackend = Flex;
#[derive(Module, Debug)]
struct TestMlp<B: Backend> {
l1: Linear<B>,
act: Relu,
l2: Linear<B>,
}
impl<B: Backend> TestMlp<B> {
fn new(device: &B::Device) -> Self {
Self {
l1: LinearConfig::new(3, 4).init(device),
act: Relu::new(),
l2: LinearConfig::new(4, 2).init(device),
}
}
}
fn approx_eq(a: &Tensor<TestBackend, 1>, b: &Tensor<TestBackend, 1>) {
let av = a
.to_data()
.into_vec::<f32>()
.expect("genome host-read of a tensor this test just built");
let bv = b
.to_data()
.into_vec::<f32>()
.expect("genome host-read of a tensor this test just built");
assert_eq!(av.len(), bv.len(), "length mismatch");
for (x, y) in av.iter().zip(bv.iter()) {
approx::assert_relative_eq!(x, y, epsilon = 1e-6);
}
}
#[test]
fn test_module_reshaper_num_params_matches_expected() {
let device = Default::default();
let mlp = TestMlp::<TestBackend>::new(&device);
let reshaper = ModuleReshaper::new(mlp);
assert_eq!(reshaper.num_params(), 26);
}
#[test]
#[should_panic(expected = "module has no float parameters to flatten")]
fn test_module_reshaper_flatten_panics_on_empty_module() {
let device = Default::default();
let reshaper = ModuleReshaper::<TestBackend, Relu>::new(Relu::new());
let _ = reshaper.flatten(&Relu::new(), &device);
}
#[test]
#[should_panic(expected = "flat length")]
fn test_module_reshaper_unflatten_panics_on_length_mismatch() {
let device = Default::default();
let reshaper = ModuleReshaper::new(TestMlp::<TestBackend>::new(&device));
let wrong =
Tensor::<TestBackend, 1>::from_data(TensorData::new(vec![0f32; 10], [10]), &device);
let _ = reshaper.unflatten(wrong);
}
#[test]
fn test_module_reshaper_round_trip_mlp() {
let device = Default::default();
let mlp = TestMlp::<TestBackend>::new(&device);
let reshaper = ModuleReshaper::new(mlp.clone());
let flat = reshaper.flatten(&mlp, &device);
assert_eq!(flat.dims(), [26]);
let restored = reshaper.unflatten(flat.clone());
let flat2 = reshaper.flatten(&restored, &device);
approx_eq(&flat, &flat2);
}
#[test]
fn test_module_reshaper_round_trip_arbitrary_flat() {
let device = Default::default();
let mlp = TestMlp::<TestBackend>::new(&device);
let reshaper = ModuleReshaper::new(mlp);
#[allow(clippy::cast_precision_loss)]
let values: Vec<f32> = (0..26).map(|i| i as f32 * 0.1 - 1.3).collect();
let flat = Tensor::<TestBackend, 1>::from_data(TensorData::new(values, [26]), &device);
let module = reshaper.unflatten(flat.clone());
let flat2 = reshaper.flatten(&module, &device);
approx_eq(&flat, &flat2);
}
#[test]
fn test_module_reshaper_batchnorm_running_stats_traversed() {
let device = Default::default();
let d = 5;
let bn: BatchNorm<TestBackend> = BatchNormConfig::new(d).init(&device);
let reshaper = ModuleReshaper::new(bn.clone());
assert_eq!(
reshaper.num_params(),
4 * d,
"expected BatchNorm running stats to be traversed as float leaves"
);
let flat = reshaper.flatten(&bn, &device);
let restored = reshaper.unflatten(flat.clone());
approx_eq(&flat, &reshaper.flatten(&restored, &device));
}
#[test]
fn test_module_reshaper_round_trip_conv() {
let device = Default::default();
let conv: Conv2d<TestBackend> = Conv2dConfig::new([2, 3], [3, 3]).init(&device);
let reshaper = ModuleReshaper::new(conv.clone());
let flat = reshaper.flatten(&conv, &device);
let restored = reshaper.unflatten(flat.clone());
approx_eq(&flat, &reshaper.flatten(&restored, &device));
}
#[derive(Module, Debug)]
struct TestSmallMlp<B: Backend> {
l1: Linear<B>,
l2: Linear<B>,
}
impl<B: Backend> TestSmallMlp<B> {
fn new(device: &B::Device) -> Self {
Self {
l1: LinearConfig::new(2, 4).init(device),
l2: LinearConfig::new(4, 1).init(device),
}
}
}
#[derive(Module, Debug)]
struct TestLargeMlp<B: Backend> {
l1: Linear<B>,
l2: Linear<B>,
l3: Linear<B>,
}
impl<B: Backend> TestLargeMlp<B> {
fn new(device: &B::Device) -> Self {
Self {
l1: LinearConfig::new(2, 8).init(device),
l2: LinearConfig::new(8, 4).init(device),
l3: LinearConfig::new(4, 1).init(device),
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Module, Debug)]
enum TestArch<B: Backend> {
Shallow(TestSmallMlp<B>),
Deep(TestLargeMlp<B>),
}
#[test]
fn test_module_reshaper_enum_derive_compiles() {
let device = Default::default();
let shallow = TestArch::<TestBackend>::Shallow(TestSmallMlp::new(&device));
let deep = TestArch::<TestBackend>::Deep(TestLargeMlp::new(&device));
let shallow_reshaper = ModuleReshaper::new(shallow);
let deep_reshaper = ModuleReshaper::new(deep);
println!(
"burn_enum_derive_probe: #[derive(Module)] on enum COMPILES; \
enum is a Module. Shallow arm flattens to {} params, \
Deep arm flattens to {} params.",
shallow_reshaper.num_params(),
deep_reshaper.num_params(),
);
assert_eq!(shallow_reshaper.num_params(), 17);
assert_eq!(deep_reshaper.num_params(), 65);
}
}