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}