use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use burn::store::RecordError;
use burn::tensor::{Bytes, TensorData};
use burn_core as burn;
use burn_pack::{Reader, Scalar, Writer};
pub(crate) struct RecordTensor {
pub(crate) name: String,
pub(crate) param_id: Option<u64>,
pub(crate) data: TensorData,
}
#[derive(Default)]
pub struct OptimizerRecord {
pub(crate) tensors: Vec<RecordTensor>,
pub(crate) scalars: BTreeMap<String, Scalar>,
pub(crate) paths: BTreeMap<String, String>,
}
impl core::fmt::Debug for OptimizerRecord {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("OptimizerRecord")
.field("num_tensors", &self.tensors.len())
.field("num_scalars", &self.scalars.len())
.finish()
}
}
impl OptimizerRecord {
pub fn len(&self) -> usize {
self.tensors.len()
}
pub fn is_empty(&self) -> bool {
self.tensors.is_empty()
}
pub fn into_bytes(self) -> Result<Bytes, RecordError> {
Ok(self.into_writer().into_bytes()?)
}
pub fn from_bytes(bytes: Bytes) -> Result<Self, RecordError> {
Self::from_reader(Reader::from_bytes(bytes)?)
}
#[cfg(feature = "std")]
pub fn save<P: AsRef<std::path::Path>>(self, path: P) -> Result<(), RecordError> {
self.into_writer().write_to_file(path)?;
Ok(())
}
#[cfg(feature = "std")]
pub fn load<P: AsRef<std::path::Path>>(path: P) -> Result<Self, RecordError> {
Self::from_reader(Reader::from_file(path)?)
}
fn into_writer(self) -> Writer {
let tensors = self
.tensors
.into_iter()
.map(|tensor| {
burn_pack::Tensor::new(
tensor.name,
tensor.data.dtype,
tensor.data.shape,
tensor.param_id,
tensor.data.bytes,
)
})
.collect();
let mut writer = Writer::new(tensors);
for (key, value) in &self.scalars {
writer = writer.with_scalar(key, *value);
}
for (key, value) in &self.paths {
writer = writer.with_metadata(key, value);
}
writer
}
fn from_reader(reader: Reader) -> Result<Self, RecordError> {
let scalars = reader.scalars().clone();
let paths = reader.metadata().clone();
let tensors = reader
.into_tensors()?
.into_iter()
.map(|tensor| {
let (name, dtype, shape, param_id, bytes) = tensor.into_parts()?;
Ok(RecordTensor {
name,
param_id,
data: TensorData::from_bytes(bytes, shape, dtype),
})
})
.collect::<Result<Vec<_>, RecordError>>()?;
Ok(Self {
tensors,
scalars,
paths,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::tensor::TensorData;
fn record(data: TensorData) -> OptimizerRecord {
OptimizerRecord {
tensors: alloc::vec![RecordTensor {
name: "42.moment".into(),
param_id: Some(42),
data,
}],
scalars: Default::default(),
paths: Default::default(),
}
}
fn assert_record(decoded: &OptimizerRecord, expected: &TensorData) {
let tensor = &decoded.tensors[0];
assert_eq!(tensor.name, "42.moment");
assert_eq!(tensor.param_id, Some(42));
assert_eq!(tensor.data.dtype, expected.dtype);
assert_eq!(tensor.data.shape, expected.shape);
assert_eq!(&tensor.data.bytes[..], &expected.bytes[..]);
}
#[test]
fn tensor_data_stays_internal_across_burnpack_boundary() {
let expected = TensorData::from([1.0_f32, 2.0, 3.0]);
let decoded =
OptimizerRecord::from_bytes(record(expected.clone()).into_bytes().unwrap()).unwrap();
assert_record(&decoded, &expected);
}
#[cfg(feature = "std")]
#[test]
fn tensor_data_round_trips_through_burnpack_file() {
let expected = TensorData::from([1.0_f32, 2.0, 3.0]);
let path = std::env::temp_dir().join(format!(
"burn_optim_record_{}_tensor_data.bpk",
std::process::id()
));
record(expected.clone()).save(&path).unwrap();
let decoded = OptimizerRecord::load(&path).unwrap();
assert_record(&decoded, &expected);
drop(decoded);
std::fs::remove_file(path).unwrap();
}
}