candela/lib.rs
1#![allow(private_bounds)]
2//! A lazy, graph-based tensor engine for the CPU, with `f32`/`f64` element
3//! types and a pluggable backend (pure-Rust by default, Intel MKL behind the
4//! `mkl` feature).
5//!
6//! Operations on a [`Tensor`] don't compute anything - they build a computation
7//! graph and return a [`TensorPromise`]. Calling `.materialize()` plans the
8//! whole graph (ordering, buffer reuse, scalar-op fusion) and runs it in one
9//! pass, handing back a finished [`Tensor`].
10//!
11//! ```
12//! use candela::{arange, Tensor};
13//!
14//! // Building the expression allocates nothing; only `.materialize()` runs it.
15//! let x: Tensor<f64> = arange!(4); // [0, 1, 2, 3]
16//! let y = (x * 2.0 + 1.0).materialize(); // 2x + 1, fused into one pass
17//! assert_eq!(y.data(), &[1.0, 3.0, 5.0, 7.0]);
18//! ```
19//!
20//! # The types
21//!
22//! - [`Tensor`] - a materialized buffer with a shape and stride.
23//! - [`TensorPromise`] - an unevaluated computation graph; `.materialize()` runs it.
24//! - [`CachedTensorPromise`] - a promise that keeps its result alive for reuse
25//! across separate materializations.
26//! - [`Skeleton`](skeleton::Skeleton) - a graph compiled once over
27//! [`SkeletonSlot`](skeleton::SkeletonSlot) placeholders and run many times against
28//! new data, skipping per-call planning. See the [`skeleton`] module for the
29//! dynamic-shape and caching variants.
30//!
31//! # Building a graph
32//!
33//! A promise is built up op by op and stays inert until you materialize it.
34//! Fallible ops (anything shape-dependent, like [`matmul`](Tensor::matmul))
35//! return a `Result` at *construction* time, so a graph that could never run is
36//! rejected before any compute happens.
37//!
38//! ```
39//! use candela::{srange, Dimension, OpError, Tensor};
40//!
41//! # fn main() -> Result<(), OpError> {
42//! let a: Tensor<f32> = srange![2 * 3, &[2, 3]]; // 2x3, values 0..6
43//! let b: Tensor<f32> = srange![3 * 2, &[3, 2]]; // 3x2, values 0..6
44//!
45//! let c = a.matmul(&b)?.materialize(); // shape mismatch would fail here
46//! assert_eq!(c.shape(), &[2, 2]);
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! # Reusing intermediate results
52//!
53//! [`.materialize()`](TensorPromise::materialize) consumes the graph and frees
54//! every intermediate buffer. When you want to keep a mid-graph value alive -
55//! to inspect it, or to branch off it more than once - call
56//! [`.cache()`](TensorPromise::cache) to turn the promise into a
57//! [`CachedTensorPromise`]. It computes at most once and hands the stored result
58//! to everyone downstream.
59//!
60//! ```
61//! use candela::Tensor;
62//!
63//! let a = Tensor::from_scalar(1.0_f64, &[3, 3]);
64//! let b = (a + 2.0).cache(); // will hold onto its result
65//!
66//! let peek = b.snapshot(); // computes b once, fills the cache
67//! assert_eq!(peek.data(), &[3.0; 9]);
68//!
69//! let c = (b * 10.0).materialize(); // reuses the cached b, no recompute
70//! assert_eq!(c.data(), &[30.0; 9]);
71//! ```
72//!
73//! # Errors
74//!
75//! Candela splits failures by how likely they are to be a bug:
76//!
77//! - The inline arithmetic operators (`+`, `-`, `*`, `/`) **panic** on a shape
78//! mismatch. A mismatch there is almost always a programming error, and making
79//! them fallible would force an `.unwrap()` onto every expression.
80//! - Everything else that can fail returns `Result<_, `[`OpError`]`>` - and,
81//! because the graph is built eagerly, it fails at construction time rather
82//! than deep inside `.materialize()`.
83//!
84//! # Feature flags
85//!
86//! - `mkl` - swap the default pure-Rust backend for Intel MKL. Links against
87//! MKL (handled by `intel-mkl-src`), so the libraries must be available on the
88//! build host. See the [`backends`](docs::backends) docs.
89//! - `tracing` - emit [`tracing`](https://docs.rs/tracing) spans through the
90//! planner and execution for profiling and debugging.
91//!
92//! # Concepts
93//!
94//! The [`docs`] module shows in detail how the processing pipeline actually works,
95//! from expression to computed tensor. Start with the [overview](docs::overview) for
96//! the general behaviour; it links each subsystem from there.
97
98mod tensor;
99
100pub use tensor::arange;
101pub use tensor::errors::OpError;
102pub use tensor::traits::Composable;
103pub use tensor::{
104 CachedTensorPromise, Dimension, InformedIter, Iter, Layout, SliceRange, StepInfo, Tensor,
105 TensorPromise,
106};
107
108pub mod backend {
109 pub use crate::tensor::backend::implementation::*;
110 pub use crate::tensor::backend::{Backend, ComputeFor, DefaultBackend};
111}
112
113pub mod skeleton {
114 pub use crate::tensor::skeleton::{
115 BakedPromise, BuildFunction, DynamicSkeleton, EvictionPolicy, LRUPolicy, MemoryMetrics,
116 Skeleton, SkeletonCache, SkeletonPromise, SkeletonSlot, UnboundedDynamicSkeleton,
117 UnboundedPolicy,
118 };
119}
120
121/// Design documentation.
122///
123/// Start with [`overview`](crate::docs::overview) for the whole pipeline, then
124/// dive into whichever subsystem you need.
125#[cfg(doc)]
126pub mod docs {
127 #[doc = include_str!("../doc/concepts/backends.md")]
128 pub mod backends {}
129 #[doc = include_str!("../doc/concepts/graph.md")]
130 pub mod graph {}
131 #[doc = include_str!("../doc/concepts/layout.md")]
132 pub mod layout {}
133 #[doc = include_str!("../doc/concepts/planner.md")]
134 pub mod planner {}
135 #[doc = include_str!("../doc/concepts/planner-history.md")]
136 pub mod planner_history {}
137 #[doc = include_str!("../doc/concepts/skeleton.md")]
138 pub mod skeleton {}
139 #[doc = include_str!("../doc/concepts/overview.md")]
140 pub mod overview {}
141}
142
143use std::ops::Neg;
144
145use crate::backend::{ComputeFor, DefaultBackend};
146use crate::tensor::FromIndex;
147use crate::tensor::definitions::NumberLike;
148use crate::tensor::ops::CanMatMul;
149use crate::tensor::ops::FloatLike;
150use crate::tensor::traits::Numeric;
151
152const PACKING_BUFFER_SIZE: usize = 2048;
153
154/// Sealed marker for floating-point tensor element types: `f32` and `f64`.
155///
156/// Bundles the bounds required for full tensor operation support: arithmetic
157/// (`NumberLike`), floating-point ops (`FloatLike`, `Neg`), matrix
158/// multiplication (`CanMatMul`), index-based construction (`FromIndex`), and a
159/// lossless `Into<f64>` conversion used by comparison utilities such as
160/// `assert_approx_eq`. The inverse, [`from_f64`](Self::from_f64), lets generic
161/// code construct typed values from float literals (precision is lost when `T`
162/// is `f32` and the source does not fit).
163///
164/// The trait is sealed: the `pub(crate)` supertraits (`CanMatMul`, `FromIndex`)
165/// cannot be named outside this crate, so no external implementation is
166/// possible.
167///
168/// # Examples
169///
170/// ```
171/// use candela::{FloatLikeTensorElement, Tensor};
172///
173/// fn scaled_identity<T: FloatLikeTensorElement>(n: usize, factor: T) -> Tensor<T> {
174/// (Tensor::from_scalar(T::from_f64(1.0), &[n]) * factor).materialize()
175/// }
176///
177/// let f64_result: Tensor<f64> = scaled_identity(4, 3.0);
178/// let f32_result: Tensor<f32> = scaled_identity(4, 3.0);
179/// assert_eq!(f64_result.data(), &vec![3.0f64; 4]);
180/// assert_eq!(f32_result.data(), &vec![3.0f32; 4]);
181/// ```
182pub trait FloatLikeTensorElement:
183 NumberLike
184 + Numeric
185 + FloatLike
186 + Into<f64>
187 + Neg<Output = Self>
188 + CanMatMul
189 + FromIndex
190 + ComputeFor<DefaultBackend>
191{
192 /// Construct a typed value from an `f64` literal. Lossless for `f64`; for
193 /// `f32` the value is narrowed via `as f32`.
194 fn from_f64(v: f64) -> Self;
195}
196
197impl FloatLikeTensorElement for f64 {
198 #[inline]
199 fn from_f64(v: f64) -> Self {
200 v
201 }
202}
203impl FloatLikeTensorElement for f32 {
204 #[inline]
205 fn from_f64(v: f64) -> Self {
206 v as f32
207 }
208}