use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::layer_path::{LayerPath, SublayerName};
use crate::neural_network::traits::{LayerBase, ParamId};
use ahash::AHashMap;
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
pub type LayerId = usize;
pub type CallId = usize;
type Slot = Box<dyn Any + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Frame {
name: SublayerName,
call: usize,
}
pub(crate) type LayerIdentity = (usize, TypeId);
pub(crate) fn identity_of(layer: &dyn LayerBase) -> LayerIdentity {
(
layer as *const dyn LayerBase as *const () as usize,
(layer as &dyn Any).type_id(),
)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CacheKey {
call: CallId,
frames: Vec<Frame>,
}
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()
}
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 `{id}` has shape {:?}, and the gradient \
already in the store has shape {:?}",
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<(LayerPath, &'static str), Slot>,
path: LayerPath,
}
impl StateSlot<'_> {
pub fn take<T: Any + Send + Sync>(&mut self, name: &'static str) -> Option<T> {
let key = (self.path.clone(), name);
let slot = self.states.remove(&key)?;
match slot.downcast::<T>() {
Ok(value) => Some(*value),
Err(slot) => {
self.states.insert(key, slot);
None
}
}
}
}
struct FrameGuard<'a> {
ctx: &'a mut Ctx,
depth: usize,
}
impl Drop for FrameGuard<'_> {
fn drop(&mut self) {
self.ctx.frames.truncate(self.depth);
}
}
#[derive(Default)]
pub struct Ctx {
training: bool,
owner: LayerId,
call: CallId,
frames: Vec<Frame>,
sublayer_calls: AHashMap<LayerPath, (LayerIdentity, String)>,
sublayer_conflicts: Vec<String>,
caches: AHashMap<CacheKey, Vec<CacheSlot>>,
states: AHashMap<(LayerPath, &'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
}
pub fn layer_path(&self) -> LayerPath {
LayerPath::from_parts(
self.owner,
self.frames.iter().map(|frame| frame.name.clone()).collect(),
)
}
pub fn sublayer<R>(
&mut self,
name: impl Into<SublayerName>,
layer: &dyn LayerBase,
body: impl FnOnce(&mut Ctx) -> R,
) -> R {
self.sublayer_call(name, 0, layer, body)
}
pub fn sublayer_call<R>(
&mut self,
name: impl Into<SublayerName>,
call: usize,
layer: &dyn LayerBase,
body: impl FnOnce(&mut Ctx) -> R,
) -> R {
let depth = self.frames.len();
self.frames.push(Frame {
name: name.into(),
call,
});
if self.training {
self.record_sublayer_call(layer);
}
let guard = FrameGuard { ctx: self, depth };
body(guard.ctx)
}
fn record_sublayer_call(&mut self, layer: &dyn LayerBase) {
let path = self.layer_path();
let identity = identity_of(layer);
match self.sublayer_calls.get(&path) {
Some((seen, first_type)) if *seen != identity => {
let conflict = format!(
"`{path}` with a layer `{first_type}` and with another layer `{}`",
layer.layer_type()
);
self.sublayer_conflicts.push(conflict);
}
Some(_) => {}
None => {
self.sublayer_calls
.insert(path, (identity, layer.layer_type().to_string()));
}
}
}
pub(crate) fn sublayer_calls(
&self,
) -> (&AHashMap<LayerPath, (LayerIdentity, String)>, &[String]) {
(&self.sublayer_calls, &self.sublayer_conflicts)
}
fn cache_key(&self) -> CacheKey {
CacheKey {
call: self.call,
frames: self.frames.clone(),
}
}
#[inline]
pub fn call(&self) -> CallId {
self.call
}
#[inline]
pub fn set_owner(&mut self, owner: LayerId) -> LayerId {
assert!(
self.frames.is_empty(),
"`Ctx::set_owner` ran inside `Ctx::sublayer`"
);
self.call = owner;
std::mem::replace(&mut self.owner, owner)
}
#[inline]
pub fn set_position(&mut self, owner: LayerId, call: CallId) {
assert!(
self.frames.is_empty(),
"`Ctx::set_position` ran inside `Ctx::sublayer`"
);
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"
);
let key = self.cache_key();
self.caches.entry(key).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 key = self.cache_key();
let stack = self
.caches
.get_mut(&key)
.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 {} at path `{}` came from layer `{found}`, and layer \
`{layer}` asked for it. Give each layer its own position with \
`Ctx::set_position`, and call each sublayer inside `Ctx::sublayer`",
self.call,
self.layer_path()
)));
}
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.layer_path(), name))?
.downcast_ref::<T>()
}
pub fn take_state<T: Any + Send + Sync>(&mut self, name: &'static str) -> Option<T> {
StateSlot {
path: self.layer_path(),
states: &mut self.states,
}
.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.layer_path(), name), Box::new(value));
}
pub fn pending_states(&self) -> usize {
self.states.len()
}
pub fn has_state(&self, path: &LayerPath) -> bool {
self.states.keys().any(|(layer, _)| layer == path)
}
pub fn state_left_under(&self, scope: LayerId) -> Vec<String> {
let mut left: Vec<String> = self
.states
.keys()
.filter(|(layer, _)| layer.scope() == scope)
.map(|(layer, name)| format!("{layer}.{name}"))
.collect();
left.sort();
left
}
pub fn state_left_below(&self, path: &LayerPath) -> Vec<String> {
let mut left: Vec<String> = self
.states
.keys()
.filter(|(layer, _)| layer.starts_with(path))
.map(|(layer, name)| format!("{layer}.{name}"))
.collect();
left.sort();
left
}
pub fn state_slot(&mut self, path: &LayerPath) -> StateSlot<'_> {
StateSlot {
states: &mut self.states,
path: path.clone(),
}
}
pub fn add_grad(&mut self, name: &'static str, grad: Tensor) -> Result<(), Error> {
self.grads.add(ParamId::at(self.layer_path(), 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("path", &self.layer_path().to_string())
.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");
}
}