Skip to main content

tenferro_runtime/runtime/
transfer.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4
5use tenferro_tensor::{AllocationDomainId, DType, Placement, Tensor, TensorRead};
6
7use super::schedule::{EventDomainId, ExecutionLocation};
8use super::{EngineId, ProviderDeviceIdentity, StorageClass, TransferEndpoint};
9
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub(crate) struct TransferRoute {
12    source: TransferEndpoint,
13    destination: TransferEndpoint,
14}
15
16impl TransferRoute {
17    pub(crate) fn new(source: TransferEndpoint, destination: TransferEndpoint) -> Self {
18        Self {
19            source,
20            destination,
21        }
22    }
23
24    pub(crate) fn source(&self) -> &TransferEndpoint {
25        &self.source
26    }
27
28    pub(crate) fn destination(&self) -> &TransferEndpoint {
29        &self.destination
30    }
31}
32
33/// A frozen transfer endpoint with its immutable physical binding and event
34/// domain.
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub(crate) struct ResolvedTransferEndpoint {
37    logical: TransferEndpoint,
38    provider_device_identity: ProviderDeviceIdentity,
39    event_domain_id: EventDomainId,
40}
41
42impl ResolvedTransferEndpoint {
43    pub(crate) fn new(
44        logical: TransferEndpoint,
45        provider_device_identity: ProviderDeviceIdentity,
46        event_domain_id: EventDomainId,
47    ) -> Self {
48        Self {
49            logical,
50            provider_device_identity,
51            event_domain_id,
52        }
53    }
54
55    pub(crate) fn logical(&self) -> &TransferEndpoint {
56        &self.logical
57    }
58
59    pub(crate) fn provider_device_identity(&self) -> &ProviderDeviceIdentity {
60        &self.provider_device_identity
61    }
62
63    pub(crate) fn event_domain_id(&self) -> EventDomainId {
64        self.event_domain_id
65    }
66}
67
68/// A frozen transfer route keyed by the exact resolved source and destination
69/// endpoints used during preparation and execution.
70#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71pub(crate) struct ResolvedTransferRoute {
72    source: ResolvedTransferEndpoint,
73    destination: ResolvedTransferEndpoint,
74}
75
76impl ResolvedTransferRoute {
77    pub(crate) fn new(
78        source: ResolvedTransferEndpoint,
79        destination: ResolvedTransferEndpoint,
80    ) -> Self {
81        Self {
82            source,
83            destination,
84        }
85    }
86
87    pub(crate) fn source(&self) -> &ResolvedTransferEndpoint {
88        &self.source
89    }
90
91    pub(crate) fn destination(&self) -> &ResolvedTransferEndpoint {
92        &self.destination
93    }
94}
95
96/// Shared immutable registry of frozen transfer providers.
97#[derive(Clone, Debug)]
98pub(crate) struct FrozenTransferRegistry {
99    routes: Arc<BTreeMap<ResolvedTransferRoute, Arc<dyn TransferProvider>>>,
100}
101
102impl FrozenTransferRegistry {
103    pub(crate) fn new(routes: BTreeMap<ResolvedTransferRoute, Arc<dyn TransferProvider>>) -> Self {
104        Self {
105            routes: Arc::new(routes),
106        }
107    }
108
109    pub(crate) fn iter(
110        &self,
111    ) -> impl Iterator<Item = (&ResolvedTransferRoute, &Arc<dyn TransferProvider>)> {
112        self.routes.iter()
113    }
114
115    pub(crate) fn len(&self) -> usize {
116        self.routes.len()
117    }
118
119    pub(crate) fn contains(&self, route: &ResolvedTransferRoute) -> bool {
120        self.routes.contains_key(route)
121    }
122
123    pub(crate) fn get(&self, route: &ResolvedTransferRoute) -> Option<&Arc<dyn TransferProvider>> {
124        self.routes.get(route)
125    }
126}
127
128/// Runtime-owned transfer provider between two execution locations.
129///
130/// Providers are registered by source and destination endpoint. Each request
131/// also identifies the event domains assigned to those endpoints at freeze.
132pub trait TransferProvider: fmt::Debug + Send + Sync + 'static {
133    /// Complete one blocking transfer into the destination execution location.
134    ///
135    /// The returned tensor must be immediately readable by the destination
136    /// executor. Providers must not return after merely enqueueing work on an
137    /// asynchronous stream or queue. Native asynchronous transfers use the
138    /// event-domain driver contract instead of this interface.
139    ///
140    /// # Errors
141    ///
142    /// Returns a [`tenferro_tensor::ErrorKind::RuntimeState`] or backend
143    /// failure when the provider cannot materialize the destination tensor, or
144    /// a validation error such as dtype, shape, placement, or buffer mismatch
145    /// when the transfer request is unsupported.
146    fn transfer_blocking(&self, request: TransferRequest<'_>) -> crate::Result<Tensor>;
147}
148
149/// Borrowed request passed to a [`TransferProvider`].
150#[derive(Debug)]
151pub struct TransferRequest<'a> {
152    source_location: &'a ExecutionLocation,
153    destination_location: &'a ExecutionLocation,
154    input: TensorRead<'a>,
155}
156
157impl<'a> TransferRequest<'a> {
158    pub(crate) fn new(
159        source_location: &'a ExecutionLocation,
160        destination_location: &'a ExecutionLocation,
161        input: TensorRead<'a>,
162    ) -> Self {
163        Self {
164            source_location,
165            destination_location,
166            input,
167        }
168    }
169
170    /// Return the source engine for this transfer.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use tenferro_runtime::{EngineId, TransferRequest};
176    ///
177    /// # fn inspect(request: TransferRequest<'_>) {
178    /// let source: &EngineId = request.source_engine_id();
179    /// assert!(!source.as_str().is_empty());
180    /// # }
181    /// ```
182    pub fn source_engine_id(&self) -> &'a EngineId {
183        self.source_location.engine_id()
184    }
185
186    /// Return the immutable source provider/device binding used for this
187    /// transfer request.
188    pub fn source_provider_device_identity(&self) -> &'a ProviderDeviceIdentity {
189        self.source_location.provider_device_identity()
190    }
191
192    /// Return the source event domain for this transfer.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use tenferro_runtime::{EventDomainId, TransferRequest};
198    ///
199    /// # fn inspect(request: TransferRequest<'_>) {
200    /// let _: EventDomainId = request.source_event_domain_id();
201    /// # }
202    /// ```
203    pub fn source_event_domain_id(&self) -> EventDomainId {
204        self.source_location.event_domain_id()
205    }
206
207    /// Return the source storage class for this transfer.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use tenferro_runtime::{StorageClass, TransferRequest};
213    ///
214    /// # fn inspect(request: TransferRequest<'_>) {
215    /// let source: &StorageClass = request.source_storage_class();
216    /// assert!(!source.as_str().is_empty());
217    /// # }
218    /// ```
219    pub fn source_storage_class(&self) -> &'a StorageClass {
220        self.source_location.storage_class()
221    }
222
223    /// Return the destination engine for this transfer.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use tenferro_runtime::{EngineId, TransferRequest};
229    ///
230    /// # fn inspect(request: TransferRequest<'_>) {
231    /// let destination: &EngineId = request.destination_engine_id();
232    /// assert!(!destination.as_str().is_empty());
233    /// # }
234    /// ```
235    pub fn destination_engine_id(&self) -> &'a EngineId {
236        self.destination_location.engine_id()
237    }
238
239    /// Return the immutable destination provider/device binding used for this
240    /// transfer request.
241    pub fn destination_provider_device_identity(&self) -> &'a ProviderDeviceIdentity {
242        self.destination_location.provider_device_identity()
243    }
244
245    /// Return the destination event domain for this transfer.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use tenferro_runtime::{EventDomainId, TransferRequest};
251    ///
252    /// # fn inspect(request: TransferRequest<'_>) {
253    /// let _: EventDomainId = request.destination_event_domain_id();
254    /// # }
255    /// ```
256    pub fn destination_event_domain_id(&self) -> EventDomainId {
257        self.destination_location.event_domain_id()
258    }
259
260    /// Return the destination storage class for this transfer.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use tenferro_runtime::{StorageClass, TransferRequest};
266    ///
267    /// # fn inspect(request: TransferRequest<'_>) {
268    /// let destination: &StorageClass = request.destination_storage_class();
269    /// assert!(!destination.as_str().is_empty());
270    /// # }
271    /// ```
272    pub fn destination_storage_class(&self) -> &'a StorageClass {
273        self.destination_location.storage_class()
274    }
275
276    /// Return the tensor read that must be transferred.
277    pub fn input(&self) -> &TensorRead<'a> {
278        &self.input
279    }
280}
281
282/// Typed runtime transfer setup failure.
283///
284/// # Examples
285///
286/// ```
287/// use tenferro_runtime::TransferError;
288///
289/// fn is_missing_provider(error: &TransferError) -> bool {
290///     matches!(error, TransferError::MissingProvider { .. })
291/// }
292/// ```
293#[derive(Debug, thiserror::Error)]
294#[non_exhaustive]
295pub enum TransferError {
296    /// No provider was registered for the endpoint pair required by two
297    /// concrete execution locations.
298    #[error(
299        "no transfer provider registered from source endpoint {source_endpoint:?} \
300         (event domain {source_event_domain_id:?}) to destination endpoint {destination_endpoint:?} \
301         (event domain {destination_event_domain_id:?})"
302    )]
303    MissingProvider {
304        /// Source transfer endpoint.
305        source_endpoint: TransferEndpoint,
306        /// Source event domain.
307        source_event_domain_id: EventDomainId,
308        /// Destination transfer endpoint.
309        destination_endpoint: TransferEndpoint,
310        /// Destination event domain.
311        destination_event_domain_id: EventDomainId,
312    },
313    /// A provider returned a tensor that violates the transfer request contract.
314    #[error("transfer provider returned an invalid tensor")]
315    ProviderContract {
316        /// Typed provider-contract violation.
317        #[source]
318        source: TransferProviderContractError,
319    },
320}
321
322/// Typed contract violation in a tensor returned by a transfer provider.
323///
324/// # Examples
325///
326/// ```
327/// use tenferro_runtime::{DType, TransferProviderContractError};
328///
329/// let error = TransferProviderContractError::DTypeMismatch {
330///     expected: DType::F64,
331///     actual: DType::F32,
332/// };
333/// assert!(error.to_string().contains("dtype"));
334/// ```
335#[derive(Debug, thiserror::Error)]
336#[non_exhaustive]
337pub enum TransferProviderContractError {
338    /// The logical source shape cannot be represented as an element count.
339    #[error("transfer source logical element count is invalid")]
340    LogicalElementCount {
341        /// Checked tensor shape-product failure.
342        #[source]
343        source: tenferro_tensor::Error,
344    },
345    /// The returned tensor changed the source dtype.
346    #[error("transfer output dtype mismatch: expected {expected:?}, actual {actual:?}")]
347    DTypeMismatch {
348        /// Source dtype required by the request.
349        expected: DType,
350        /// Dtype returned by the provider.
351        actual: DType,
352    },
353    /// The returned tensor changed the source shape.
354    #[error("transfer output shape mismatch: expected {expected:?}, actual {actual:?}")]
355    ShapeMismatch {
356        /// Source shape required by the request.
357        expected: Vec<usize>,
358        /// Shape returned by the provider.
359        actual: Vec<usize>,
360    },
361    /// The returned tensor placement is not accepted at the destination endpoint.
362    #[error(
363        "transfer output placement {actual:?} is incompatible with destination storage \
364         {destination_storage_class:?} on engine {destination_engine_id:?}"
365    )]
366    DestinationPlacementMismatch {
367        /// Destination engine from the transfer request.
368        destination_engine_id: EngineId,
369        /// Destination storage class from the transfer request.
370        destination_storage_class: StorageClass,
371        /// Placement returned by the provider.
372        actual: Placement,
373    },
374    /// The returned tensor has compatible metadata but is not owned by the
375    /// destination engine's backend or allocation domain.
376    #[error(
377        "transfer output storage family {actual_backend_family:?} and allocation domain \
378         {actual_allocation_domain:?} are not resident in destination storage \
379         {destination_storage_class:?} on engine {destination_engine_id:?}"
380    )]
381    DestinationResidencyMismatch {
382        /// Destination engine from the transfer request.
383        destination_engine_id: EngineId,
384        /// Destination storage class from the transfer request.
385        destination_storage_class: StorageClass,
386        /// Physical backend family returned by the provider.
387        actual_backend_family: Option<&'static str>,
388        /// Shared allocation domain returned by the provider.
389        actual_allocation_domain: Option<AllocationDomainId>,
390    },
391    /// The returned tensor's buffer length does not match its shape.
392    #[error(
393        "transfer output buffer length mismatch: shape requires {expected} elements, \
394         buffer reports {actual}"
395    )]
396    InvalidBufferLength {
397        /// Element count required by the returned shape.
398        expected: usize,
399        /// Element count reported by the returned buffer.
400        actual: usize,
401    },
402}