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}