use super::base::{
Error, FORMAT_VERSION, HEADER_SIZE, Header, MAGIC_NUMBER, Metadata, Scalar, TENSOR_ALIGNMENT,
TensorDescriptor, aligned_data_section_start, validate_tensor_byte_len,
};
use super::tensor::Tensor;
#[cfg(feature = "std")]
use crate::atomic::AtomicFile;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use burn_std::Bytes;
#[cfg(feature = "std")]
use std::fs::File;
#[cfg(feature = "std")]
use std::io::{Read, Write};
#[cfg(feature = "std")]
use std::path::Path;
#[inline]
const fn align_offset(offset: u64, alignment: u64) -> u64 {
offset.div_ceil(alignment) * alignment
}
const WRITE_CHUNK_SIZE: usize = 8 * 1024 * 1024;
type Descriptors = (BTreeMap<String, TensorDescriptor>, Vec<Placement>, usize);
pub struct Writer {
pub(crate) tensors: Vec<Tensor>,
pub(crate) metadata: BTreeMap<String, String>,
pub(crate) scalars: BTreeMap<String, Scalar>,
#[cfg(feature = "std")]
auto_extension: bool,
#[cfg(feature = "std")]
overwrite: bool,
}
impl Writer {
pub fn new(tensors: Vec<Tensor>) -> Self {
Self {
tensors,
metadata: BTreeMap::new(),
scalars: BTreeMap::new(),
#[cfg(feature = "std")]
auto_extension: true,
#[cfg(feature = "std")]
overwrite: true,
}
}
pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
pub fn with_scalar(mut self, key: &str, value: Scalar) -> Self {
self.scalars.insert(key.to_string(), value);
self
}
#[cfg(feature = "std")]
pub fn auto_extension(mut self, enable: bool) -> Self {
self.auto_extension = enable;
self
}
#[cfg(feature = "std")]
pub fn overwrite(mut self, enable: bool) -> Self {
self.overwrite = enable;
self
}
pub fn size(&self) -> Result<usize, Error> {
Ok(self.plan()?.total_size())
}
pub fn write_into(self, buffer: &mut [u8]) -> Result<(), Error> {
let layout = self.plan()?;
let total_size = layout.total_size();
if buffer.len() < total_size {
return Err(Error::IoError(format!(
"Buffer too small: need {} bytes, got {} bytes",
total_size,
buffer.len()
)));
}
let mut sink = BufferSink { buffer, offset: 0 };
self.write_container(&layout, &mut sink)
}
pub fn into_bytes(self) -> Result<Bytes, Error> {
let layout = self.plan()?;
let mut buffer = vec![0u8; layout.total_size()];
let mut sink = BufferSink {
buffer: &mut buffer,
offset: 0,
};
self.write_container(&layout, &mut sink)?;
Ok(Bytes::from_bytes_vec(buffer))
}
#[cfg(feature = "std")]
pub fn write_to_file<P: AsRef<Path>>(self, path: P) -> Result<(), Error> {
let path = self.resolve_path(path.as_ref());
let layout = self.plan()?;
let overwrite = self.overwrite;
let (scratch, file) = AtomicFile::create(&path)?;
let mut sink = FileSink {
file,
path: scratch.path().to_path_buf(),
};
self.write_container(&layout, &mut sink)?;
scratch.persist(sink, overwrite)
}
#[cfg(feature = "std")]
pub fn write_to_file_in_place<P: AsRef<Path>>(self, path: P) -> Result<(), Error> {
let path = self.resolve_path(path.as_ref());
let layout = self.plan()?;
let file = File::options()
.write(true)
.truncate(true)
.create(self.overwrite)
.create_new(!self.overwrite)
.open(&path)
.map_err(|e| match e.kind() {
std::io::ErrorKind::AlreadyExists => {
Error::AlreadyExists(path.display().to_string())
}
_ => Error::IoError(format!("cannot create '{}': {e}", path.display())),
})?;
let mut sink = FileSink { file, path };
self.write_container(&layout, &mut sink)
}
#[cfg(feature = "std")]
fn resolve_path(&self, path: &Path) -> std::path::PathBuf {
if self.auto_extension && path.extension().is_none() {
path.with_extension(crate::EXTENSION)
} else {
path.to_path_buf()
}
}
fn plan(&self) -> Result<Layout, Error> {
let (tensors, placements, data_size) = self.build_descriptors()?;
let metadata = Metadata {
tensors,
metadata: self.metadata.clone(),
scalars: self.scalars.clone(),
};
let mut metadata_bytes = Vec::new();
ciborium::ser::into_writer(&metadata, &mut metadata_bytes)
.map_err(|e| Error::MetadataSerializationError(e.to_string()))?;
let metadata_size: u32 = metadata_bytes.len().try_into().map_err(|_| {
Error::IoError(format!(
"Metadata size {} exceeds maximum of {} bytes",
metadata_bytes.len(),
u32::MAX
))
})?;
let header = Header {
magic: MAGIC_NUMBER,
version: FORMAT_VERSION,
metadata_size,
};
let data_section_start = aligned_data_section_start(metadata_bytes.len());
Ok(Layout {
metadata_bytes,
placements,
header,
data_section_start,
data_size,
})
}
fn build_descriptors(&self) -> Result<Descriptors, Error> {
let mut tensors = BTreeMap::new();
let mut placements = Vec::with_capacity(self.tensors.len());
let mut current_offset = 0u64;
for tensor in &self.tensors {
let name = &tensor.name;
let dtype = tensor.dtype;
let shape: Vec<u64> = tensor.shape.iter().map(|&size| size as u64).collect();
let data_len = tensor.byte_len() as u64;
validate_tensor_byte_len(name, dtype, &shape, data_len)?;
let aligned_start = align_offset(current_offset, TENSOR_ALIGNMENT);
let end = aligned_start.checked_add(data_len).ok_or_else(|| {
Error::IoError(format!(
"Tensor offset overflow: {} + {} exceeds maximum",
aligned_start, data_len
))
})?;
if tensors
.insert(
name.clone(),
TensorDescriptor {
dtype,
shape,
data_offsets: (aligned_start, end),
param_id: tensor.param_id,
},
)
.is_some()
{
return Err(Error::ValidationError(format!(
"Duplicate tensor name '{}'",
name
)));
}
placements.push(Placement {
name: name.clone(),
offset: aligned_start as usize,
});
current_offset = end;
}
Ok((tensors, placements, current_offset as usize))
}
fn write_container(self, layout: &Layout, sink: &mut impl Sink) -> Result<(), Error> {
sink.write(&layout.header.into_bytes())?;
sink.write(&layout.metadata_bytes)?;
let unaligned_data_start = HEADER_SIZE + layout.metadata_bytes.len();
if layout.data_section_start > unaligned_data_start {
sink.pad(layout.data_section_start - unaligned_data_start)?;
}
self.write_tensors(&layout.placements, sink)
}
fn write_tensors(self, placements: &[Placement], sink: &mut impl Sink) -> Result<(), Error> {
assert_eq!(placements.len(), self.tensors.len());
let mut data_offset = 0usize;
for (tensor, placement) in self.tensors.into_iter().zip(placements) {
if placement.offset > data_offset {
sink.pad(placement.offset - data_offset)?;
data_offset = placement.offset;
}
let data = Self::materialize(tensor, placement)?;
write_tensor_data(&data, sink)?;
data_offset += data.len();
}
Ok(())
}
fn materialize(tensor: Tensor, placement: &Placement) -> Result<Bytes, Error> {
tensor
.into_bytes()
.map_err(|e| e.in_tensor(&placement.name))
}
}
struct Placement {
name: String,
offset: usize,
}
fn write_tensor_data(data: &Bytes, sink: &mut impl Sink) -> Result<(), Error> {
let len = data.len();
let mut offset = 0;
while offset < len {
let end = (offset + WRITE_CHUNK_SIZE).min(len);
match data.view(offset, end) {
Ok(chunk) => {
sink.write(&chunk)?;
offset = end;
}
Err(_) => {
sink.write(&data[offset..])?;
break;
}
}
}
Ok(())
}
struct Layout {
metadata_bytes: Vec<u8>,
placements: Vec<Placement>,
header: Header,
data_section_start: usize,
data_size: usize,
}
impl Layout {
fn total_size(&self) -> usize {
self.data_section_start + self.data_size
}
}
trait Sink {
fn pad(&mut self, count: usize) -> Result<(), Error>;
fn write(&mut self, data: &[u8]) -> Result<(), Error>;
}
struct BufferSink<'a> {
buffer: &'a mut [u8],
offset: usize,
}
impl Sink for BufferSink<'_> {
fn pad(&mut self, count: usize) -> Result<(), Error> {
self.buffer[self.offset..self.offset + count].fill(0);
self.offset += count;
Ok(())
}
fn write(&mut self, data: &[u8]) -> Result<(), Error> {
self.buffer[self.offset..self.offset + data.len()].copy_from_slice(data);
self.offset += data.len();
Ok(())
}
}
#[cfg(feature = "std")]
impl AtomicFile {
fn persist(mut self, sink: FileSink, overwrite: bool) -> Result<(), Error> {
sink.finish()?;
if overwrite {
self.rename_onto()
} else {
self.link_onto()
}
}
}
#[cfg(feature = "std")]
struct FileSink {
file: File,
path: std::path::PathBuf,
}
#[cfg(feature = "std")]
impl FileSink {
fn finish(self) -> Result<(), Error> {
self.file.sync_all().map_err(|e| {
Error::IoError(format!(
"cannot flush '{}' to disk: {e}",
self.path.display()
))
})
}
}
#[cfg(feature = "std")]
impl Sink for FileSink {
fn pad(&mut self, count: usize) -> Result<(), Error> {
std::io::copy(&mut std::io::repeat(0).take(count as u64), &mut self.file)
.map(|_| ())
.map_err(|e| Error::IoError(format!("cannot write to '{}': {e}", self.path.display())))
}
fn write(&mut self, data: &[u8]) -> Result<(), Error> {
self.file
.write_all(data)
.map_err(|e| Error::IoError(format!("cannot write to '{}': {e}", self.path.display())))
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use burn_std::{AllocationProperty, DType};
use std::sync::{Arc, Mutex};
const PAYLOAD: usize = 1024 * 1024;
const COUNT: usize = 4;
type FreedLog = Arc<Mutex<Vec<usize>>>;
type WriteFn = fn(Writer, &Path) -> usize;
struct Tracked {
data: Vec<u8>,
index: usize,
freed: FreedLog,
}
impl AsRef<[u8]> for Tracked {
fn as_ref(&self) -> &[u8] {
&self.data
}
}
impl Drop for Tracked {
fn drop(&mut self) {
self.freed.lock().unwrap().push(self.index);
}
}
fn tracked_tensors(freed: &FreedLog) -> Vec<Tensor> {
(0..COUNT)
.map(|index| {
let freed = freed.clone();
Tensor::deferred(
format!("t{index}"),
DType::U8,
vec![PAYLOAD],
None,
PAYLOAD,
move || {
let already_freed = freed.lock().unwrap().clone();
assert_eq!(
already_freed,
(0..index).collect::<Vec<_>>(),
"tensor t{index} was requested while an earlier tensor's bytes were \
still live"
);
let backing = Tracked {
data: vec![index as u8; PAYLOAD],
index,
freed: freed.clone(),
};
Ok(Bytes::from_shared(
bytes::Bytes::from_owner(backing),
AllocationProperty::Native,
))
},
)
})
.collect()
}
#[test]
fn each_tensor_is_freed_before_the_next_is_produced() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("model.bpk");
let writes: [(&str, WriteFn); 3] = [
("write_to_file", |writer, path| {
writer.write_to_file(path).unwrap();
std::fs::metadata(path).unwrap().len() as usize
}),
("write_to_file_in_place", |writer, path| {
writer.write_to_file_in_place(path).unwrap();
std::fs::metadata(path).unwrap().len() as usize
}),
("into_bytes", |writer, _| writer.into_bytes().unwrap().len()),
];
for (name, write) in writes {
let freed = FreedLog::default();
let written = write(Writer::new(tracked_tensors(&freed)), &path);
assert_eq!(
*freed.lock().unwrap(),
(0..COUNT).collect::<Vec<_>>(),
"{name}: every tensor's bytes should be freed by the end of the write"
);
assert!(
written >= COUNT * PAYLOAD,
"{name}: all tensors should have been written"
);
}
}
}