use super::base::{
Error, FORMAT_VERSION, HEADER_SIZE, Header, MAGIC_NUMBER, Metadata, Scalar, TENSOR_ALIGNMENT,
TensorDescriptor, aligned_data_section_start,
};
use super::tensor::Tensor;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use burn_std::{Bytes, DType, Shape};
#[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>,
}
impl Writer {
pub fn new(tensors: Vec<Tensor>) -> Self {
Self {
tensors,
metadata: BTreeMap::new(),
scalars: BTreeMap::new(),
}
}
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
}
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 file = File::create(&path)
.map_err(|e| Error::IoError(format!("cannot create '{}': {e}", path.display())))?;
let mut sink = FileSink { file, path };
self.write_container(&layout, &mut sink)
}
#[cfg(feature = "std")]
pub fn write_to_file_atomic<P: AsRef<Path>>(self, path: P) -> Result<(), Error> {
let path = Self::resolve_path(path.as_ref());
let layout = self.plan()?;
let (scratch, mut sink) = ScratchFile::create(&path)?;
self.write_container(&layout, &mut sink)?;
scratch.persist(sink, &path)
}
#[cfg(feature = "std")]
fn resolve_path(path: &Path) -> std::path::PathBuf {
if 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 = &tensor.shape;
let data_len = tensor.byte_len() as u64;
Self::check_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: shape.iter().map(|&s| s as u64).collect(),
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 check_byte_len(name: &str, dtype: DType, shape: &Shape, data_len: u64) -> Result<(), Error> {
if matches!(dtype, DType::QFloat(_)) {
return Ok(());
}
let expected = shape.num_elements() as u64 * dtype.size() as u64;
if data_len != expected {
return Err(Error::ValidationError(format!(
"tensor '{}' declares {} bytes but its shape {} and dtype {:?} need {}",
name, data_len, shape, dtype, expected
)));
}
Ok(())
}
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")]
struct ScratchFile {
path: std::path::PathBuf,
persisted: bool,
}
#[cfg(feature = "std")]
impl ScratchFile {
fn create(destination: &Path) -> Result<(Self, FileSink), Error> {
loop {
let path = Self::path_beside(destination)?;
match File::create_new(&path) {
Ok(file) => {
let sink = FileSink {
file,
path: path.clone(),
};
return Ok((
Self {
path,
persisted: false,
},
sink,
));
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => {
return Err(Error::IoError(format!(
"cannot create '{}': {e}",
path.display()
)));
}
}
}
}
fn path_beside(destination: &Path) -> Result<std::path::PathBuf, Error> {
use core::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
let name = destination.file_name().ok_or_else(|| {
Error::IoError(format!(
"cannot write to '{}': not a file path",
destination.display()
))
})?;
let mut scratch = name.to_os_string();
scratch.push(format!(
".{}-{}.tmp",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
Ok(destination.with_file_name(scratch))
}
fn persist(mut self, sink: FileSink, destination: &Path) -> Result<(), Error> {
sink.finish()?;
std::fs::rename(&self.path, destination).map_err(|e| {
Error::IoError(format!(
"cannot move the completed container '{}' onto '{}': {e}",
self.path.display(),
destination.display()
))
})?;
self.persisted = true;
#[cfg(unix)]
{
let parent = match destination.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent,
_ => Path::new("."),
};
File::open(parent)
.and_then(|directory| directory.sync_all())
.map_err(|e| {
Error::IoError(format!(
"'{}' was saved, but syncing its directory failed, so the rename may \
not survive power loss: {e}",
destination.display()
))
})?;
}
Ok(())
}
}
#[cfg(feature = "std")]
impl Drop for ScratchFile {
fn drop(&mut self) {
if !self.persisted {
let _ = std::fs::remove_file(&self.path);
}
}
}
#[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())))
}
}