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}