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}