burn-store 0.22.1

Storage and serialization infrastructure for Burn
Documentation
use burn_core as burn;

use crate::{ModuleSnapshot, SafetensorsStore};
use burn_core::module::{Module, Param};
use burn_core::tensor::{Device, Tensor, shape};
use burn_nn::{Linear, LinearConfig};

#[derive(Module, Debug)]
pub(super) struct ComplexModule {
    pub encoder: EncoderModule,
    pub decoder: DecoderModule,
    pub layers: Vec<Linear>,
}

#[derive(Module, Debug)]
pub(super) struct EncoderModule {
    pub weight: Param<Tensor<3>>,
    pub bias: Param<Tensor<1>>,
    pub norm: Param<Tensor<1>>,
}

#[derive(Module, Debug)]
pub(super) struct DecoderModule {
    pub projection: Linear,
    pub scale: Param<Tensor<2>>,
}

impl ComplexModule {
    pub fn new(device: &Device) -> Self {
        Self {
            encoder: EncoderModule {
                weight: Param::from_data(
                    [[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]],
                    device,
                ),
                bias: Param::from_data([0.1, 0.2, 0.3], device),
                norm: Param::from_data([1.0, 1.0, 1.0], device),
            },
            decoder: DecoderModule {
                projection: LinearConfig::new(4, 2).with_bias(true).init(device),
                scale: Param::from_data([[0.5, 0.5], [0.5, 0.5]], device),
            },
            layers: vec![
                LinearConfig::new(3, 4).with_bias(false).init(device),
                LinearConfig::new(4, 3).with_bias(true).init(device),
            ],
        }
    }

    pub fn new_zeros(device: &Device) -> Self {
        Self {
            encoder: EncoderModule {
                weight: Param::from_tensor(Tensor::zeros([2, 2, 2], device)),
                bias: Param::from_tensor(Tensor::zeros([3], device)),
                norm: Param::from_tensor(Tensor::zeros([3], device)),
            },
            decoder: DecoderModule {
                projection: LinearConfig::new(4, 2).with_bias(true).init(device),
                scale: Param::from_tensor(Tensor::zeros([2, 2], device)),
            },
            layers: vec![
                LinearConfig::new(3, 4).with_bias(false).init(device),
                LinearConfig::new(4, 3).with_bias(true).init(device),
            ],
        }
    }
}

#[test]
fn complex_module_round_trip() {
    let device = Default::default();
    let module1 = ComplexModule::new(&device);
    let mut module2 = ComplexModule::new_zeros(&device);

    // Save module1 using new store API
    let mut save_store = SafetensorsStore::from_bytes(None);
    module1.save_into(&mut save_store).unwrap();

    // Load into module2
    let mut load_store = SafetensorsStore::from_bytes(None);
    if let SafetensorsStore::Memory(ref mut p) = load_store
        && let SafetensorsStore::Memory(ref p_save) = save_store
    {
        // Get Arc and extract data
        let data_arc = p_save.data().unwrap();
        p.set_data(data_arc.as_ref().clone());
    }
    let result = module2.load_from(&mut load_store).unwrap();

    assert!(result.is_success());
    assert!(result.applied.len() > 5);
    assert_eq!(result.errors.len(), 0);

    // Verify data was imported correctly
    let module2_views = module2.collect(None, None, false);
    let encoder_weight = module2_views
        .iter()
        .find(|v| v.name == "encoder.weight")
        .map(|v| crate::bridge::to_data(v).unwrap())
        .unwrap();
    assert_eq!(*encoder_weight.shape(), shape![2, 2, 2]);
}

/// Safetensors stores no parameter identity, so importing from it must leave the target module's
/// ParamIds untouched. Randomizing them here would orphan optimizer state keyed by ParamId.
#[test]
fn round_trip_keeps_target_param_ids() {
    let device = Default::default();
    let module1 = ComplexModule::new(&device);
    let mut module2 = ComplexModule::new_zeros(&device);

    let expected_bias_id = module2.encoder.bias.id;
    let expected_norm_id = module2.encoder.norm.id;

    let mut save_store = SafetensorsStore::from_bytes(None);
    module1.save_into(&mut save_store).unwrap();

    let mut load_store = SafetensorsStore::from_bytes(None);
    if let SafetensorsStore::Memory(ref mut p) = load_store
        && let SafetensorsStore::Memory(ref p_save) = save_store
    {
        let data_arc = p_save.data().unwrap();
        p.set_data(data_arc.as_ref().clone());
    }
    let result = module2.load_from(&mut load_store).unwrap();
    assert!(result.is_success());

    assert_eq!(
        module2.encoder.bias.id, expected_bias_id,
        "encoder.bias should keep its ParamId across a safetensors import"
    );
    assert_eq!(
        module2.encoder.norm.id, expected_norm_id,
        "encoder.norm should keep its ParamId across a safetensors import"
    );
}