Skip to main content

candela/tensor/skeleton/
frame.rs

1use std::collections::HashMap;
2use std::iter::zip;
3use std::sync::Arc;
4
5use crate::tensor::backend::{Backend, ComputeFor, DefaultBackend};
6use crate::tensor::executor::{owned_step, run_plan};
7use crate::tensor::graph::{NodeKind, TensorGraphBaked, TensorGraphNode, TensorGraphSlot};
8use crate::tensor::planner::{
9    OutputKind, OwnedComputeKind, OwnedCorePlan, core_plan_computation, from_borrowed_core_to_owned,
10};
11use crate::tensor::storage::TensorData;
12use crate::tensor::traits::{Composable, Numeric, Operand, Promising};
13use crate::{Dimension, Layout, OpError, Tensor, TensorPromise};
14
15/// The input slots of a [`Skeleton`].
16///
17/// Represents the inputs of a [`Skeleton`] and prevents constructing graphs that
18/// cannot be safely materialized. A slot supports all the operations of a
19/// [`Tensor`], but produces a [`SkeletonPromise`] instead of a [`TensorPromise`];
20/// that promise is then baked - akin to `.materialize()` - through
21/// [`into_skeleton`].
22///
23/// [`into_skeleton`]: SkeletonPromise::into_skeleton
24/// [`TensorPromise`]: crate::TensorPromise
25///
26/// # Examples
27///
28/// ```
29/// use candela::skeleton::SkeletonSlot;
30/// use candela::Tensor;
31///
32/// // A slot stands in for a [4] input; the graph is planned once here...
33/// let slot = SkeletonSlot::from_shape(&[4]);
34/// let skeleton = (&slot * 2.0 + 1.0).into_skeleton(&[slot])?;
35///
36/// // ...then run against as many real tensors as you like.
37/// let out = skeleton.run(&[&Tensor::from_slice(&[0.0, 1.0, 2.0, 3.0], &[4])])?;
38/// assert_eq!(out.data(), &[1.0, 3.0, 5.0, 7.0]);
39/// # Ok::<(), candela::OpError>(())
40/// ```
41pub struct SkeletonSlot<T, B: Backend = DefaultBackend> {
42    pub(crate) graph: Arc<TensorGraphSlot<T, B>>,
43}
44
45impl<T, B: Backend> std::fmt::Debug for SkeletonSlot<T, B> {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("SkeletonSlot")
48            .field("layout", self.graph.layout())
49            .finish()
50    }
51}
52
53impl<T, B: Backend> SkeletonSlot<T, B> {
54    /// Creates a new input slot with the given [`Layout`].
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use candela::skeleton::SkeletonSlot;
60    /// use candela::{Dimension, Layout};
61    ///
62    /// let slot: SkeletonSlot<f64> = SkeletonSlot::new(Layout::new(&[2, 3]));
63    /// assert_eq!(slot.shape(), &[2, 3]);
64    /// ```
65    #[inline]
66    pub fn new(layout: Layout) -> Self {
67        Self {
68            graph: Arc::new(TensorGraphSlot::new(layout)),
69        }
70    }
71
72    /// A deep clone of a Slot
73    ///
74    /// Equivalent to creating a new slot with the same layout as the old one.
75    /// If you just need to reuse the slot use [`clone`] instead.
76    ///
77    /// [`clone`]: SkeletonSlot::clone
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use candela::skeleton::SkeletonSlot;
83    /// use candela::Dimension;
84    ///
85    /// let a: SkeletonSlot<f64> = SkeletonSlot::from_shape(&[4]);
86    /// let b = a.deep_clone(); // independent slot, same layout
87    /// assert_eq!(a.shape(), b.shape());
88    /// ```
89    #[inline]
90    pub fn deep_clone(&self) -> Self {
91        Self::new(self.graph.layout().clone())
92    }
93}
94
95impl<T> SkeletonSlot<T, DefaultBackend> {
96    /// Creates a new contiguous input slot with the given shape.
97    ///
98    /// A shorthand for `SkeletonSlot::new(Layout::new(shape))`.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use candela::skeleton::SkeletonSlot;
104    /// use candela::Dimension;
105    ///
106    /// let slot: SkeletonSlot<f64> = SkeletonSlot::from_shape(&[2, 3]);
107    /// assert_eq!(slot.shape(), &[2, 3]);
108    /// assert!(slot.is_contiguous());
109    /// ```
110    #[inline]
111    pub fn from_shape(shape: &[usize]) -> Self {
112        SkeletonSlot::new(Layout::new(shape))
113    }
114}
115
116impl<T, B: Backend> Dimension for SkeletonSlot<T, B> {
117    fn layout(&self) -> &Layout {
118        self.graph.layout()
119    }
120}
121
122impl<T, B: Backend> Operand<T, B> for SkeletonSlot<T, B> {
123    fn to_node(&self) -> NodeKind<T, B> {
124        NodeKind::Slot(self.graph.clone())
125    }
126}
127
128impl<T, B: Backend> Tainting for SkeletonSlot<T, B> {
129    type Mark = Tainted;
130}
131
132impl<T, B: Backend> Clone for SkeletonSlot<T, B> {
133    /// A shallow clone of a Slot
134    ///
135    /// The copy is equivalent to the slot it was copied from. If you want
136    /// a new slot with the same layout, use [`deep_clone`] instead.
137    ///
138    /// [`deep_clone`]: Self::deep_clone
139    fn clone(&self) -> Self {
140        Self {
141            graph: self.graph.clone(),
142        }
143    }
144}
145
146//////////////////////////////////////////////////////////////////////////////////
147
148/// A pre-baked [`Skeleton`] ready to slot into another graph.
149///
150/// Produced by [`Skeleton::compose`]. It holds the skeleton's plan with its
151/// inputs already bound, so it can be used like a regular promise inside
152/// operations - it is treated as an opaque node during planning. It can only be
153/// materialized through [`to_promise`], because computing it still requires
154/// planning.
155///
156/// [`to_promise`]: BakedPromise::to_promise
157pub struct BakedPromise<T, B: Backend> {
158    graph: Arc<TensorGraphBaked<T, B>>,
159}
160
161impl<T: std::fmt::Debug, B: Backend> std::fmt::Debug for BakedPromise<T, B> {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        std::fmt::Debug::fmt(&self.graph, f)
164    }
165}
166
167impl<T: Clone + PartialEq, B: Backend> BakedPromise<T, B> {
168    fn from_node(
169        plan: &Arc<OwnedCorePlan<T, B>>,
170        inputs: Box<[NodeKind<T, B>]>,
171        inputs_idx: Box<[usize]>,
172        layout: &Layout,
173    ) -> Self {
174        Self {
175            graph: Arc::new(TensorGraphBaked::from_node(
176                plan, inputs, inputs_idx, layout,
177            )),
178        }
179    }
180
181    /// Creates a fresh [`SkeletonSlot`] matching the shape of this promise's output.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use candela::skeleton::SkeletonSlot;
187    /// use candela::{Dimension, Tensor};
188    ///
189    /// let a = Tensor::from_scalar(1.0, &[4]);
190    /// let x = SkeletonSlot::from_shape(&[4]);
191    /// let baked = (&x * 2.0).into_skeleton(&[x])?.compose(&[&a])?;
192    ///
193    /// let slot = baked.to_slot(); // fresh slot shaped like the baked output
194    /// assert_eq!(slot.shape(), &[4]);
195    /// # Ok::<(), candela::OpError>(())
196    /// ```
197    pub fn to_slot(&self) -> SkeletonSlot<T, B> {
198        SkeletonSlot::new(self.layout().clone())
199    }
200}
201
202impl<T: Numeric, B: Backend> BakedPromise<T, B> {
203    /// Wraps the baked computation in a [`TensorPromise`].
204    ///
205    /// Used mainly when the baked output should act like a regular promise
206    /// (e.g. `+=` loops), or simply to materialize it.
207    ///
208    /// # Examples
209    ///
210    /// ```
211    /// use candela::skeleton::SkeletonSlot;
212    /// use candela::{Layout, Tensor};
213    ///
214    /// let base_a = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[4]);
215    /// let base_b = Tensor::from_scalar(10.0, &[4]);
216    ///
217    /// // Two lazy inputs - promises, not materialized tensors.
218    /// let a = &base_a + 1.0;
219    /// let b = &base_b * 2.0;
220    ///
221    /// // Compose `x + y` over the two promises, then materialize the result.
222    /// let x = SkeletonSlot::new(Layout::new(&[4]));
223    /// let y = x.deep_clone();
224    /// let baked = (&x + &y).into_skeleton(&[x, y])?.compose(&[&a, &b])?;
225    ///
226    /// let result = baked.to_promise().materialize();
227    /// assert_eq!(result.data(), &[22.0, 23.0, 24.0, 25.0]);
228    /// # Ok::<(), candela::OpError>(())
229    /// ```
230    pub fn to_promise(&self) -> TensorPromise<T, B> {
231        // The promise can always be unwrapped as it's a noop
232        unsafe {
233            TensorPromise::new(
234                crate::tensor::ops::def_op::OpKind::NoOp,
235                Box::new([NodeKind::Baked(self.graph.clone())]),
236            )
237            .unwrap_unchecked()
238        }
239    }
240}
241
242impl<T, B: Backend> Dimension for BakedPromise<T, B> {
243    fn layout(&self) -> &Layout {
244        self.graph.layout()
245    }
246}
247
248impl<T, B: Backend> Operand<T, B> for BakedPromise<T, B> {
249    fn to_node(&self) -> NodeKind<T, B> {
250        NodeKind::Baked(self.graph.clone())
251    }
252}
253
254impl<T, B: Backend> Tainting for BakedPromise<T, B> {
255    type Mark = Clean;
256}
257
258impl<T, B: Backend> Composable<T, B> for BakedPromise<T, B> {}
259
260//////////////////////////////////////////////////////////////////////////////////
261
262/// A computation with at least one [`SkeletonSlot`] in its lineage.
263///
264/// This is the "tainted" sibling of [`TensorPromise`]: every op that touches a
265/// slot yields one of these instead of a `TensorPromise`, and it deliberately
266/// has no `materialize` - the only way out is [`into_skeleton`], which binds
267/// the slots and compiles the plan.
268///
269/// [`into_skeleton`]: SkeletonPromise::into_skeleton
270pub struct SkeletonPromise<T, B: Backend>(TensorPromise<T, B>);
271
272impl<T: std::fmt::Debug, B: Backend> std::fmt::Debug for SkeletonPromise<T, B> {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        std::fmt::Debug::fmt(&self.0, f)
275    }
276}
277
278impl<T, B: Backend> SkeletonPromise<T, B> {
279    pub(crate) fn from_promise(promise: TensorPromise<T, B>) -> Self {
280        Self(promise)
281    }
282}
283
284impl<T: ComputeFor<B>, B: Backend> SkeletonPromise<T, B> {
285    /// Bakes the recorded computation into a reusable [`Skeleton`].
286    ///
287    /// `slots` must be the list of slots used during the construction of this
288    /// graph. Their order is the order [`Skeleton::run`] and [`Skeleton::compose`]
289    /// expect their inputs.
290    ///
291    /// Planning happens once, during the construction of the skeleton.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// use candela::skeleton::SkeletonSlot;
297    /// use candela::Tensor;
298    ///
299    /// let slot = SkeletonSlot::from_shape(&[4]);
300    /// // Building over the slot yields a SkeletonPromise; into_skeleton compiles it.
301    /// let skeleton = (&slot + 10.0).into_skeleton(&[slot])?;
302    ///
303    /// let out = skeleton.run(&[&Tensor::from_slice(&[0.0, 1.0, 2.0, 3.0], &[4])])?;
304    /// assert_eq!(out.data(), &[10.0, 11.0, 12.0, 13.0]);
305    /// # Ok::<(), candela::OpError>(())
306    /// ```
307    ///
308    /// # Errors
309    ///
310    /// Returns [`OpError::IncorrectSlotAmount`] if the number of `slots` differs
311    /// from the number the computation depends on, or [`OpError::NotSameSlot`] if
312    /// a provided slot was never used while building it.
313    pub fn into_skeleton(self, slots: &[SkeletonSlot<T, B>]) -> Result<Skeleton<T, B>, OpError> {
314        let declared: Vec<(usize, Layout)> = slots
315            .iter()
316            .map(|s| (s.graph.id, s.layout().clone()))
317            .collect();
318
319        Skeleton::from_node(&self.0.graph, declared)
320    }
321}
322
323impl<T, B: Backend> Dimension for SkeletonPromise<T, B> {
324    #[inline]
325    fn layout(&self) -> &Layout {
326        self.0.layout()
327    }
328}
329
330impl<T, B: Backend> Operand<T, B> for SkeletonPromise<T, B> {
331    fn to_node(&self) -> NodeKind<T, B> {
332        self.0.to_node()
333    }
334}
335
336impl<T, B: Backend> Tainting for SkeletonPromise<T, B> {
337    type Mark = Tainted;
338}
339
340//////////////////////////////////////////////////////////////////////////////////
341// Taint algebra
342//
343// Every `Operand` carries a `Mark`: `Clean` for a materializable value,
344// `Tainted` for anything with a slot in its lineage (`SkeletonSlot`,
345// `SkeletonPromise`). An op's output wrapper is the join of its operands'
346// marks - `Tainted` is absorbing - so a slot anywhere in an expression forces a
347// `SkeletonPromise`, which has no `materialize`.
348
349pub struct Clean;
350pub struct Tainted;
351
352/// Taint marker for an operand: `Clean` for a materializable value, `Tainted`
353/// when a [`SkeletonSlot`] is in its lineage.
354pub trait Tainting {
355    type Mark;
356}
357
358/// Join of two marks. `Tainted` absorbs `Clean`.
359pub trait JoinMark<Rhs> {
360    type Out;
361}
362impl JoinMark<Clean> for Clean {
363    type Out = Clean;
364}
365impl JoinMark<Tainted> for Clean {
366    type Out = Tainted;
367}
368impl JoinMark<Clean> for Tainted {
369    type Out = Tainted;
370}
371impl JoinMark<Tainted> for Tainted {
372    type Out = Tainted;
373}
374
375/// Maps a mark to the concrete promise wrapper, with the constructor that turns
376/// the raw graph result an op produces into that wrapper.
377pub trait Wrap<T, B: Backend> {
378    type Output;
379    fn wrap(promise: TensorPromise<T, B>) -> Self::Output;
380}
381impl<T, B: Backend> Wrap<T, B> for Clean {
382    type Output = TensorPromise<T, B>;
383    #[inline]
384    fn wrap(promise: TensorPromise<T, B>) -> TensorPromise<T, B> {
385        promise
386    }
387}
388impl<T, B: Backend> Wrap<T, B> for Tainted {
389    type Output = SkeletonPromise<T, B>;
390    #[inline]
391    fn wrap(promise: TensorPromise<T, B>) -> SkeletonPromise<T, B> {
392        SkeletonPromise::from_promise(promise)
393    }
394}
395
396/// Output of a unary op on a single operand: wrapped by the operand's own mark.
397pub trait UnaryResult<T, B: Backend> {
398    type Output;
399    fn wrap(promise: TensorPromise<T, B>) -> Self::Output;
400}
401impl<L, T, B: Backend> UnaryResult<T, B> for L
402where
403    L: Tainting,
404    L::Mark: Wrap<T, B>,
405{
406    type Output = <L::Mark as Wrap<T, B>>::Output;
407    #[inline]
408    fn wrap(promise: TensorPromise<T, B>) -> Self::Output {
409        <L::Mark as Wrap<T, B>>::wrap(promise)
410    }
411}
412
413/// Output of a binary op on two operands: wrapped by the join of their marks.
414pub trait BinaryResult<Rhs, T, B: Backend> {
415    type Output;
416    fn wrap(promise: TensorPromise<T, B>) -> Self::Output;
417}
418impl<L, R, T, B: Backend> BinaryResult<R, T, B> for L
419where
420    L: Tainting,
421    R: Tainting,
422    L::Mark: JoinMark<R::Mark>,
423    <L::Mark as JoinMark<R::Mark>>::Out: Wrap<T, B>,
424{
425    type Output = <<L::Mark as JoinMark<R::Mark>>::Out as Wrap<T, B>>::Output;
426    #[inline]
427    fn wrap(promise: TensorPromise<T, B>) -> Self::Output {
428        <<L::Mark as JoinMark<R::Mark>>::Out as Wrap<T, B>>::wrap(promise)
429    }
430}
431
432//////////////////////////////////////////////////////////////////////////////////
433
434/// A precompiled execution plan, built once and run many times against new inputs.
435///
436/// # Examples
437///
438/// ```
439/// use candela::Tensor;
440/// use std::error::Error;
441///
442/// // Creates tensors
443/// let a = Tensor::from_scalar(0.3, &[4]);
444/// let b = Tensor::from_scalar(0.3, &[8]);
445///
446/// // Creates a slot for a tensor with the same shape as a
447/// let slot = a.to_slot();
448///
449/// // Create a skeleton with that slot
450/// let skeleton = (&slot * 2.0 + 1.0).log2().into_skeleton(&[slot]).unwrap();
451///
452/// // Running the skeleton
453/// let output_a = skeleton.run(&[&a]);
454///
455/// // Running the skeleton for an invalid shape
456/// let output_b = skeleton.run(&[&b]);
457///
458/// // Check the output is ok
459/// assert!(output_a.is_ok());
460///
461/// // Check the output is an error
462/// assert!(output_b.is_err());
463/// ```
464pub struct Skeleton<T, B: Backend = DefaultBackend> {
465    plan: Arc<OwnedCorePlan<T, B>>,
466    declared_slots: Vec<(usize, Layout)>,
467    layout: Layout,
468}
469
470impl<T, B: Backend> std::fmt::Debug for Skeleton<T, B> {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        f.debug_struct("Skeleton")
473            .field("declared_slots", &self.declared_slots)
474            .field("layout", &self.layout)
475            .finish_non_exhaustive()
476    }
477}
478
479impl<T: Clone + PartialEq + ComputeFor<B>, B: Backend> Skeleton<T, B> {
480    pub(crate) fn from_node(
481        node: &TensorGraphNode<T, B>,
482        declared_slots: Vec<(usize, Layout)>,
483    ) -> Result<Self, OpError> {
484        let plan = core_plan_computation(node);
485
486        if plan.external_inputs.len() != declared_slots.len() {
487            return Err(OpError::IncorrectSlotAmount(
488                plan.external_inputs.len(),
489                declared_slots.len(),
490            ));
491        }
492
493        // Every declared slot must correspond to a slot the plan actually needs.
494        // Match them up by id, removing each as it's found so duplicates are handled
495        // correctly; a declared slot with no match was never used in the graph.
496        let mut external_ids: Vec<usize> = plan.external_inputs.clone();
497
498        for (slot_id, _) in &declared_slots {
499            match external_ids.iter().position(|id| id == slot_id) {
500                Some(pos) => {
501                    external_ids.swap_remove(pos);
502                }
503                None => return Err(OpError::NotSameSlot(*slot_id)),
504            }
505        }
506
507        Ok(Self {
508            plan: Arc::new(from_borrowed_core_to_owned(plan)),
509            declared_slots,
510            layout: node.layout().clone(),
511        })
512    }
513
514    /// Executes the compiled plan against `inputs` and returns the result.
515    ///
516    /// Runs the stored plan on the provided inputs without re-planning. The
517    /// `inputs` must be supplied in the same order they were declared to
518    /// [`into_skeleton`].
519    ///
520    /// [`into_skeleton`]: SkeletonPromise::into_skeleton
521    ///
522    /// # Errors
523    ///
524    /// Returns [`OpError::IncorrectSlotAmount`] if `inputs.len()` differs from
525    /// the number of declared slots, or [`OpError::NotSameLayoutAtSlot`] if an
526    /// input's [`Layout`] does not match the layout its slot was declared with.
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// use candela::skeleton::SkeletonSlot;
532    /// use candela::{Layout, Tensor};
533    ///
534    /// // The same compiled plan, executed against two different inputs.
535    /// let slot = SkeletonSlot::new(Layout::new(&[4]));
536    /// let skeleton = (&slot * 2.0 + 1.0).into_skeleton(std::slice::from_ref(&slot))?;
537    ///
538    /// let a = skeleton.run(&[&Tensor::from_slice(&[0.0, 1.0, 2.0, 3.0], &[4])])?;
539    /// let b = skeleton.run(&[&Tensor::from_scalar(5.0, &[4])])?;
540    /// assert_eq!(a.data(), &[1.0, 3.0, 5.0, 7.0]);
541    /// assert_eq!(b.data(), &[11.0; 4]);
542    /// # Ok::<(), candela::OpError>(())
543    /// ```
544    pub fn run(&self, inputs: &[&Tensor<T, B>]) -> Result<Tensor<T, B>, OpError> {
545        if inputs.len() != self.declared_slots.len() {
546            return Err(OpError::IncorrectSlotAmount(
547                self.declared_slots.len(),
548                inputs.len(),
549            ));
550        }
551
552        for ((i, t), (_, layout)) in zip(inputs.iter().enumerate(), self.declared_slots.iter()) {
553            if t.layout() != layout {
554                return Err(OpError::NotSameLayoutAtSlot(i));
555            }
556        }
557
558        let external: Vec<(usize, TensorData<T>)> = zip(inputs.iter(), self.declared_slots.iter())
559            .map(|(t, (id, _))| (*id, t.graph.compute()))
560            .collect();
561
562        let output = run_plan(
563            &mut self.plan.plan.iter().map(owned_step),
564            self.plan.root_id,
565            external,
566        );
567
568        Ok(Tensor::from_data(output))
569    }
570
571    /// Embeds the compiled plan as a node in a larger graph.
572    ///
573    /// Embeds the [`Skeleton`]'s plan into a promise that must still be planned
574    /// and materialized to produce a [`Tensor`]. Unlike [`run`], its inputs may
575    /// be any [`Composable`] operand except a slot - [`Tensor`], [`TensorPromise`],
576    /// or [`BakedPromise`].
577    ///
578    /// For all practical purposes, treat the output of this function as a
579    /// compressed representation of a [`TensorPromise`].
580    ///
581    /// [`run`]: Skeleton::run
582    /// [`Composable`]: crate::Composable
583    /// [`TensorPromise`]: crate::TensorPromise
584    ///
585    /// # Errors
586    ///
587    /// Returns [`OpError::IncorrectSlotAmount`] if `inputs.len()` differs from
588    /// the number of declared slots, or [`OpError::NotSameLayoutAtSlot`] if an
589    /// input's [`Layout`] does not match the layout its slot was declared with.
590    ///
591    /// # Examples
592    ///
593    /// ```
594    /// use candela::skeleton::SkeletonSlot;
595    /// use candela::{Layout, Tensor};
596    ///
597    /// let lhs = Tensor::from_slice(&[1.0, 2.0, 3.0, 4.0], &[4]);
598    /// let rhs = Tensor::from_scalar(10.0, &[4]);
599    ///
600    /// // Compile `a + b` over two slots, then splice it into a bigger expression.
601    /// let a = SkeletonSlot::new(Layout::new(&[4]));
602    /// let b = a.deep_clone();
603    /// let sum = (&a + &b).into_skeleton(&[a, b])?;
604    ///
605    /// let baked = sum.compose(&[&lhs, &rhs])?;
606    /// // `baked` slots into a normal promise expression.
607    /// let result = (baked * 2.0).materialize();
608    /// assert_eq!(result.data(), &[22.0, 24.0, 26.0, 28.0]);
609    /// # Ok::<(), candela::OpError>(())
610    /// ```
611    pub fn compose<C: Composable<T, B>>(
612        &self,
613        inputs: &[&C],
614    ) -> Result<BakedPromise<T, B>, OpError> {
615        if inputs.len() != self.declared_slots.len() {
616            return Err(OpError::IncorrectSlotAmount(
617                self.declared_slots.len(),
618                inputs.len(),
619            ));
620        }
621
622        for ((i, t), (_, layout)) in zip(inputs.iter().enumerate(), self.declared_slots.iter()) {
623            if t.layout() != layout {
624                return Err(OpError::NotSameLayoutAtSlot(i));
625            }
626        }
627
628        let inputs_idx: Box<[usize]> = self.declared_slots.iter().map(|(id, _)| *id).collect();
629        let inputs: Vec<NodeKind<T, B>> = inputs.iter().map(|x| x.to_node()).collect();
630
631        Ok(BakedPromise::from_node(
632            &self.plan.clone(),
633            inputs.into_boxed_slice(),
634            inputs_idx,
635            &self.layout,
636        ))
637    }
638}
639
640impl<T, B: Backend> Dimension for Skeleton<T, B> {
641    fn layout(&self) -> &Layout {
642        &self.layout
643    }
644}
645
646//////////////////////////////////////////////////////////////////////////////////
647/// A snapshot of the allocations a [`Skeleton`] will perform when run.
648///
649/// Returned by [`Skeleton::memory_report`]; every field is in bytes and reflects
650/// the plan's cache state at the moment the report was taken.
651///
652/// # Note
653///
654/// All allocations are reported as Candela sees them; they do not account for
655/// caching or memory reuse by the system allocator, so the figures may differ
656/// from what actually happens at runtime.
657///
658/// # Examples
659///
660/// ```
661/// use candela::skeleton::SkeletonSlot;
662///
663/// let slot = SkeletonSlot::from_shape(&[4]);
664/// let skeleton = (&slot * 2.0 + 1.0).into_skeleton(&[slot])?;
665///
666/// let report = skeleton.memory_report();
667/// // Every field is in bytes; a [4] f64 output is 4 * 8 = 32 bytes.
668/// assert_eq!(report.output_memory_usage, 32);
669/// assert!(report.peak_memory_usage >= report.output_memory_usage);
670/// # Ok::<(), candela::OpError>(())
671/// ```
672#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct MemoryMetrics {
674    /// peak memory usage in bytes
675    pub peak_memory_usage: usize,
676    /// number of allocations performed
677    pub total_number_of_allocations: usize,
678    /// the sizes, in bytes, of all allocated buffers
679    pub allocated_buffers_size: Vec<usize>,
680    /// total memory allocated in bytes
681    pub total_memory_allocated: usize,
682    /// the size, in bytes, of the output node
683    pub output_memory_usage: usize,
684}
685
686impl<T, B: Backend> Skeleton<T, B> {
687    fn memory_report_plan(&self, root_id: usize, plan: &[OwnedComputeKind<T, B>]) -> MemoryMetrics {
688        let mut allocated_slots: HashMap<usize, usize> = HashMap::new();
689        let mut allocated_buffers_size: Vec<usize> = Vec::new();
690        let mut total_memory_allocated: usize = 0;
691        let mut total_number_of_allocations: usize = 0;
692        let mut current_memory_usage: usize = 0;
693        let mut peak_memory_usage: usize = 0;
694        let mut output_memory_usage: usize = 0;
695
696        for compute_kind in plan.iter() {
697            match compute_kind {
698                OwnedComputeKind::Op {
699                    node,
700                    output,
701                    resolved_inputs,
702                    dealloc_after,
703                } => match output {
704                    OutputKind::Allocate(size) => {
705                        let mem: usize = *size * size_of::<T>();
706                        allocated_slots.insert(node.id, mem);
707                        allocated_buffers_size.push(mem);
708                        total_memory_allocated += mem;
709                        total_number_of_allocations += 1;
710                        current_memory_usage += mem;
711                        peak_memory_usage = peak_memory_usage.max(current_memory_usage);
712
713                        for id in dealloc_after.iter() {
714                            if let Some(mem) = allocated_slots.remove(id) {
715                                current_memory_usage -= mem;
716                            }
717                        }
718                    }
719                    OutputKind::Buffer(id) => {
720                        if let Some(mem) = allocated_slots.remove(id) {
721                            allocated_slots.insert(node.id, mem);
722                        }
723                    }
724                    OutputKind::InPlaceIdx(idx) => {
725                        let id = resolved_inputs[*idx];
726
727                        if let Some(mem) = allocated_slots.remove(&id) {
728                            allocated_slots.insert(node.id, mem);
729                        }
730                    }
731                    OutputKind::Reference(_) => {}
732                },
733                OwnedComputeKind::CachedOp {
734                    cache,
735                    output,
736                    resolved_inputs,
737                    dealloc_after,
738                    ..
739                } => {
740                    if cache.is_cache_filled() {
741                        // If the cache is filled the planner assigns no allocations but, in case of data races,
742                        // the planner saw an unfilled cache. Then, it emits something that is not an Allocate.
743                        // In that case, the executor removes the allocation made for the cache.
744                        match output {
745                            OutputKind::Buffer(id) => {
746                                if let Some(mem) = allocated_slots.remove(id) {
747                                    current_memory_usage -= mem;
748                                }
749                            }
750                            OutputKind::InPlaceIdx(idx) => {
751                                if let Some(mem) = allocated_slots.remove(&resolved_inputs[*idx]) {
752                                    current_memory_usage -= mem;
753                                }
754                            }
755                            _ => {}
756                        }
757
758                        for id in dealloc_after.iter() {
759                            if let Some(mem) = allocated_slots.remove(id) {
760                                current_memory_usage -= mem;
761                            }
762                        }
763                    } else {
764                        if let OutputKind::Allocate(size) = output {
765                            let mem = *size * size_of::<T>();
766                            allocated_slots.insert(cache.get_node().id, mem);
767                            allocated_buffers_size.push(mem);
768                            total_memory_allocated += mem;
769                            total_number_of_allocations += 1;
770                            current_memory_usage += mem;
771                            peak_memory_usage = peak_memory_usage.max(current_memory_usage);
772
773                            for id in dealloc_after.iter() {
774                                if let Some(mem) = allocated_slots.remove(id) {
775                                    current_memory_usage -= mem;
776                                }
777                            }
778                        }
779                    }
780                }
781                OwnedComputeKind::Baked {
782                    baked,
783                    dealloc_after,
784                    ..
785                } => {
786                    let report = self.memory_report_plan(baked.plan.root_id, &baked.plan.plan);
787
788                    allocated_slots.insert(baked.id, report.output_memory_usage);
789                    allocated_buffers_size.extend(report.allocated_buffers_size.iter());
790                    total_memory_allocated += report.total_memory_allocated;
791                    total_number_of_allocations += report.total_number_of_allocations;
792                    peak_memory_usage =
793                        peak_memory_usage.max(current_memory_usage + report.peak_memory_usage);
794                    current_memory_usage += report.output_memory_usage;
795
796                    for id in dealloc_after.iter() {
797                        if let Some(mem) = allocated_slots.remove(id) {
798                            current_memory_usage -= mem;
799                        }
800                    }
801                }
802                OwnedComputeKind::Leaf { .. } => {}
803            }
804        }
805
806        if let Some(size) = allocated_slots.remove(&root_id) {
807            output_memory_usage = size;
808        }
809
810        MemoryMetrics {
811            peak_memory_usage,
812            total_number_of_allocations,
813            allocated_buffers_size,
814            total_memory_allocated,
815            output_memory_usage,
816        }
817    }
818
819    /// Reports memory allocations
820    ///
821    /// Reports the memory that will be allocated during the execution of the [`Skeleton`].
822    /// The report is correct *at the moment* this function was called, but changes to
823    /// cache state (filled vs empty) after it was run *will* change the metrics.
824    ///
825    /// For the most accurate results rerun this function every time a cache part of this
826    /// [`Skeleton`] is changed (even by itself on the first run).
827    ///
828    /// # Examples
829    ///
830    /// ```
831    /// use candela::skeleton::SkeletonSlot;
832    ///
833    /// let slot = SkeletonSlot::from_shape(&[8]);
834    /// let skeleton = (&slot * 2.0).into_skeleton(&[slot])?;
835    ///
836    /// let report = skeleton.memory_report();
837    /// assert!(report.total_number_of_allocations >= 1);
838    /// assert_eq!(report.output_memory_usage, 64); // [8] f64 = 64 bytes
839    /// # Ok::<(), candela::OpError>(())
840    /// ```
841    pub fn memory_report(&self) -> MemoryMetrics {
842        self.memory_report_plan(self.plan.root_id, &self.plan.plan)
843    }
844}