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}