use crate::error::{Error, IoError};
use crate::neural_network::Shape;
use crate::neural_network::layer_path::{LayerPath, walk, weight_path};
use crate::neural_network::traits::{Layer, LayerBase, WeightKind};
use crate::{Deserialize, Serialize};
use ndarray::ArrayViewMutD;
use std::borrow::Cow;
pub const MODEL_MAGIC: u32 = 0x524D_4C4D;
pub const MODEL_FORMAT_VERSION: u32 = 4;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildConfig {
pub input_shapes: Vec<Shape>,
}
impl BuildConfig {
#[inline]
pub fn new(inputs: &[Shape]) -> Self {
Self {
input_shapes: inputs.iter().map(Shape::free_batch).collect(),
}
}
#[inline]
pub fn unary(input: &Shape) -> Self {
Self {
input_shapes: vec![input.free_batch()],
}
}
fn describe(&self) -> String {
self.input_shapes
.iter()
.map(Shape::to_string)
.collect::<Vec<_>>()
.join(", ")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeightRecord<'a> {
pub name: Cow<'a, str>,
pub kind: WeightKind,
pub shape: Vec<usize>,
pub data: Cow<'a, [f32]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayerCheckpoint<'a> {
pub layer_type: Cow<'a, str>,
pub build: Option<BuildConfig>,
pub weights: Vec<WeightRecord<'a>>,
pub sublayers: Vec<SublayerCheckpoint<'a>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SublayerCheckpoint<'a> {
pub name: Cow<'a, str>,
pub layer: LayerCheckpoint<'a>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCheckpoint<'a> {
pub magic: u32,
pub format_version: u32,
pub layers: Vec<LayerCheckpoint<'a>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LoadReport {
pub applied: Vec<String>,
pub missing: Vec<String>,
pub unused: Vec<String>,
}
pub fn capture(layers: &[Box<dyn Layer>]) -> ModelCheckpoint<'_> {
ModelCheckpoint {
magic: MODEL_MAGIC,
format_version: MODEL_FORMAT_VERSION,
layers: layers.iter().map(|layer| capture_layer(&**layer)).collect(),
}
}
fn capture_layer(layer: &dyn LayerBase) -> LayerCheckpoint<'_> {
LayerCheckpoint {
layer_type: Cow::Borrowed(layer.layer_type()),
build: layer.build_config(),
weights: layer
.weights()
.into_iter()
.map(|entry| WeightRecord {
name: Cow::Borrowed(entry.name),
kind: entry.kind,
shape: entry.value.shape().to_vec(),
data: match entry.value.to_slice() {
Some(run) => Cow::Borrowed(run),
None => Cow::Owned(entry.value.iter().copied().collect()),
},
})
.collect(),
sublayers: layer
.sublayers()
.into_iter()
.map(|sub| SublayerCheckpoint {
name: sub.name,
layer: capture_layer(sub.layer),
})
.collect(),
}
}
pub fn apply(layers: &mut [Box<dyn Layer>], file: &ModelCheckpoint<'_>) -> Result<(), Error> {
if file.layers.len() != layers.len() {
return Err(mismatch(format!(
"layer count mismatch: model has {} layers, file has {} layers",
layers.len(),
file.layers.len()
)));
}
for (scope, (layer, saved)) in layers.iter().zip(file.layers.iter()).enumerate() {
check_layer(&LayerPath::root(scope), &**layer, saved)?;
}
for (layer, saved) in layers.iter_mut().zip(file.layers.iter()) {
write_layer(&mut **layer, saved);
}
Ok(())
}
fn check_layer(
path: &LayerPath,
layer: &dyn LayerBase,
saved: &LayerCheckpoint<'_>,
) -> Result<(), Error> {
let layer_type = layer.layer_type();
if layer_type != saved.layer_type {
return Err(mismatch(format!(
"layer `{path}` type mismatch: model has `{layer_type}`, file has `{}`",
saved.layer_type
)));
}
if let (Some(wanted), Some(found)) = (layer.build_config(), saved.build.as_ref())
&& wanted != *found
{
return Err(mismatch(format!(
"layer `{path}` (`{layer_type}`) was built for input shape {}, and the file records \
{}",
wanted.describe(),
found.describe()
)));
}
let targets = layer.weights();
if targets.len() != saved.weights.len() {
return Err(mismatch(format!(
"layer `{path}` (`{layer_type}`) holds the arrays {:?}, and the file holds {:?}",
targets.iter().map(|e| e.name).collect::<Vec<_>>(),
saved.weights.iter().map(|r| &*r.name).collect::<Vec<_>>()
)));
}
for (target, record) in targets.iter().zip(saved.weights.iter()) {
let at = weight_path(path, target.name);
if target.name != record.name {
return Err(mismatch(format!(
"the model holds `{at}` where the file holds `{}`",
weight_path(path, &record.name)
)));
}
if target.kind != record.kind {
return Err(mismatch(format!(
"`{at}` is {} in the model, and {} in the file",
kind_word(target.kind),
kind_word(record.kind)
)));
}
if target.value.shape() != record.shape.as_slice() {
return Err(mismatch(format!(
"`{at}` has shape {:?} in the model, and {:?} in the file",
target.value.shape(),
record.shape
)));
}
if record.data.len() != target.value.len() {
return Err(mismatch(format!(
"`{at}` holds {} elements at shape {:?}, and the file record carries {}",
target.value.len(),
record.shape,
record.data.len()
)));
}
}
let subs = layer.sublayers();
let model_names: Vec<&str> = subs.iter().map(|sub| &*sub.name).collect();
let file_names: Vec<&str> = saved.sublayers.iter().map(|sub| &*sub.name).collect();
if model_names != file_names {
return Err(mismatch(format!(
"layer `{path}` (`{layer_type}`) holds the sublayers {model_names:?}, and the file \
holds {file_names:?}"
)));
}
for (sub, record) in subs.iter().zip(saved.sublayers.iter()) {
check_layer(&path.child(sub.name.clone()), sub.layer, &record.layer)?;
}
Ok(())
}
fn write_layer(layer: &mut dyn LayerBase, saved: &LayerCheckpoint<'_>) {
for (target, record) in layer.weights_mut().iter_mut().zip(saved.weights.iter()) {
write_record(&mut target.value, &record.data);
}
for (sub, record) in layer
.sublayers_mut()
.into_iter()
.zip(saved.sublayers.iter())
{
write_layer(sub.layer, &record.layer);
}
}
pub fn apply_partial(layers: &mut [Box<dyn Layer>], file: &ModelCheckpoint<'_>) -> LoadReport {
let mut report = LoadReport::default();
for (scope, layer) in layers.iter_mut().enumerate() {
partial_layer(
&LayerPath::root(scope),
&mut **layer,
file.layers.get(scope),
&mut report,
);
}
for (scope, saved) in file.layers.iter().enumerate().skip(layers.len()) {
file_paths(&LayerPath::root(scope), saved, &mut report.unused);
}
report
}
fn partial_layer(
path: &LayerPath,
layer: &mut dyn LayerBase,
at_path: Option<&LayerCheckpoint<'_>>,
report: &mut LoadReport,
) {
let saved = at_path.filter(|saved| saved.layer_type == layer.layer_type());
let Some(saved) = saved else {
model_paths(path, layer, &mut report.missing);
if let Some(other) = at_path {
file_paths(path, other, &mut report.unused);
}
return;
};
if let (Some(wanted), Some(found)) = (layer.build_config(), saved.build.as_ref())
&& wanted != *found
{
model_paths(path, layer, &mut report.missing);
file_paths(path, saved, &mut report.unused);
return;
}
let mut taken = vec![false; saved.weights.len()];
for target in layer.weights_mut().iter_mut() {
let at = weight_path(path, target.name);
let found = saved
.weights
.iter()
.enumerate()
.find(|(index, record)| {
!taken[*index]
&& record.name == target.name
&& record.kind == target.kind
&& record.shape.as_slice() == target.value.shape()
&& record.data.len() == target.value.len()
})
.map(|(index, _)| index);
match found {
Some(index) => {
taken[index] = true;
write_record(&mut target.value, &saved.weights[index].data);
report.applied.push(at);
}
None => report.missing.push(at),
}
}
report.unused.extend(
saved
.weights
.iter()
.zip(taken.iter())
.filter(|(_, used)| !**used)
.map(|(record, _)| weight_path(path, &record.name)),
);
let mut sub_taken = vec![false; saved.sublayers.len()];
for sub in layer.sublayers_mut() {
let sub_path = path.child(sub.name.clone());
let found = saved
.sublayers
.iter()
.enumerate()
.find(|(index, record)| !sub_taken[*index] && record.name == sub.name)
.map(|(index, _)| index);
if let Some(index) = found {
sub_taken[index] = true;
}
partial_layer(
&sub_path,
sub.layer,
found.map(|index| &saved.sublayers[index].layer),
report,
);
}
for (record, used) in saved.sublayers.iter().zip(sub_taken.iter()) {
if !*used {
file_paths(
&path.child(record.name.to_string()),
&record.layer,
&mut report.unused,
);
}
}
}
fn model_paths(path: &LayerPath, layer: &dyn LayerBase, into: &mut Vec<String>) {
walk(layer, path, &mut |node_path, node| {
into.extend(
node.weights()
.iter()
.map(|e| weight_path(node_path, e.name)),
);
});
}
fn file_paths(path: &LayerPath, saved: &LayerCheckpoint<'_>, into: &mut Vec<String>) {
into.extend(saved.weights.iter().map(|r| weight_path(path, &r.name)));
for sub in &saved.sublayers {
file_paths(&path.child(sub.name.to_string()), &sub.layer, into);
}
}
fn write_record(target: &mut ArrayViewMutD<'_, f32>, data: &[f32]) {
match target.as_slice_mut() {
Some(run) => run.copy_from_slice(data),
None => {
for (slot, value) in target.iter_mut().zip(data.iter()) {
*slot = *value;
}
}
}
}
fn kind_word(kind: WeightKind) -> &'static str {
match kind {
WeightKind::Trainable => "trainable",
WeightKind::NonTrainable => "non-trainable",
}
}
#[cold]
fn mismatch(message: String) -> Error {
Error::Io(IoError::ModelStructureMismatch(message))
}