Skip to main content

tenferro_runtime/runtime/
engine.rs

1//! The runtime-facing side of a caller-held execution engine.
2//!
3//! A runtime keeps only what was prepared (graphs, compiled and prepared
4//! programs); everything execution writes travels with the engine the caller
5//! passes to [`crate::Runtime::run`]. This module defines what the runtime needs
6//! from such an engine: an immutable target description used at prepare time,
7//! the engine-owned extension caches, its event-domain driver, and the hand-off
8//! point for unfinished device work.
9
10use std::any::Any;
11use std::fmt;
12use std::sync::Arc;
13
14use tenferro_tensor::{AllocationDomainId, Engine, Tensor, TensorRead};
15
16use super::{
17    EngineId, EventDomainDriver, EventDomainRun, ExecutionContextIdentity,
18    ImmediateEventDomainDriver,
19};
20use crate::extension_cache::ExtensionCacheStore;
21
22/// Immutable description of one execution engine, used to prepare a program.
23///
24/// A target names the registered engine kind (`engine_id`), the concrete
25/// engine type (`context_identity`) and the memory the engine executes on
26/// (`allocation_domain`, `None` for host memory). It holds no threads, streams
27/// or memory. [`crate::Runtime::prepare`] places each part of a program on one
28/// of the targets it is given, and [`crate::Runtime::run`] checks that the
29/// engines it receives report the same targets.
30///
31/// # Examples
32///
33/// ```
34/// use tenferro_runtime::{EngineId, EngineTarget, ExecutionContextIdentity};
35///
36/// struct ExampleEngine;
37/// let target = EngineTarget::new(
38///     EngineId::new("example.engine.v1").unwrap(),
39///     ExecutionContextIdentity::of::<ExampleEngine>(),
40///     None,
41/// );
42/// assert_eq!(target.engine_id().as_str(), "example.engine.v1");
43/// assert!(target.allocation_domain().is_none());
44/// ```
45#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub struct EngineTarget {
47    engine_id: EngineId,
48    context_identity: ExecutionContextIdentity,
49    allocation_domain: Option<AllocationDomainId>,
50}
51
52impl EngineTarget {
53    /// Describe an engine of the registered kind `engine_id`.
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use tenferro_runtime::{EngineId, EngineTarget, ExecutionContextIdentity};
59    ///
60    /// struct ExampleEngine;
61    /// let target = EngineTarget::new(
62    ///     EngineId::new("example.engine.v1").unwrap(),
63    ///     ExecutionContextIdentity::of::<ExampleEngine>(),
64    ///     None,
65    /// );
66    /// assert_eq!(target.context_identity(), ExecutionContextIdentity::of::<ExampleEngine>());
67    /// ```
68    pub fn new(
69        engine_id: EngineId,
70        context_identity: ExecutionContextIdentity,
71        allocation_domain: Option<AllocationDomainId>,
72    ) -> Self {
73        Self {
74            engine_id,
75            context_identity,
76            allocation_domain,
77        }
78    }
79
80    /// The registered engine kind this target runs as.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use tenferro_runtime::{EngineId, EngineTarget, ExecutionContextIdentity};
86    /// struct E;
87    /// let id = EngineId::new("example.engine.v1").unwrap();
88    /// let target = EngineTarget::new(id.clone(), ExecutionContextIdentity::of::<E>(), None);
89    /// assert_eq!(target.engine_id(), &id);
90    /// ```
91    pub fn engine_id(&self) -> &EngineId {
92        &self.engine_id
93    }
94
95    /// The concrete engine type, checked against the registration.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use tenferro_runtime::{EngineId, EngineTarget, ExecutionContextIdentity};
101    /// struct E;
102    /// let target = EngineTarget::new(
103    ///     EngineId::new("example.engine.v1").unwrap(),
104    ///     ExecutionContextIdentity::of::<E>(),
105    ///     None,
106    /// );
107    /// assert_eq!(target.context_identity(), ExecutionContextIdentity::of::<E>());
108    /// ```
109    pub fn context_identity(&self) -> ExecutionContextIdentity {
110        self.context_identity
111    }
112
113    /// The memory domain the engine allocates in, `None` for plain host memory.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use tenferro_runtime::{EngineId, EngineTarget, ExecutionContextIdentity};
119    /// struct E;
120    /// let target = EngineTarget::new(
121    ///     EngineId::new("example.engine.v1").unwrap(),
122    ///     ExecutionContextIdentity::of::<E>(),
123    ///     None,
124    /// );
125    /// assert_eq!(target.allocation_domain(), None);
126    /// ```
127    pub fn allocation_domain(&self) -> Option<AllocationDomainId> {
128        self.allocation_domain
129    }
130}
131
132/// An engine the runtime can run prepared programs on.
133///
134/// Every operation runs on the [`Engine`] the caller passes in; this trait adds
135/// what the scheduler needs from that engine: its target description, its own
136/// extension caches, its event-domain driver and the hand-off point for device
137/// work that is still running when a call returns. A CPU engine finishes its
138/// work before returning, so the defaults (no caches, immediate events, drain
139/// on hand-off) already describe it.
140///
141/// # Examples
142///
143/// ```
144/// use tenferro_runtime::{EngineTarget, ExecutionEngine};
145///
146/// fn describe(engine: &dyn ExecutionEngine) -> EngineTarget {
147///     engine.target()
148/// }
149/// ```
150pub trait ExecutionEngine: Engine {
151    /// Immutable description used by [`crate::Runtime::prepare`].
152    fn target(&self) -> EngineTarget;
153
154    /// Move this engine's extension caches out for the duration of one run.
155    ///
156    /// The runtime calls [`Self::restore_extension_caches`] with the same store
157    /// when the run ends, on success and on error. The default keeps no caches.
158    fn take_extension_caches(&mut self) -> ExtensionCacheStore {
159        ExtensionCacheStore::new()
160    }
161
162    /// Return the extension caches taken by [`Self::take_extension_caches`].
163    fn restore_extension_caches(&mut self, caches: ExtensionCacheStore) {
164        drop(caches);
165    }
166
167    /// Record that an extension callback panicked while the runtime had lent
168    /// this engine's extension caches to it.
169    ///
170    /// The runtime restores the caches, calls this, and resumes the panic. An
171    /// engine that tracks a broken state refuses later operations; the default
172    /// keeps no such state.
173    fn mark_broken(&mut self) {}
174
175    /// The driver that opens this engine's event-domain runs.
176    ///
177    /// Host engines complete each launch before it returns, which the default
178    /// immediate driver describes.
179    fn event_domain_driver(&self) -> Arc<dyn EventDomainDriver> {
180        Arc::new(ImmediateEventDomainDriver::new())
181    }
182
183    /// Take ownership of an event-domain run whose launches may still be running.
184    ///
185    /// The runtime calls this after a run's outputs have been collected. An
186    /// engine that executes asynchronously keeps the run (and with it the
187    /// memory its unfinished work uses) and retires it later; the default
188    /// waits for it now.
189    ///
190    /// # Errors
191    ///
192    /// Returns the run's typed [`crate::Error::Internal`] (or another
193    /// driver-defined event-domain error) when queued work reports a completion
194    /// failure or the native queue cannot be synchronized while it is retired.
195    fn retire_run(&mut self, mut run: Box<dyn EventDomainRun>) -> crate::Result<()> {
196        run.drain()
197    }
198
199    /// Block until the work this engine queued has completed.
200    ///
201    /// [`crate::Runtime::run`] returns as soon as an asynchronous engine has
202    /// queued the work; [`crate::Runtime::warm_up`] calls this afterwards, so a
203    /// caller can pay a program's first execution at a moment it chooses. Host
204    /// engines complete every launch before it returns, which the default
205    /// describes.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`crate::Error::TensorRuntime`] when the engine reports that
210    /// queued work failed or that the device cannot be synchronized.
211    fn synchronize(&mut self) -> crate::Result<()> {
212        Ok(())
213    }
214
215    /// Typed access for prepared operations that execute against the concrete
216    /// engine type instead of the [`Engine`] operations.
217    fn as_any_mut(&mut self) -> &mut dyn Any;
218}
219
220/// One output of [`crate::Runtime::run`].
221///
222/// A value the program computed is returned as a new tensor with a compact
223/// layout. An output that is just one of the inputs, or a view of one, is
224/// returned as that input's view instead of a copy; Rust's borrow rules keep
225/// the input alive for as long as the output is used.
226///
227/// # Examples
228///
229/// ```
230/// use tenferro_runtime::Output;
231/// use tenferro_tensor::{Tensor, TensorRead};
232///
233/// let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
234/// let output = Output::Borrowed(TensorRead::from_tensor(&tensor));
235/// assert_eq!(output.read().shape(), &[2]);
236/// let owned = Output::Owned(Tensor::from_vec_col_major(vec![1], vec![3.0_f64]).unwrap());
237/// assert!(owned.into_owned().is_some());
238/// ```
239// The borrowed variant carries a `TensorRead` by value so the output keeps
240// the input's layout and ownership without an allocation on the common path;
241// the size difference is the accepted cost (phase decision, T1): boxing the
242// owned variant instead would cost an allocation per output.
243#[allow(clippy::large_enum_variant)]
244pub enum Output<'a> {
245    /// A tensor the program computed.
246    Owned(Tensor),
247    /// One of the inputs, or a view of one, returned without a copy.
248    Borrowed(TensorRead<'a>),
249}
250
251impl<'a> Output<'a> {
252    /// Read the output, owned or borrowed.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use tenferro_runtime::Output;
258    /// use tenferro_tensor::Tensor;
259    /// let output = Output::Owned(Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap());
260    /// assert_eq!(output.read().shape(), &[1]);
261    /// ```
262    pub fn read(&self) -> TensorRead<'_> {
263        match self {
264            Self::Owned(tensor) => TensorRead::from_tensor(tensor),
265            Self::Borrowed(read) => read.clone(),
266        }
267    }
268
269    /// The owned tensor, or `None` for a borrowed output.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use tenferro_runtime::Output;
275    /// use tenferro_tensor::{Tensor, TensorRead};
276    /// let tensor = Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
277    /// assert!(Output::Borrowed(TensorRead::from_tensor(&tensor)).into_owned().is_none());
278    /// ```
279    pub fn into_owned(self) -> Option<Tensor> {
280        match self {
281            Self::Owned(tensor) => Some(tensor),
282            Self::Borrowed(_) => None,
283        }
284    }
285
286    /// The output as an owned tensor, copying a borrowed output on `engine`.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use tenferro_runtime::Output;
292    /// use tenferro_tensor::{Engine, Tensor};
293    ///
294    /// fn owned(output: Output<'_>, engine: &mut dyn Engine) -> tenferro_tensor::Result<Tensor> {
295    ///     output.into_tensor(engine)
296    /// }
297    /// ```
298    ///
299    /// # Errors
300    ///
301    /// Returns [`tenferro_tensor::Error::BackendFailure`] or
302    /// [`tenferro_tensor::Error::BackendSource`] when copying a borrowed output
303    /// through the engine fails; an owned output never fails.
304    pub fn into_tensor(self, engine: &mut dyn Engine) -> tenferro_tensor::Result<Tensor> {
305        match self {
306            Self::Owned(tensor) => Ok(tensor),
307            Self::Borrowed(read) => engine.to_contiguous_read(read),
308        }
309    }
310}
311
312impl fmt::Debug for Output<'_> {
313    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            Self::Owned(tensor) => formatter
316                .debug_struct("Output::Owned")
317                .field("dtype", &tensor.dtype())
318                .field("shape", &tensor.shape())
319                .finish(),
320            Self::Borrowed(read) => formatter
321                .debug_struct("Output::Borrowed")
322                .field("dtype", &read.dtype())
323                .field("shape", &read.shape())
324                .finish(),
325        }
326    }
327}