Skip to main content

candela/tensor/
promise.rs

1//! Promise types for lazy tensor evaluation.
2//!
3//! A [`TensorPromise`] describes a computation without running it. Building a
4//! chain of ops constructs a graph; nothing is allocated or computed until you
5//! call `.materialize()`. [`CachedTensorPromise`] is the same idea, but it keeps
6//! the result around so subsequent uses don't need to recompute.
7
8#![allow(private_bounds)]
9use std::sync::Arc;
10
11use crate::tensor::backend::{Backend, ComputeFor, DefaultBackend};
12use crate::tensor::definitions::NumberLike;
13use crate::tensor::errors::OpError;
14use crate::tensor::graph::{NodeKind, TensorGraphCacheNode, TensorGraphNode};
15use crate::tensor::mem_formats::layout::Layout;
16use crate::tensor::ops::def_op::OpKind;
17use crate::tensor::skeleton::{Clean, SkeletonSlot, Tainting};
18use crate::tensor::tensor_interface::Tensor;
19use crate::tensor::traits::{Composable, Dimension, Numeric, Operand, Promising};
20
21/// A lazy computation that runs when you call [`.materialize()`].
22///
23/// Building a `TensorPromise` chain allocates no intermediate tensors - the
24/// graph is constructed, not evaluated.
25///
26/// [`.materialize()`]: TensorPromise::materialize
27///
28/// # Examples
29///
30/// ```
31/// use candela::Tensor;
32///
33/// let t = Tensor::from_scalar(3.0_f64, &[4]);
34/// let result = (t * 2.0 + 1.0).materialize();
35/// assert_eq!(result.data(), &vec![7.0; 4]);
36/// ```
37pub struct TensorPromise<T, B: Backend = DefaultBackend> {
38    pub(crate) graph: Arc<TensorGraphNode<T, B>>,
39}
40
41impl<T: std::fmt::Debug, B: Backend> std::fmt::Debug for TensorPromise<T, B> {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        std::fmt::Debug::fmt(&self.graph, f)
44    }
45}
46
47impl<T: Numeric, B: Backend> TensorPromise<T, B> {
48    pub(crate) fn new(op: OpKind<T>, inputs: Box<[NodeKind<T, B>]>) -> Result<Self, OpError> {
49        let node = TensorGraphNode::new(op, inputs);
50
51        match node {
52            Ok(node) => Ok(Self {
53                graph: Arc::new(node),
54            }),
55            Err(err) => Err(err),
56        }
57    }
58
59    pub(crate) fn with_layout(
60        op: OpKind<T>,
61        inputs: Box<[NodeKind<T, B>]>,
62        layout: Layout,
63    ) -> Self {
64        Self {
65            graph: Arc::new(TensorGraphNode::with_layout(op, inputs, layout)),
66        }
67    }
68
69    /// Convert this promise into a [`CachedTensorPromise`] that stores its
70    /// result after the first evaluation.
71    ///
72    /// Internally this wraps `self` in a `NoOp` cache node, so the original
73    /// graph is unchanged - caching is layered on top, not baked in.
74    /// The `NoOp` is used to stop the fusion layer from skipping the cache.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use candela::Tensor;
80    ///
81    /// let t = Tensor::from_scalar(5.0_f64, &[3]);
82    /// let cached = t.to_promise().cache();
83    ///
84    /// // Safe to use multiple times; result is computed only once.
85    /// let _ = (&cached * 2.0).materialize();
86    /// let _ = (&cached + 1.0).materialize();
87    /// ```
88    pub fn cache(self) -> CachedTensorPromise<T, B> {
89        let base = unsafe {
90            TensorPromise::new(OpKind::AsContiguous, [NodeKind::Node(self.graph)].into())
91                .unwrap_unchecked()
92        };
93
94        unsafe {
95            CachedTensorPromise::new(OpKind::NoOp, [NodeKind::Node(base.graph)].into())
96                .unwrap_unchecked()
97        }
98    }
99}
100
101impl<T: NumberLike + ComputeFor<B>, B: Backend> TensorPromise<T, B> {
102    /// Execute the computation graph and return the result as a [`Tensor`].
103    ///
104    /// This is where the work actually happens. The planner analyses the graph,
105    /// assigns buffers, and then the executor runs each op in dependency order,
106    /// freeing intermediate results as soon as they're no longer needed.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use candela::Tensor;
112    ///
113    /// let t = Tensor::from_scalar(4.0_f64, &[3]);
114    /// let result = (t - 1.0).materialize();
115    /// assert_eq!(result.data(), &vec![3.0; 3]);
116    /// ```
117    pub fn materialize(self) -> Tensor<T> {
118        Tensor::from_data(self.graph.compute())
119    }
120
121    /// Execute the computation graph and return the result as a [`Tensor`].
122    ///
123    /// Same as [`.materialize()`] but does not consume self.
124    ///
125    /// [`.materialize()`]: TensorPromise::materialize
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use candela::Tensor;
131    ///
132    /// let t = Tensor::from_scalar(4.0_f64, &[3]).to_promise();
133    /// let result1 = t.clone_and_materialize();
134    /// let result2 = t.materialize(); // consumes t
135    /// assert_eq!(result1.data(), result2.data());
136    /// ```
137    pub fn clone_and_materialize(&self) -> Tensor<T> {
138        Tensor::from_data(self.graph.compute())
139    }
140
141    /// Creates a [`SkeletonSlot`] shaped like this promise's output.
142    ///
143    /// The slot is an input placeholder for a [`Skeleton`]. It has the promise's
144    /// [`Layout`] but holds no data.
145    ///
146    /// [`Skeleton`]: crate::skeleton::Skeleton
147    pub fn to_slot(&self) -> SkeletonSlot<T, B> {
148        SkeletonSlot::new(self.layout().clone())
149    }
150}
151
152impl<T, B: Backend> Operand<T, B> for TensorPromise<T, B> {
153    fn to_node(&self) -> NodeKind<T, B> {
154        NodeKind::Node(self.graph.clone())
155    }
156}
157
158impl<T, B: Backend> Tainting for TensorPromise<T, B> {
159    type Mark = Clean;
160}
161
162impl<T, B: Backend> Composable<T, B> for TensorPromise<T, B> {}
163
164impl<T, B: Backend> Dimension for TensorPromise<T, B> {
165    #[inline]
166    fn layout(&self) -> &Layout {
167        self.graph.layout()
168    }
169}
170
171impl<T, B: Backend> Clone for TensorPromise<T, B> {
172    fn clone(&self) -> Self {
173        Self {
174            graph: self.graph.clone(),
175        }
176    }
177}
178
179//////////////////////////////////////////////////////////////////////////////////
180
181/// A lazy computation whose result is kept alive after the first evaluation.
182///
183/// Once [`.materialize()`] has been called (directly or through a derived
184/// promise), the result is cached. Every subsequent call returns the stored
185/// value without re-running the graph.
186///
187/// Use this when the same promise feeds into multiple independent downstream
188/// graphs that materialise at different times. You pay the memory cost of
189/// keeping the tensor alive, which is why caching is opt-in.
190///
191/// [`.materialize()`]: CachedTensorPromise::materialize
192///
193/// # Examples
194///
195/// ```
196/// use candela::Tensor;
197///
198/// let t = Tensor::from_scalar(1.0_f64, &[4]);
199/// let cached = (t + 2.0).cache();
200///
201/// // Two separate materializations - the inner graph runs only once.
202/// let r1 = (&cached * 2.0).materialize();
203/// let r2 = (&cached + 10.0).materialize();
204/// assert_eq!(r1.data(), &vec![6.0; 4]);
205/// assert_eq!(r2.data(), &vec![13.0; 4]);
206/// ```
207pub struct CachedTensorPromise<T, B: Backend = DefaultBackend> {
208    pub(crate) graph: Arc<TensorGraphCacheNode<T, B>>,
209}
210
211impl<T: std::fmt::Debug, B: Backend> std::fmt::Debug for CachedTensorPromise<T, B> {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        std::fmt::Debug::fmt(&self.graph, f)
214    }
215}
216
217impl<T: Numeric, B: Backend> CachedTensorPromise<T, B> {
218    pub(crate) fn new(op: OpKind<T>, inputs: Box<[NodeKind<T, B>]>) -> Result<Self, OpError> {
219        let node = TensorGraphCacheNode::new(op, inputs);
220
221        match node {
222            Ok(node) => Ok(Self {
223                graph: Arc::new(node),
224            }),
225            Err(err) => Err(err),
226        }
227    }
228}
229
230impl<T: NumberLike + ComputeFor<B>, B: Backend> CachedTensorPromise<T, B> {
231    /// Return the cached result if it has already been computed, or `None` if
232    /// [`.materialize()`] has not been called yet.
233    ///
234    /// [`.materialize()`]: CachedTensorPromise::materialize
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// use candela::Tensor;
240    ///
241    /// let t = Tensor::from_scalar(1.0_f64, &[2]);
242    /// let cached = t.to_promise().cache();
243    ///
244    /// assert!(cached.get_cache().is_none());
245    /// let _ = (&cached + 0.0).materialize();
246    /// assert!(cached.get_cache().is_some());
247    /// ```
248    pub fn get_cache(&self) -> Option<Tensor<T>> {
249        self.graph
250            .get_cache()
251            .map(|tensor| Tensor::from_data(tensor.clone()))
252    }
253
254    /// Return the cached result if it has already been computed, or calls
255    /// [`.materialize()`] and then returns the cached tensor if it was not.
256    ///
257    /// [`.materialize()`]: CachedTensorPromise::materialize
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use candela::Tensor;
263    ///
264    /// let t = Tensor::from_scalar(1.0_f64, &[2]);
265    /// let cached = t.to_promise().cache();
266    ///
267    /// assert!(cached.get_cache().is_none());
268    /// assert_eq!(t.data(), cached.snapshot().data());
269    /// assert!(cached.get_cache().is_some());
270    /// ```
271    pub fn snapshot(&self) -> Tensor<T> {
272        if let Some(tensor) = self.graph.get_cache() {
273            Tensor::from_data(tensor.clone())
274        } else {
275            self.clone_and_materialize()
276        }
277    }
278
279    /// Execute the computation graph and return the result as a [`Tensor`].
280    ///
281    /// See [`TensorPromise::materialize`] for details.
282    pub fn materialize(self) -> Tensor<T> {
283        Tensor::from_data(self.graph.compute())
284    }
285
286    /// Same as [`.materialize()`](Self::materialize) but does not consume self.
287    pub fn clone_and_materialize(&self) -> Tensor<T> {
288        Tensor::from_data(self.graph.compute())
289    }
290
291    /// Creates a [`SkeletonSlot`] shaped like this promise's output.
292    ///
293    /// The slot is an input placeholder for a [`Skeleton`]. It has the promise's
294    /// [`Layout`] but holds no data.
295    ///
296    /// [`Skeleton`]: crate::skeleton::Skeleton
297    pub fn to_slot(&self) -> SkeletonSlot<T, B> {
298        SkeletonSlot::new(self.layout().clone())
299    }
300}
301
302impl<T, B: Backend> Operand<T, B> for CachedTensorPromise<T, B> {
303    fn to_node(&self) -> NodeKind<T, B> {
304        NodeKind::Cache(self.graph.clone())
305    }
306}
307
308impl<T, B: Backend> Tainting for CachedTensorPromise<T, B> {
309    type Mark = Clean;
310}
311
312impl<T, B: Backend> Composable<T, B> for CachedTensorPromise<T, B> {}
313
314impl<T, B: Backend> Dimension for CachedTensorPromise<T, B> {
315    #[inline]
316    fn layout(&self) -> &Layout {
317        self.graph.layout()
318    }
319}
320
321impl<T, B: Backend> Clone for CachedTensorPromise<T, B> {
322    fn clone(&self) -> Self {
323        Self {
324            graph: self.graph.clone(),
325        }
326    }
327}