use std::collections::BTreeMap;
use eredu_checkpoint::{
recipe::{AtomicRecipeSet, RecipeCatalog, RecipeError},
store::{CheckpointSource, ReadPolicy, StoreError, TensorReadRequest},
};
use eredu_nn::{
ParameterId, ParameterMetadata, ParameterVisitor, ParameterVisitorMut, Parameterized,
};
use crate::{ParameterBackend, ResidencyDeclarationError, WeightBinding, WeightBindingPlan};
pub fn bindings_from_recipe_set<C: RecipeCatalog + ?Sized>(
catalog: &C,
set: AtomicRecipeSet,
) -> Result<Vec<WeightBinding>, RecipeBindingError> {
let (outputs, aliases) = set.into_parts();
let mut bytes = BTreeMap::new();
for (name, recipe) in &outputs {
bytes.insert(name.clone(), recipe.infer(catalog)?.byte_len());
}
let mut bindings = outputs
.into_iter()
.map(|(name, recipe)| {
let expected = bytes[&name];
WeightBinding::from_recipe(name, recipe, expected)
})
.collect::<Result<Vec<_>, _>>()?;
for (alias, owner) in aliases {
bindings.push(WeightBinding::alias(alias, owner.clone(), bytes[&owner])?);
}
WeightBindingPlan::new(&bindings)?;
Ok(bindings)
}
#[derive(Debug, thiserror::Error)]
pub enum RecipeBindingError {
#[error(transparent)]
Recipe(#[from] RecipeError),
#[error(transparent)]
Declaration(#[from] ResidencyDeclarationError),
}
pub struct MaterializedUnit<B: ParameterBackend> {
weights: BTreeMap<ParameterId, B::MaterializedWeight>,
}
impl<B: ParameterBackend> MaterializedUnit<B> {
pub fn len(&self) -> usize {
self.weights.len()
}
pub fn is_empty(&self) -> bool {
self.weights.is_empty()
}
pub fn contains(&self, id: &ParameterId) -> bool {
self.weights.contains_key(id)
}
}
pub fn materialize_bindings<B: ParameterBackend>(
source: &dyn CheckpointSource,
bindings: &[WeightBinding],
context: &B::MaterializationContext,
) -> Result<MaterializedUnit<B>, ParameterOrchestrationError<B::ParameterError>> {
let plan = WeightBindingPlan::new(bindings)?;
for binding in plan.owners() {
let inferred = binding.source_recipe().infer(source)?;
if inferred.byte_len() != binding.expected_bytes() {
return Err(ParameterOrchestrationError::ByteMismatch {
parameter: binding.name().to_owned(),
expected: binding.expected_bytes(),
actual: inferred.byte_len(),
});
}
}
let mut weights = BTreeMap::new();
for binding in plan.owners() {
let materialization = match binding.recipe() {
Some(recipe) => B::materialize_recipe(recipe, source, context),
None => {
let lease = source.acquire_lease(TensorReadRequest {
key: binding.checkpoint_key().to_owned(),
selection: binding.selection().clone(),
policy: ReadPolicy::RequireBounded,
})?;
B::materialize(lease, context)
}
}
.map_err(ParameterOrchestrationError::Backend)?;
let weight = B::finish_materialization(materialization)
.map_err(ParameterOrchestrationError::Backend)?;
let id = ParameterId::new(binding.name()).map_err(|error| {
ParameterOrchestrationError::InvalidParameterIdentity(error.to_string())
})?;
if weights.insert(id.clone(), weight).is_some() {
return Err(ParameterOrchestrationError::DuplicateBinding { parameter: id });
}
}
for (alias, owner) in plan.aliases() {
let owner_id = ParameterId::new(owner.name()).map_err(|error| {
ParameterOrchestrationError::InvalidParameterIdentity(error.to_string())
})?;
let weight = weights
.get(&owner_id)
.expect("validated owner was materialized before aliases");
let weight =
B::share_materialized_weight(weight).map_err(ParameterOrchestrationError::Backend)?;
let alias_id = ParameterId::new(alias.name()).map_err(|error| {
ParameterOrchestrationError::InvalidParameterIdentity(error.to_string())
})?;
weights.insert(alias_id, weight);
}
Ok(MaterializedUnit { weights })
}
pub fn bind_materialized_unit<B, M>(
module: &mut M,
mut unit: MaterializedUnit<B>,
) -> Result<(), ParameterOrchestrationError<B::ParameterError>>
where
B: ParameterBackend,
M: Parameterized<B::Parameter>,
{
struct Validator<'a, B: ParameterBackend> {
weights: &'a BTreeMap<ParameterId, B::MaterializedWeight>,
visited: BTreeMap<ParameterId, ()>,
error: Option<ParameterOrchestrationError<B::ParameterError>>,
}
impl<'a, 'value, B: ParameterBackend> ParameterVisitor<'value, B::Parameter> for Validator<'a, B> {
fn visit(&mut self, metadata: ParameterMetadata, parameter: &'value B::Parameter) {
if self.error.is_some() {
return;
}
let Some(weight) = self.weights.get(&metadata.id) else {
self.error = Some(ParameterOrchestrationError::MissingBinding {
parameter: metadata.id,
});
return;
};
if let Err(error) = B::validate_bind(parameter, weight) {
self.error = Some(ParameterOrchestrationError::Backend(error));
return;
}
self.visited.insert(metadata.id, ());
}
}
let mut validator = Validator::<B> {
weights: &unit.weights,
visited: BTreeMap::new(),
error: None,
};
module.visit_parameters(&mut validator);
if let Some(error) = validator.error {
return Err(error);
}
let unexpected = unit
.weights
.keys()
.filter(|id| !validator.visited.contains_key(*id))
.cloned()
.collect::<Vec<_>>();
if !unexpected.is_empty() {
return Err(ParameterOrchestrationError::UnexpectedBindings {
parameters: unexpected,
});
}
struct Binder<'a, B: ParameterBackend> {
weights: &'a mut BTreeMap<ParameterId, B::MaterializedWeight>,
error: Option<ParameterOrchestrationError<B::ParameterError>>,
}
impl<'a, 'value, B: ParameterBackend> ParameterVisitorMut<'value, B::Parameter> for Binder<'a, B> {
fn visit_mut(&mut self, metadata: ParameterMetadata, parameter: &'value mut B::Parameter) {
if self.error.is_some() {
return;
}
let Some(weight) = self.weights.remove(&metadata.id) else {
self.error = Some(ParameterOrchestrationError::MissingBinding {
parameter: metadata.id,
});
return;
};
if let Err(error) = B::bind(parameter, weight) {
self.error = Some(ParameterOrchestrationError::Backend(error));
}
}
}
let mut binder = Binder::<B> {
weights: &mut unit.weights,
error: None,
};
module.visit_parameters_mut(&mut binder);
if let Some(error) = binder.error {
return Err(error);
}
if !unit.weights.is_empty() {
return Err(ParameterOrchestrationError::UnexpectedBindings {
parameters: unit.weights.into_keys().collect(),
});
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ParameterOrchestrationError<E>
where
E: std::error::Error + Send + Sync + 'static,
{
#[error(transparent)]
Declaration(#[from] ResidencyDeclarationError),
#[error(transparent)]
Store(#[from] StoreError),
#[error(transparent)]
Recipe(#[from] RecipeError),
#[error("invalid runtime parameter identity: {0}")]
InvalidParameterIdentity(String),
#[error("duplicate materialized binding for parameter {parameter}")]
DuplicateBinding {
parameter: ParameterId,
},
#[error("parameter {parameter:?} declares {expected} bytes but its recipe produces {actual}")]
ByteMismatch {
parameter: String,
expected: u64,
actual: u64,
},
#[error("materialized unit has no value for parameter {parameter}")]
MissingBinding {
parameter: ParameterId,
},
#[error("materialized unit contains values for unknown parameters: {parameters:?}")]
UnexpectedBindings {
parameters: Vec<ParameterId>,
},
#[error("backend parameter operation failed: {0}")]
Backend(E),
}