use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::traits::ParamId;
use ahash::AHashMap;
use std::any::Any;
use std::collections::BTreeMap;
pub type LayerId = usize;
pub type CallId = usize;
type Slot = Box<dyn Any + Send + Sync>;
struct CacheSlot {
layer: &'static str,
value: Slot,
}
#[derive(Debug, Default)]
pub struct Grads {
map: BTreeMap<ParamId, Tensor>,
}
impl Grads {
#[inline]
pub fn get(&self, id: ParamId) -> Option<&Tensor> {
self.map.get(&id)
}
#[inline]
pub fn len(&self) -> usize {
self.map.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (ParamId, &Tensor)> {
self.map.iter().map(|(id, grad)| (*id, grad))
}
fn add(&mut self, id: ParamId, grad: Tensor) -> Result<(), Error> {
match self.map.get_mut(&id) {
Some(total) => {
if total.shape() != grad.shape() {
return Err(Error::invalid_input(format!(
"the gradient of parameter `{}` of layer {} has shape {:?}, and the \
gradient already in the store has shape {:?}",
id.name,
id.scope,
grad.shape(),
total.shape()
)));
}
*total += &grad;
Ok(())
}
None => {
self.map.insert(id, standard(grad));
Ok(())
}
}
}
}
fn standard(grad: Tensor) -> Tensor {
if grad.is_standard_layout() {
grad
} else {
grad.as_standard_layout().into_owned()
}
}
pub struct StateSlot<'a> {
states: &'a mut AHashMap<(LayerId, &'static str), Slot>,
owner: LayerId,
}
impl StateSlot<'_> {
pub fn take<T: Any + Send + Sync>(&mut self, name: &'static str) -> Option<T> {
let slot = self.states.remove(&(self.owner, name))?;
match slot.downcast::<T>() {
Ok(value) => Some(*value),
Err(slot) => {
self.states.insert((self.owner, name), slot);
None
}
}
}
}
#[derive(Default)]
pub struct Ctx {
training: bool,
owner: LayerId,
call: CallId,
caches: AHashMap<CallId, Vec<CacheSlot>>,
states: AHashMap<(LayerId, &'static str), Slot>,
grads: Grads,
}
impl Ctx {
pub fn training() -> Self {
Self {
training: true,
..Self::default()
}
}
pub fn inference() -> Self {
Self::default()
}
#[inline]
pub fn is_training(&self) -> bool {
self.training
}
#[inline]
pub fn owner(&self) -> LayerId {
self.owner
}
#[inline]
pub fn call(&self) -> CallId {
self.call
}
#[inline]
pub fn set_owner(&mut self, owner: LayerId) -> LayerId {
self.call = owner;
std::mem::replace(&mut self.owner, owner)
}
#[inline]
pub fn set_position(&mut self, owner: LayerId, call: CallId) {
self.owner = owner;
self.call = call;
}
pub fn push_cache<T: Any + Send + Sync>(&mut self, layer: &'static str, cache: T) {
debug_assert!(
self.training,
"a layer must write no cache in an inference pass"
);
self.caches.entry(self.call).or_default().push(CacheSlot {
layer,
value: Box::new(cache),
});
}
pub fn pop_cache<T: Any + Send + Sync>(&mut self, layer: &'static str) -> Result<T, Error> {
let stack = self
.caches
.get_mut(&self.call)
.ok_or_else(|| Error::forward_pass_not_run(layer))?;
let slot = stack
.pop()
.ok_or_else(|| Error::forward_pass_not_run(layer))?;
if slot.layer != layer {
let found = slot.layer;
stack.push(slot);
return Err(Error::computation(format!(
"the cache of call {} came from layer `{found}`, and layer `{layer}` asked for \
it. Give each layer its own position with `Ctx::set_position`",
self.call
)));
}
match slot.value.downcast::<T>() {
Ok(cache) => Ok(*cache),
Err(value) => {
stack.push(CacheSlot { layer, value });
Err(Error::computation(format!(
"layer `{layer}` parked a cache of another type than the one it asked for"
)))
}
}
}
pub fn pending_caches(&self) -> usize {
self.caches.values().map(Vec::len).sum()
}
pub fn state<T: Any + Send + Sync>(&self, name: &'static str) -> Option<&T> {
self.states.get(&(self.owner, name))?.downcast_ref::<T>()
}
pub fn take_state<T: Any + Send + Sync>(&mut self, name: &'static str) -> Option<T> {
StateSlot {
states: &mut self.states,
owner: self.owner,
}
.take(name)
}
pub fn set_state<T: Any + Send + Sync>(&mut self, name: &'static str, value: T) {
debug_assert!(
self.training,
"a layer must change no state in an inference pass"
);
self.states.insert((self.owner, name), Box::new(value));
}
pub fn pending_states(&self) -> usize {
self.states.len()
}
pub fn has_state(&self, owner: LayerId) -> bool {
self.states.keys().any(|(layer, _)| *layer == owner)
}
pub fn state_slot(&mut self, owner: LayerId) -> StateSlot<'_> {
StateSlot {
states: &mut self.states,
owner,
}
}
pub fn add_grad(&mut self, name: &'static str, grad: Tensor) -> Result<(), Error> {
self.grads.add(ParamId::new(self.owner, name), grad)
}
#[inline]
pub fn grads(&self) -> &Grads {
&self.grads
}
pub fn take_grads(&mut self) -> Grads {
std::mem::take(&mut self.grads)
}
}
impl std::fmt::Debug for Ctx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ctx")
.field("training", &self.training)
.field("owner", &self.owner)
.field("call", &self.call)
.field("caches", &self.pending_caches())
.field("states", &self.states.len())
.field("grads", &self.grads.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::neural_network::layers::regularization::dropout::dropout::Dropout;
use crate::neural_network::traits::UnaryLayer;
use crate::neural_network::{Shape, Tensor};
#[test]
fn a_cache_of_another_layer_is_refused() {
let mut ctx = Ctx::training();
ctx.push_cache("Dense", vec![2_usize, 3]);
let taken = ctx.pop_cache::<Vec<usize>>("Flatten");
let message = match taken {
Ok(_) => panic!("the cache of another layer must not come back"),
Err(error) => error.to_string(),
};
assert!(message.contains("Dense"), "{message}");
assert!(message.contains("Flatten"), "{message}");
assert_eq!(ctx.pop_cache::<Vec<usize>>("Dense").unwrap(), vec![2, 3]);
}
#[test]
fn a_cache_of_another_type_is_refused() {
let mut ctx = Ctx::training();
ctx.push_cache("Dense", vec![2_usize, 3]);
let message = match ctx.pop_cache::<Tensor>("Dense") {
Ok(_) => panic!("a cache of another type must not come back"),
Err(error) => error.to_string(),
};
assert!(message.contains("another type"), "{message}");
}
#[test]
fn an_empty_stack_reports_a_missing_forward_pass() {
let mut ctx = Ctx::training();
let message = match ctx.pop_cache::<Tensor>("Dense") {
Ok(_) => panic!("an empty context holds no cache"),
Err(error) => error.to_string(),
};
assert!(
message.contains("forward pass has not been run"),
"{message}"
);
}
#[test]
fn a_hand_driven_pass_advances_the_random_stream() {
let input = Tensor::ones([4, 8].as_slice());
let mut advancing = Dropout::new(0.5).unwrap().with_random_state(7);
let mut ctx = Ctx::training();
let first = advancing.forward_mut(&input, &mut ctx).unwrap();
assert_eq!(ctx.pending_states(), 0, "the state must reach the layer");
let mut ctx = Ctx::training();
let second = advancing.forward_mut(&input, &mut ctx).unwrap();
assert_ne!(first, second, "the random stream did not advance");
let mut pure = Dropout::new(0.5).unwrap().with_random_state(7);
pure.build(&Shape::known(&[4, 8])).unwrap();
let mut ctx = Ctx::training();
let one = pure.forward(&input, &mut ctx).unwrap();
let mut ctx = Ctx::training();
let two = pure.forward(&input, &mut ctx).unwrap();
assert_eq!(one, two, "a pure pass must not move the stream");
assert_eq!(
one, first,
"the first draw of the 2 layers is the same draw"
);
}
#[test]
fn take_grads_empties_the_store() {
let mut ctx = Ctx::training();
ctx.add_grad("kernel", Tensor::ones([2].as_slice()))
.unwrap();
ctx.add_grad("kernel", Tensor::ones([2].as_slice()))
.unwrap();
let taken = ctx.take_grads();
assert_eq!(taken.len(), 1);
assert_eq!(
taken.get(ParamId::new(0, "kernel")).unwrap(),
&Tensor::from_elem([2].as_slice(), 2.0),
"the store sums a gradient that arrives twice"
);
assert!(ctx.grads().is_empty(), "the store is empty after the take");
}
}