Skip to main content

tenferro_gpu/gpu/
mod.rs

1//! Backend-neutral GPU seam: storage identity, the allocation a buffer carries, and the
2//! failure vocabulary every GPU backend reports through.
3//!
4//! Nothing here names a CUDA (or any other vendor) type. A backend implements [`Backend`]
5//! to name its own allocation handle; the shared layer stores and passes allocations as
6//! [`Buffer<B>`] and never reads a vendor type. The seam exists so a second backend
7//! (ROCm/HIP, then Metal) is added without touching the dispatch, transfer or publication
8//! code.
9//!
10//! # Examples
11//!
12//! ```
13//! use tenferro_gpu::gpu::{GpuError, SeamError};
14//!
15//! let error = GpuError::unsupported("gemm", "the vendor adapter is not installed");
16//! assert_eq!(error.kind(), SeamError::Unsupported);
17//! ```
18
19use tenferro_tensor::{AllocationDomainId, AllocationId};
20
21/// A failure a GPU backend reports through the seam.
22///
23/// The provider-specific error types keep their own variants and ownership packages
24/// (a rejected submission hands the submitted value back, for example); this type is
25/// the seam's *cause* vocabulary only, and every backend maps into it one way.
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28#[doc(hidden)]
29pub enum GpuError {
30    /// No device or runtime is available for the request.
31    Unavailable {
32        /// What was probed and why it failed.
33        detail: String,
34    },
35    /// The operation is not served by this backend or this build.
36    Unsupported {
37        /// Operation name.
38        op: &'static str,
39        /// Why it is not served.
40        detail: String,
41    },
42    /// A buffer or device belongs to another backend or allocation domain.
43    ForeignDomain,
44    /// The backend cannot admit the work within its pending budget.
45    Backpressure {
46        /// Bytes the admission needs.
47        bytes: usize,
48    },
49    /// The runtime or engine has been closed.
50    Closed,
51    /// The transfer failed, with the route that failed.
52    Transfer {
53        /// Whether the failed route staged through host memory.
54        staged: bool,
55        /// What the backend reported.
56        detail: String,
57    },
58    /// An earlier failure left the backend unusable.
59    Broken {
60        /// The original failure.
61        detail: String,
62    },
63}
64
65/// The category of a [`GpuError`], for callers that only branch on the class.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[doc(hidden)]
68pub enum SeamError {
69    /// No device or runtime.
70    Unavailable,
71    /// Not served by this backend or build.
72    Unsupported,
73    /// Another backend or domain.
74    ForeignDomain,
75    /// Out of pending budget.
76    Backpressure,
77    /// Closed.
78    Closed,
79    /// A transfer failed.
80    Transfer,
81    /// Unusable after an earlier failure.
82    Broken,
83}
84
85impl GpuError {
86    /// The category of this failure.
87    #[doc(hidden)]
88    pub fn kind(&self) -> SeamError {
89        match self {
90            GpuError::Unavailable { .. } => SeamError::Unavailable,
91            GpuError::Unsupported { .. } => SeamError::Unsupported,
92            GpuError::ForeignDomain => SeamError::ForeignDomain,
93            GpuError::Backpressure { .. } => SeamError::Backpressure,
94            GpuError::Closed => SeamError::Closed,
95            GpuError::Transfer { .. } => SeamError::Transfer,
96            GpuError::Broken { .. } => SeamError::Broken,
97        }
98    }
99
100    /// The cause of an unsupported operation.
101    #[doc(hidden)]
102    pub fn unsupported(op: &'static str, detail: impl Into<String>) -> GpuError {
103        GpuError::Unsupported {
104            op,
105            detail: detail.into(),
106        }
107    }
108
109    /// The cause of a transfer failure.
110    #[doc(hidden)]
111    pub fn transfer(staged: bool, detail: impl Into<String>) -> GpuError {
112        GpuError::Transfer {
113            staged,
114            detail: detail.into(),
115        }
116    }
117}
118
119impl std::fmt::Display for GpuError {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            GpuError::Unavailable { detail } => write!(f, "GPU backend unavailable: {detail}"),
123            GpuError::Unsupported { op, detail } => write!(f, "{op} is not served: {detail}"),
124            GpuError::ForeignDomain => f.write_str("the operand belongs to another GPU domain"),
125            GpuError::Backpressure { bytes } => {
126                write!(f, "the GPU pending budget cannot admit {bytes} bytes")
127            }
128            GpuError::Closed => f.write_str("the GPU runtime has been closed"),
129            GpuError::Transfer { staged, detail } => {
130                let route = if *staged { "staged" } else { "direct" };
131                write!(f, "{route} transfer failed: {detail}")
132            }
133            GpuError::Broken { detail } => {
134                write!(
135                    f,
136                    "the GPU backend is unusable after an earlier failure: {detail}"
137                )
138            }
139        }
140    }
141}
142
143impl std::error::Error for GpuError {}
144
145/// Identity of one storage allocation.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[doc(hidden)]
148pub struct StorageIdentity {
149    /// Shared-allocation domain of the runtime that owns the allocation.
150    pub domain: AllocationDomainId,
151    /// Stable identity of the allocation inside that domain.
152    pub allocation: AllocationId,
153    /// Device ordinal that owns the allocation.
154    pub device: usize,
155}
156
157/// One GPU backend: the concrete types the seam carries without naming them.
158///
159/// The shared layer stores and passes allocations through [`Buffer`] and never reads a
160/// backend type. A second backend supplies its own allocation handle here; the execution
161/// vocabulary that lets it reuse the dispatch, transfer and publication code lands with
162/// that vocabulary's first implementor rather than as an unused declaration.
163///
164/// # Examples
165///
166/// ```
167/// use tenferro_gpu::gpu::{Backend, Buffer, StorageIdentity};
168/// use tenferro_tensor::AllocationDomainId;
169///
170/// struct ExampleBackend;
171/// impl Backend for ExampleBackend {
172///     type Allocation = u64;
173/// }
174///
175/// let identity = StorageIdentity {
176///     domain: AllocationDomainId::fresh(),
177///     allocation: tenferro_tensor::AllocationId::from_backend_id(2),
178///     device: 0,
179/// };
180/// let buffer = Buffer::<ExampleBackend>::new(identity, 64, 0xdead_beef);
181/// assert_eq!(buffer.len(), 64);
182/// assert_eq!(*buffer.allocation(), 0xdead_beef);
183/// ```
184pub trait Backend: Send + 'static {
185    /// One device allocation. The [`Buffer`] that carries it owns it.
186    type Allocation: Send + 'static;
187}
188
189/// A backend allocation the seam carries by identity, owning the handle itself.
190///
191/// The handle is the backend's; the seam carries the identity every consumer needs
192/// (domain, allocation, device), the byte length, and the allocation the adapter serves
193/// vendor and kernel calls with. A constructor is the backend allocator's, so identity
194/// and handle cannot disagree.
195#[doc(hidden)]
196pub struct Buffer<B: Backend> {
197    identity: StorageIdentity,
198    bytes: usize,
199    allocation: B::Allocation,
200}
201
202impl<B: Backend> Buffer<B> {
203    /// Build a seam buffer from a backend's identity, byte length and allocation.
204    #[doc(hidden)]
205    pub fn new(identity: StorageIdentity, bytes: usize, allocation: B::Allocation) -> Buffer<B> {
206        Buffer {
207            identity,
208            bytes,
209            allocation,
210        }
211    }
212
213    /// The allocation's identity.
214    #[doc(hidden)]
215    pub fn identity(&self) -> StorageIdentity {
216        self.identity
217    }
218
219    /// Allocation length in bytes.
220    #[doc(hidden)]
221    pub fn len(&self) -> usize {
222        self.bytes
223    }
224
225    /// Whether the allocation is empty.
226    #[doc(hidden)]
227    pub fn is_empty(&self) -> bool {
228        self.bytes == 0
229    }
230
231    /// The backend's own allocation handle.
232    #[doc(hidden)]
233    pub fn allocation(&self) -> &B::Allocation {
234        &self.allocation
235    }
236}
237
238impl<B: Backend> std::fmt::Debug for Buffer<B> {
239    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        formatter
241            .debug_struct("Buffer")
242            .field("identity", &self.identity)
243            .field("bytes", &self.bytes)
244            .finish_non_exhaustive()
245    }
246}