cubecl_runtime/server/base.rs
1use super::{CaptureStatus, DeviceCaptures, Handle};
2use crate::kernel::BufferIOAttr;
3use crate::{
4 client::Client,
5 compiler::CompilationError,
6 config::{CubeClRuntimeConfig, RuntimeConfig, compilation::BoundsCheckMode},
7 dry_run::LaunchMode,
8 id::GraphId,
9 kernel::CubeKernel,
10 logging::ServerLogger,
11 memory_management::{
12 ManagedMemoryHandle, ManagedMemoryId, MemoryAllocationMode, StreamMemoryReport,
13 },
14 poison::DevicePoison,
15 server::{BufferBinding, KernelResource},
16 storage::{ComputeStorage, ManagedResource},
17 tma::{OobFill, TensorMapFormat, TensorMapInterleave, TensorMapPrefetch, TensorMapSwizzle},
18};
19use alloc::boxed::Box;
20use alloc::string::String;
21use alloc::sync::Arc;
22use alloc::vec::Vec;
23use core::{
24 fmt::{Debug, Display},
25 hash::{BuildHasher, Hash},
26};
27use cubecl_common::{
28 bytes::Bytes,
29 device::{self, DeviceId, ServiceId},
30 profile::ProfileDuration,
31};
32use cubecl_environment::backtrace::BackTrace;
33use cubecl_environment::collections::HashSet;
34use cubecl_environment::future::DynFut;
35use cubecl_environment::stream::StreamId;
36use cubecl_environment::sync::RwLock;
37use cubecl_ir::{DeviceProperties, ElemType, TargetProperties, settings::Dim3};
38use cubecl_zspace::{Shape, Strides, metadata::Metadata};
39use derive_more::{Deref, DerefMut, From};
40use foldhash::fast::FixedState;
41use itertools::Itertools;
42use thiserror::Error;
43
44#[derive(Error, Clone)]
45#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
46/// An error during profiling.
47pub enum ProfileError {
48 /// An unknown error happened during profiling
49 #[error(
50 "An unknown error happened during profiling\nCaused by:\n {reason}\nBacktrace:\n{backtrace}"
51 )]
52 Unknown {
53 /// The caused of the error
54 reason: String,
55 /// The captured backtrace.
56 #[cfg_attr(serializable, serde(skip))]
57 backtrace: BackTrace,
58 },
59
60 /// No profiling was registered
61 #[error("No profiling registered\nBacktrace:\n{backtrace}")]
62 NotRegistered {
63 /// The captured backtrace.
64 #[cfg_attr(serializable, serde(skip))]
65 backtrace: BackTrace,
66 },
67
68 /// The profiled window resolved no device timing.
69 ///
70 /// Distinct from a zero duration, which is a measurement that came back
71 /// instant. This is the absence of one: nothing the window enqueued was
72 /// timestamped, so the backend has nothing to report. A caller that only
73 /// wants a number can treat it as zero; a caller comparing candidates must
74 /// not, because an absence that reads as zero is the fastest result there
75 /// is and wins every comparison it enters.
76 #[error("The profiled window resolved no device timing\nBacktrace:\n{backtrace}")]
77 NotMeasured {
78 /// The captured backtrace.
79 #[cfg_attr(serializable, serde(skip))]
80 backtrace: BackTrace,
81 },
82
83 /// A launch error happened during profiling
84 #[error("A launch error happened during profiling\nCaused by:\n {0}")]
85 Launch(#[from] LaunchError),
86
87 /// An execution error happened during profiling
88 #[error("An execution error happened during profiling\nCaused by:\n {0}")]
89 Server(#[from] Box<ServerError>),
90}
91
92/// A failure during a profiling window invalidates the measurement, whatever
93/// the failure was. Every backend answers a launch, write or replay failure
94/// this way, so the conversion lives here rather than five times over.
95impl From<&ServerError> for ProfileError {
96 fn from(error: &ServerError) -> Self {
97 ProfileError::Server(Box::new(error.clone()))
98 }
99}
100
101impl core::fmt::Debug for ProfileError {
102 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103 f.write_fmt(format_args!("{self}"))
104 }
105}
106
107/// Contains many different types that are useful for server implementations and compute clients.
108pub struct ServerUtilities {
109 /// The time when `profile-tracy` is activated.
110 #[cfg(feature = "profile-tracy")]
111 pub epoch_time: cubecl_environment::time::Instant,
112 /// The GPU client when `profile-tracy` is activated.
113 #[cfg(feature = "profile-tracy")]
114 pub gpu_client: tracy_client::GpuContext,
115 /// The service these utilities belong to: what the handles it allocates
116 /// are stamped with, and what a client compares them against.
117 pub service: ServiceId,
118 /// The runtime name on this device, as logs and cache keys show it:
119 /// `cuda`, `wgpu<spirv>`.
120 pub name: &'static str,
121 /// Information shared between all servers.
122 pub properties: Arc<DeviceProperties>,
123 /// Stable hash of the device properties
124 pub properties_hash: u64,
125 /// What the target the server compiles for guarantees about its own
126 /// instructions — [`Runtime::target_properties`](crate::runtime::Runtime::target_properties)
127 /// resolved once, when the device came up, rather than on every launch.
128 ///
129 /// A kernel keeps a clone of this `Arc` so it can expand itself on the
130 /// device thread without naming a runtime, so building it per launch
131 /// would put a `TargetProperties` construction — and the allocations
132 /// inside it — on the hot path of every already-compiled kernel.
133 pub target_properties: Arc<TargetProperties>,
134 /// The logger based on global cubecl configs.
135 pub logger: Arc<ServerLogger>,
136 /// How to create the allocation.
137 pub layout_policy: Box<dyn MemoryLayoutPolicy>,
138 /// Whether the server can move data to a peer server of the same runtime
139 /// directly, without a round trip through the host: the device transport
140 /// `Client::has_device_transport` reports. Off unless the backend turns it
141 /// on at init.
142 pub server_comm_enabled: bool,
143 /// How to enforce bounds checking on kernels.
144 pub check_mode: BoundsCheckMode,
145 /// A set containing the ids for which the inter-device communication has already been initialized.
146 pub initialized_comms: RwLock<HashSet<CommunicationId>>,
147 /// The graph captures under way on the device, read-only: the streams update them through
148 /// the [`DeviceCaptures`] [`init`](Self::init) returns alongside.
149 pub captures: CaptureStatus,
150}
151
152/// Defines how the memory layout is determined.
153pub trait MemoryLayoutPolicy: Send + Sync + 'static {
154 /// Applies the memory layout policy to a list of descriptors.
155 ///
156 /// Returns a vector of `MemoryLayout`, one per descriptor, with layouts that share a
157 /// single `Binding`.
158 fn apply(
159 &self,
160 service: ServiceId,
161 stream_id: StreamId,
162 descriptors: &[MemoryLayoutDescriptor],
163 ) -> (Handle, Vec<MemoryLayout>);
164}
165
166impl core::fmt::Debug for ServerUtilities {
167 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
168 f.debug_struct("ServerUtilities")
169 .field("properties", &self.properties)
170 .field("name", &self.name)
171 .field("logger", &self.logger)
172 .finish()
173 }
174}
175
176impl ServerUtilities {
177 /// Creates the utilities of a device, with the [captures](DeviceCaptures) its streams
178 /// update.
179 ///
180 /// The utilities are shared with every client and only read the captures; the server hands
181 /// the returned writer to the streams it creates, and to nothing else.
182 pub fn init(
183 service: ServiceId,
184 name: &'static str,
185 properties: DeviceProperties,
186 target_properties: TargetProperties,
187 logger: Arc<ServerLogger>,
188 allocator: impl MemoryLayoutPolicy,
189 ) -> (Self, DeviceCaptures) {
190 let captures = DeviceCaptures::default();
191 // Start a tracy client if needed.
192 #[cfg(feature = "profile-tracy")]
193 let client = tracy_client::Client::start();
194
195 let utilities = Self {
196 service,
197 name,
198 properties_hash: properties.checksum(),
199 properties: Arc::new(properties),
200 target_properties: Arc::new(target_properties),
201 logger,
202 // Create the GPU client if needed.
203 #[cfg(feature = "profile-tracy")]
204 gpu_client: client
205 .clone()
206 .new_gpu_context(
207 Some(name),
208 // In the future should ask the server what makes sense here. 'Invalid' atm is a generic stand-in (Tracy doesn't have CUDA/RocM atm anyway).
209 tracy_client::GpuContextType::Invalid,
210 0, // Timestamps are manually aligned to this epoch so start at 0.
211 1.0, // Timestamps are manually converted to be nanoseconds so period is 1.
212 )
213 .unwrap(),
214 #[cfg(feature = "profile-tracy")]
215 epoch_time: cubecl_environment::time::Instant::now(),
216 layout_policy: Box::new(allocator),
217 server_comm_enabled: false,
218 check_mode: CubeClRuntimeConfig::get().compilation.check_mode,
219 initialized_comms: RwLock::new(HashSet::default()),
220 captures: captures.status(),
221 };
222
223 (utilities, captures)
224 }
225}
226
227/// Kernel Launch Errors.
228#[derive(Error, Clone)]
229#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
230pub enum LaunchError {
231 /// The given kernel can't be compiled.
232 #[error("A compilation error happened during launch\nCaused by:\n {0}")]
233 CompilationError(#[from] CompilationError),
234
235 /// The server is out of memory.
236 #[error(
237 "An out-of-memory error happened during launch\nCaused by:\n {reason}\nBacktrace\n{backtrace}"
238 )]
239 OutOfMemory {
240 /// The caused of the memory error.
241 reason: String,
242 /// The backtrace for this error.
243 #[cfg_attr(serializable, serde(skip))]
244 backtrace: BackTrace,
245 },
246
247 /// Too many resources were requested
248 #[error("Too many resources were requested during launch\n{0}")]
249 TooManyResources(#[from] ResourceLimitError),
250
251 /// The device is poisoned: a kernel faulted in a way the driver makes
252 /// sticky (an illegal address, a trap, a hardware fault, etc.)
253 #[error("The device was poisoned during launch\nCaused by:\n {0}")]
254 DevicePoisoned(#[from] DevicePoison),
255
256 /// Unknown launch error.
257 #[error(
258 "An unknown error happened during launch\nCaused by:\n {reason}\nBacktrace\n{backtrace}"
259 )]
260 Unknown {
261 /// The caused of the unknown error.
262 reason: String,
263 /// The backtrace for this error.
264 #[cfg_attr(serializable, serde(skip))]
265 backtrace: BackTrace,
266 },
267}
268
269/// Resource limit errors.
270#[derive(Error, Clone)]
271#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
272pub enum ResourceLimitError {
273 /// Shared memory exceeds maximum
274 #[error(
275 "Too much shared memory requested.\nRequested {requested} bytes, maximum {max} bytes available.\nBacktrace\n{backtrace}"
276 )]
277 SharedMemory {
278 /// Value requested
279 requested: usize,
280 /// Maximum value
281 max: usize,
282 /// The backtrace for this error.
283 #[cfg_attr(serializable, serde(skip))]
284 backtrace: BackTrace,
285 },
286 /// Total units exceeds maximum
287 #[error(
288 "Total unit count exceeds maximum.\nRequested {requested} units, max units is {max}.\nBacktrace\n{backtrace}"
289 )]
290 Units {
291 /// Requested value
292 requested: u32,
293 /// Maximum value
294 max: u32,
295 /// The backtrace for this error.
296 #[cfg_attr(serializable, serde(skip))]
297 backtrace: BackTrace,
298 },
299 /// `CubeDim` exceeds maximum
300 #[error(
301 "Cube dim exceeds maximum bounds.\nRequested {requested:?}, max is {max:?}.\nBacktrace\n{backtrace}"
302 )]
303 CubeDim {
304 /// Requested value
305 requested: (u32, u32, u32),
306 /// Maximum value
307 max: (u32, u32, u32),
308 /// The backtrace for this error.
309 #[cfg_attr(serializable, serde(skip))]
310 backtrace: BackTrace,
311 },
312}
313
314impl LaunchError {
315 /// Whether the device that emitted the error is poisoned.
316 pub fn is_device_poisoned(&self) -> bool {
317 match self {
318 Self::DevicePoisoned(_) => true,
319 Self::CompilationError(error) => error.is_device_poisoned(),
320 _ => false,
321 }
322 }
323
324 /// Whether this is the kernel being refused before it ran.
325 pub fn is_refusal(&self) -> bool {
326 match self {
327 Self::CompilationError(error) => error.is_refusal(),
328 Self::TooManyResources(_) => true,
329 _ => false,
330 }
331 }
332}
333
334impl core::fmt::Debug for LaunchError {
335 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
336 f.write_fmt(format_args!("{self}"))
337 }
338}
339
340impl core::fmt::Debug for ResourceLimitError {
341 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
342 f.write_fmt(format_args!("{self}"))
343 }
344}
345
346/// A collective operation between the devices of one runtime.
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
349pub enum Collective {
350 /// Setting up the communication between a group of devices.
351 CommInit,
352 /// Reducing a buffer across a group of devices.
353 AllReduce,
354 /// Sending a buffer to a peer device.
355 Send,
356 /// Receiving a buffer from a peer device.
357 Recv,
358 /// Waiting for queued collectives to finish.
359 SyncCollective,
360}
361
362impl Display for Collective {
363 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
364 f.write_str(match self {
365 Self::CommInit => "comm_init",
366 Self::AllReduce => "all_reduce",
367 Self::Send => "send",
368 Self::Recv => "recv",
369 Self::SyncCollective => "sync_collective",
370 })
371 }
372}
373
374/// Error that can happen asynchronously while executing registered kernels.
375#[derive(Error, Clone)]
376#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
377pub enum ServerError {
378 /// A runtime validation error
379 #[error(
380 "A validation error happened during execution\nCaused by:\n {message}\nBacktrace:\n{backtrace}"
381 )]
382 Validation {
383 /// The details of the validation error.
384 message: String,
385 /// The backtrace for this error.
386 #[cfg_attr(serializable, serde(skip))]
387 backtrace: BackTrace,
388 },
389
390 /// A generic runtime error.
391 #[error("An error happened during execution\nCaused by:\n {reason}\nBacktrace:\n{backtrace}")]
392 Generic {
393 /// The details of the generic error.
394 reason: String,
395 /// The backtrace for this error.
396 #[cfg_attr(serializable, serde(skip))]
397 backtrace: BackTrace,
398 },
399
400 /// The runtime has no transport between its devices, so the collective was
401 /// not run. Its own variant rather than a `Generic` reason, so a caller can
402 /// tell a runtime that cannot do this apart from a collective that failed.
403 #[error(
404 "{operation} needs a transport between devices, and this runtime has none\nBacktrace:\n{backtrace}"
405 )]
406 NoDeviceTransport {
407 /// The collective that was asked for.
408 operation: Collective,
409 /// The backtrace for this error.
410 #[cfg_attr(serializable, serde(skip))]
411 backtrace: BackTrace,
412 },
413
414 /// A handle from one service was handed to a client of another: memory
415 /// coordinates mean nothing there, so nothing was run.
416 #[error("A handle of {handle} was used on {client}\nBacktrace:\n{backtrace}")]
417 ForeignHandle {
418 /// The service whose memory the handle addresses.
419 handle: String,
420 /// The service the client reaches.
421 client: String,
422 /// The backtrace for this error.
423 #[cfg_attr(serializable, serde(skip))]
424 backtrace: BackTrace,
425 },
426
427 /// A caller named a server type the client does not reach. The client is
428 /// erased over its server, so the type it is asked for is checked against
429 /// the one it was built from, and nothing was run.
430 #[error("The client reaches {client}, not a {requested}\nBacktrace:\n{backtrace}")]
431 ServiceMismatch {
432 /// The service the client reaches.
433 client: String,
434 /// The server type the caller asked for.
435 requested: String,
436 /// The backtrace for this error.
437 #[cfg_attr(serializable, serde(skip))]
438 backtrace: BackTrace,
439 },
440
441 /// The device is poisoned: a kernel faulted in a way the driver makes
442 /// sticky (an illegal address, a trap, a hardware fault, etc.)
443 #[error("The device is poisoned\nCaused by:\n {0}")]
444 DevicePoisoned(#[from] DevicePoison),
445
446 /// A launch error happened
447 #[error("A launch error happened\nCaused by:\n {0}")]
448 Launch(#[from] LaunchError),
449
450 /// An IO error happened
451 #[error("An IO error happened\nCaused by:\n {0}")]
452 Io(#[from] IoError),
453
454 /// The work writing this buffer was torn down before it could say what
455 /// went wrong: its write scope never reached the exit that names the real
456 /// failure, which a panic mid-launch explains.
457 ///
458 /// This is the provisional error every write scope enters with, so it
459 /// carries no payload and captures no backtrace — a launch that succeeds
460 /// mints one and drops it again, and paying a `String` and a stack walk
461 /// per launch for the message nobody normally reads is the whole reason
462 /// it is a variant rather than a [`Generic`](Self::Generic).
463 #[error(
464 "The work writing this buffer was torn down before it could say what went wrong: its \
465 write scope never reached the exit that names the real failure, which a panic \
466 mid-launch explains"
467 )]
468 TornDown,
469
470 /// The bytes asked about were never written: the work that was going to
471 /// write them failed, or was skipped downstream of a failure. `chain`
472 /// walks from the buffer asked about back toward the root, newest skip
473 /// first, and `root` is the failure that started it.
474 #[error(
475 "The bytes were never written (failure #{failure}, still claiming {claimed} buffer(s))\n{}Caused by:\n {root}\nAsked at:\n{backtrace}",
476 chain.iter().map(|hop| alloc::format!(" {hop}\n")).collect::<String>()
477 )]
478 Unwritten {
479 /// The failure's id in the device's error store, as printed by every
480 /// other read that trips over the same failure.
481 failure: u64,
482 /// How many buffers the failure still claims.
483 claimed: u32,
484 /// The skip chain from the buffer asked about back toward the root.
485 chain: Vec<String>,
486 /// The failure that started it, backtrace included.
487 root: Box<ServerError>,
488 /// Where the question was asked, so the lazy report and the read that
489 /// tripped over it can be tied together.
490 #[cfg_attr(serializable, serde(skip))]
491 backtrace: BackTrace,
492 },
493
494 /// The work did not run, because an input it needed carried a failure.
495 ///
496 /// The report is on the buffers: the work's outputs claim the failure its
497 /// inputs did, so a read of one of them names the root cause and the path
498 /// back to it. This variant says only *that* the caller's work was
499 /// skipped, which is why it carries no payload — the failure the inputs
500 /// held is not the caller's to receive here, and minting a formatted
501 /// message per skip would cost the loop that skips on every iteration.
502 #[error(
503 "The work was skipped: an input carried a failure, and the work's outputs claim it now \
504 — a read of one of them names the root cause"
505 )]
506 Skipped,
507
508 /// More than one thing went wrong at once, and the caller is owed all of
509 /// them: a read naming buffers that several distinct failures claim, or a
510 /// capture that was both doomed and abandoned.
511 #[error("Several failures at once\nCaused by:\n {}", errors.iter().join("\n"))]
512 Several {
513 /// The failures, in the order they were found.
514 errors: Vec<Self>,
515 /// The backtrace for this error.
516 #[cfg_attr(serializable, serde(skip))]
517 backtrace: BackTrace,
518 },
519}
520
521impl Debug for ServerError {
522 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
523 write!(f, "{self}")
524 }
525}
526
527impl ServerError {
528 /// The error every collective returns on a runtime with no transport between devices.
529 pub fn no_device_transport(operation: Collective) -> Self {
530 Self::NoDeviceTransport {
531 operation,
532 backtrace: BackTrace::capture(),
533 }
534 }
535
536 /// Whether this is the kernel being refused before it ran, rather than
537 /// something going wrong while running it.
538 ///
539 /// The distinction a test harness or an autotuner needs: a kernel a
540 /// backend cannot build at this configuration is a candidate to drop or a
541 /// case to skip, while a fault, an out-of-memory, an IO failure or a
542 /// kernel that panicked while expanding is a defect that has to be
543 /// reported. Answering it by reading the message is
544 /// how a harness ends up accepting the second as the first.
545 ///
546 /// Walks [`Several`](Self::Several) and [`Unwritten`](Self::Unwritten) to
547 /// the roots, because a read of an unwritten buffer reports the failure
548 /// that stopped its writer and that is where the distinction lives. A
549 /// group answers yes only when every root does: one real failure among
550 /// refusals is still a real failure, and an empty group refuses nothing.
551 pub fn is_refusal(&self) -> bool {
552 match self {
553 Self::Launch(error) => error.is_refusal(),
554 Self::Unwritten { root, .. } => root.is_refusal(),
555 Self::Several { errors, .. } => {
556 !errors.is_empty() && errors.iter().all(Self::is_refusal)
557 }
558 _ => false,
559 }
560 }
561
562 /// Whether the device that emitted the error is poisoned.
563 ///
564 /// The other half of [`is_refusal`](Self::is_refusal): a refusal says
565 /// "drop this candidate", a poisoned device says "drop the device". Anything
566 /// else — an out-of-memory, a failed copy, a skipped launch — leaves the
567 /// device usable, and redoing the work may well succeed.
568 ///
569 /// Walks [`Several`](Self::Several) and [`Unwritten`](Self::Unwritten) to
570 /// the roots, and answers yes when *any* root poisoned the device: one poisoned
571 /// device among other failures still means nothing on it can be trusted.
572 pub fn is_device_poisoned(&self) -> bool {
573 match self {
574 Self::DevicePoisoned(_) => true,
575 Self::Launch(error) => error.is_device_poisoned(),
576 Self::Io(error) => error.is_device_poisoned(),
577 Self::Unwritten { root, .. } => root.is_device_poisoned(),
578 Self::Several { errors, .. } => errors.iter().any(Self::is_device_poisoned),
579 _ => false,
580 }
581 }
582
583 /// A graph-capture call the stream's lifecycle does not allow — a
584 /// `begin_capture` without `graph_prepare`, a second overlapping capture, a
585 /// replay of an unknown graph, or an operation a capture window cannot
586 /// record. `reason` names the call and what was wrong with it.
587 pub fn graph_state(reason: impl Into<String>) -> Self {
588 Self::Generic {
589 reason: reason.into(),
590 backtrace: BackTrace::capture(),
591 }
592 }
593
594 /// The error the default (unsupported) graph-capture methods return, for a
595 /// backend that has no graph support at all.
596 pub fn graph_capture_unsupported() -> Self {
597 Self::graph_state("graph capture is not supported by this backend")
598 }
599}
600
601/// The compute server is responsible for handling resources and computations over resources.
602///
603/// Everything in the server is mutable, therefore it should be solely accessed through the
604/// [`Client`] for thread safety.
605pub trait Server:
606 core::any::Any + Send + core::fmt::Debug + ServerCommunication + device::DeviceService + 'static
607{
608 /// Initializes [memory](ManagedMemoryHandle) on the given [stream](StreamId) with the given size.
609 ///
610 /// # Errors
611 ///
612 /// Returns a [`ServerError`] when the memory could not be initialized, e.g. because the stream
613 /// cannot be created on a poisoned device. The memory then carries the error: the next
614 /// sync point reading that memory reports it.
615 fn initialize_memory(
616 &mut self,
617 memory: ManagedMemoryHandle,
618 size: u64,
619 stream_id: StreamId,
620 ) -> Result<(), ServerError>;
621
622 /// Reserves N [Bytes] of the provided sizes to be used as staging to load data.
623 fn staging(
624 &mut self,
625 _sizes: &[usize],
626 _stream_id: StreamId,
627 ) -> Result<Vec<Bytes>, ServerError> {
628 Err(IoError::UnsupportedIoOperation {
629 backtrace: BackTrace::capture(),
630 }
631 .into())
632 }
633
634 /// Retrieve the server logger.
635 fn logger(&self) -> Arc<ServerLogger>;
636
637 /// Retrieve the server utilities.
638 fn utilities(&self) -> Arc<ServerUtilities>;
639
640 /// Given bindings, returns the owned resources as bytes.
641 ///
642 /// # Errors
643 ///
644 /// [`ServerError::Several`] when the work that was supposed to
645 /// write one of these buffers failed, whichever stream it ran on — copying
646 /// bytes out would hand back whatever was in memory before. Every
647 /// implementation asks
648 /// `FailureStore::ensure_written` (in `cubecl-server`) before it copies
649 /// anything.
650 fn read(
651 &mut self,
652 descriptors: Vec<CopyDescriptor>,
653 stream_id: StreamId,
654 ) -> DynFut<Result<Vec<Bytes>, ServerError>>;
655
656 /// Writes the specified bytes into the buffers given
657 fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId);
658
659 /// Wait for the completion of every task in the server, then answer for
660 /// `handles`: the barrier first, so device faults count, and then the
661 /// claim check a read would have made — a read without the read.
662 ///
663 /// An empty `handles` is the plain barrier plus the device fault, which
664 /// is the only failure left that no buffer can report.
665 fn sync(
666 &mut self,
667 handles: Vec<BufferBinding>,
668 stream_id: StreamId,
669 ) -> DynFut<Result<(), ServerError>>;
670
671 /// Whether the bytes the handles name can be trusted, right now and with
672 /// no barrier: the claim check a read makes, without the read. Instant —
673 /// enqueue-time failures only. A device fault needs [`sync`](Self::sync),
674 /// which drains first.
675 fn check(
676 &mut self,
677 handles: Vec<BufferBinding>,
678 stream_id: StreamId,
679 ) -> Result<(), ServerError>;
680
681 /// Executes the `kernel` over the given memory `handles`.
682 ///
683 /// Kernels have mutable access to every resource they are given
684 /// and are responsible of determining which should be read or written.
685 ///
686 /// `launch_mode` says whether the kernel actually runs. On
687 /// [`LaunchMode::Skip`] the server must still do everything a first launch
688 /// does short of dispatching — expand, compile, validate, fill its caches —
689 /// and then drop the launch; skipping the compilation instead would defeat
690 /// the whole point of a [dry run](crate::dry_run).
691 ///
692 /// # Safety
693 ///
694 /// When executing with mode [`ExecutionMode::Unchecked`], out-of-bound reads and writes can happen.
695 unsafe fn launch(
696 &mut self,
697 kernel: Box<dyn CubeKernel>,
698 count: CubeCount,
699 bindings: KernelArguments,
700 stream_id: StreamId,
701 launch_mode: LaunchMode,
702 );
703
704 /// Flush all outstanding tasks in the server.
705 ///
706 /// # Errors
707 ///
708 /// The device fault, when the context itself is broken — a launch failure
709 /// is not the flush's to report: it lives on the buffers the launch left
710 /// unwritten, and surfaces on any read, sync or check of them.
711 fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError>;
712
713 /// Prepare `stream_id` for an upcoming graph capture. Call this
714 /// **before** the warmup run: the capture window allocates nothing, so it
715 /// reuses what the warmup run left in the pools, and every page the
716 /// recording touches is guarded for the graph's lifetime.
717 ///
718 /// Prefer having kernels already **autotuned before** this call, so the
719 /// warmup run allocates what the recorded run asks for and nothing else.
720 ///
721 /// A no-op by default (harmless on backends without graph support).
722 fn graph_prepare(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
723 let _ = stream_id;
724 Ok(())
725 }
726
727 /// Begin recording the launches issued on `stream_id` into a graph instead
728 /// of executing them, so the sequence can later be
729 /// [replayed](Server::replay) without paying the launch path again.
730 /// Call [`graph_prepare`](Server::graph_prepare) and warm up first.
731 ///
732 /// Between this call and [`end_capture`](Server::end_capture) the
733 /// stream must not synchronize — a read, a sync or a profile either aborts
734 /// the capture or is refused — and should not allocate fresh device memory,
735 /// which `graph_prepare` plus a warmup run is what avoids. Whether an
736 /// operation the window cannot record fails the call or fails
737 /// `end_capture`, and whether a mid-window allocation is fatal, is the
738 /// backend's to say; see `StreamCapture` in `cubecl-server`.
739 ///
740 /// The default is unsupported. Two shapes of backend override it: a
741 /// **hardware graph** (CUDA, HIP), where the driver records a replayable
742 /// graph object, and a **software graph** (wgpu), where the runtime records
743 /// fully-resolved dispatches and re-encodes them on replay.
744 fn begin_capture(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
745 let _ = stream_id;
746 Err(ServerError::graph_capture_unsupported())
747 }
748
749 /// Stop recording (see [`begin_capture`](Server::begin_capture)),
750 /// store the captured graph in the backend's registry, and return its
751 /// [`GraphId`], ready to [replay](Server::replay).
752 fn end_capture(&mut self, stream_id: StreamId) -> Result<GraphId, ServerError> {
753 let _ = stream_id;
754 Err(ServerError::graph_capture_unsupported())
755 }
756
757 /// Replay the graph identified by `graph` on `stream_id`, re-running the
758 /// whole recorded launch sequence against its original buffers. A hardware
759 /// graph replays as a single dispatch; a software graph re-encodes the
760 /// recorded dispatches, which is still far cheaper than the launch path but
761 /// stays O(n) in recorded launches.
762 ///
763 /// The call enqueues the dispatch and returns without waiting for the
764 /// device; what it reports is the enqueue — an unknown or destroyed
765 /// graph, a refusal — since a caller replaying a graph is standing right
766 /// there. A failure also leaves the graph's write set carrying it, so a
767 /// read of those buffers fails until a replay lands. Unsupported by
768 /// default: a [`GraphId`] can only come from
769 /// [`end_capture`](Server::end_capture).
770 fn replay(&mut self, graph: GraphId, stream_id: StreamId) -> Result<(), ServerError> {
771 let _ = (graph, stream_id);
772 Err(ServerError::graph_capture_unsupported())
773 }
774
775 /// Release the graph identified by `graph`, destroying whatever it recorded
776 /// and unpinning the buffers it retained. Replay returns at enqueue time,
777 /// so the backend must guarantee no in-flight replay can still read those
778 /// buffers once they return to the pool — by syncing `stream_id` where
779 /// nothing weaker will do (CUDA, HIP), or by relying on the queue ordering
780 /// that already places a submitted replay ahead of any later write (wgpu).
781 /// A no-op by default and for an unknown id.
782 fn graph_destroy(&mut self, graph: GraphId, stream_id: StreamId) {
783 let _ = (graph, stream_id);
784 }
785
786 /// Structured per-pool report of the given stream's **main GPU** memory:
787 /// each pool's shape, usage, and high-water marks, in allocation-routing
788 /// order. The read side of a measured memory plan — see
789 /// `MemoryManagement::memory_report` in `cubecl-server`.
790 ///
791 /// `None` when the stream was never created.
792 fn memory_report(&mut self, stream_id: StreamId) -> Option<StreamMemoryReport>;
793
794 /// Stream ids the client should iterate to aggregate across the device.
795 ///
796 /// Default is just the calling stream, which is correct for
797 /// non-multi-stream backends; multi-stream backends override to
798 /// return one id per initialized stream pool slot.
799 fn stream_ids(&self) -> Vec<StreamId> {
800 Vec::from([StreamId::current()])
801 }
802
803 /// Ask the server to release memory that it can release.
804 ///
805 /// # Errors
806 ///
807 /// Refused while a stream records a graph: releasing memory waits on the
808 /// device, and a wait on a stream that records aborts its capture.
809 fn memory_cleanup(&mut self, stream_id: StreamId) -> Result<(), ServerError>;
810
811 /// Enable collecting timestamps.
812 fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError>;
813
814 /// Disable collecting timestamps.
815 fn end_profile(
816 &mut self,
817 stream_id: StreamId,
818 token: ProfilingToken,
819 ) -> Result<ProfileDuration, ProfileError>;
820
821 /// Drop the window `token` opened without measuring it, for a caller that
822 /// will never close it with [`end_profile`](Self::end_profile).
823 ///
824 /// An open window is not free: depending on the backend it retains
825 /// command buffers, keeps timestamp writes on, or holds a device event.
826 ///
827 /// Every backend overrides this, and should: the default closes the window
828 /// and throws the measurement away, which is the most expensive way to be
829 /// rid of it — [`end_profile`](Self::end_profile) is where the syncing and
830 /// flushing live, and this is the one call that needs none of it.
831 fn abandon_profile(&mut self, stream_id: StreamId, token: ProfilingToken) {
832 let _ = self.end_profile(stream_id, token);
833 }
834
835 /// Update the memory mode of allocation in the server.
836 fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId);
837}
838
839/// The storage a server allocates from, and the native resources it hands
840/// out. Kept off [`Server`] so that trait is object-safe: a resource's
841/// type is the backend's own, and only a caller that names the backend can
842/// receive one.
843pub trait ServerStorage: Server {
844 /// The [storage](ComputeStorage) type defines how data is stored and accessed.
845 type Storage: ComputeStorage;
846 /// Given a resource handle, returns the storage resource.
847 ///
848 /// The same claim check a read makes guards this too: a buffer a failed
849 /// launch never filled reports the failure rather than handing back a
850 /// pointer to whatever was there before. It costs a field read on a slice
851 /// the resolution walks anyway.
852 fn get_resource(
853 &mut self,
854 binding: BufferBinding,
855 stream_id: StreamId,
856 ) -> Result<ManagedResource<<Self::Storage as ComputeStorage>::Resource>, ServerError>;
857}
858
859/// An ID unique to any unordered combination of devices.
860#[derive(Clone, Debug, Hash, Eq, PartialEq)]
861pub struct CommunicationId {
862 /// The ID as a `String`.
863 pub id: u64,
864}
865
866impl From<Vec<DeviceId>> for CommunicationId {
867 fn from(mut value: Vec<DeviceId>) -> Self {
868 // Make sure that device ids are sorted so that any combination of the same devices uses the same communicator.
869 value.sort();
870 CommunicationId {
871 id: FixedState::default().hash_one(value),
872 }
873 }
874}
875
876/// Different reduce operations.
877pub enum ReduceOperation {
878 /// Sum.
879 Sum,
880 /// Mean.
881 Mean,
882}
883
884/// Defines functions for optimized data transfer between servers, supporting custom communication
885/// mechanisms such as peer-to-peer communication or specialized implementations.
886///
887/// # Inside the tainted-buffer rules
888///
889/// A collective reads a source buffer and produces a destination one, and owes
890/// the same two answers the rest of the server gives: ask whether the source
891/// carries a failure on the way in (as [`read`](Server::read) does
892/// through
893/// `FailureStore::ensure_written` in `cubecl-server`),
894/// and taint the destination on the way out when the operation fails (as a
895/// failed [`launch`](Server::launch) does). Skipping either lets a
896/// collective reduce stale bytes across every device in the group, or leave a
897/// destination that reads back clean when nothing wrote it.
898///
899/// The default methods are for a runtime with no transport between its devices:
900/// each returns [`ServerError::NoDeviceTransport`] before touching any buffer, so
901/// there is no destination to taint.
902pub trait ServerCommunication {
903 /// Ensure that all queued collective operations have been executed.
904 ///
905 /// # Arguments
906 ///
907 /// * `stream_id` - The [`StreamId`] of the stream waiting for the sync.
908 ///
909 /// # Returns
910 ///
911 /// Returns a `Result` containing an `ServerError` if the operation fails.
912 ///
913 /// # Errors
914 ///
915 /// The default returns [`ServerError::NoDeviceTransport`].
916 #[allow(unused_variables)]
917 fn sync_collective(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
918 Err(ServerError::no_device_transport(Collective::SyncCollective))
919 }
920
921 /// Initialize the communication between the devices in `device_ids`.
922 ///
923 /// # Arguments
924 ///
925 /// * `device_ids` - The IDs of the devices that need communication.
926 ///
927 /// # Returns
928 ///
929 /// Returns a `Result` containing an `ServerError` if the operation fails.
930 ///
931 /// # Errors
932 ///
933 /// The default returns [`ServerError::NoDeviceTransport`].
934 #[allow(unused_variables)]
935 fn comm_init(&mut self, device_ids: Vec<DeviceId>) -> Result<(), ServerError> {
936 Err(ServerError::no_device_transport(Collective::CommInit))
937 }
938
939 /// Performs an `all_reduce` operation on the input data and writes it to the output buffer.
940 /// see <https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html#allreduce>
941 ///
942 /// # Arguments
943 ///
944 /// * `src` - The data to be reduced.
945 /// * `dst` - Where to write the result.
946 /// * `dtype` - The element type of the data being reduced
947 /// * `stream_id` - The data's stream id.
948 /// * `op` - The reduce's aggregation operation e.g. mean, sum, etc.
949 /// * `device_ids` - The list of device ids from which to `all_reduce`.
950 ///
951 /// # Returns
952 ///
953 /// Returns a `Result` containing an `ServerError` if the operation fails.
954 ///
955 /// # Errors
956 ///
957 /// The default returns [`ServerError::NoDeviceTransport`].
958 #[allow(unused_variables)]
959 fn all_reduce(
960 &mut self,
961 src: BufferBinding,
962 dst: BufferBinding,
963 dtype: ElemType,
964 stream_id: StreamId,
965 op: ReduceOperation,
966 device_ids: Vec<DeviceId>,
967 ) -> Result<(), ServerError> {
968 Err(ServerError::no_device_transport(Collective::AllReduce))
969 }
970
971 /// Sends data from this server to a destination server.
972 ///
973 /// # Arguments
974 ///
975 /// * `desc` - A descriptor specifying the data to be sent, including shape, strides, and binding.
976 /// * `dtype` - The element type of the data being sent.
977 /// * `stream_id` - The stream ID associated with the server's operation.
978 /// * `device_id_dst` - ID of the device receiving the data.
979 ///
980 /// # Returns
981 ///
982 /// Returns a `Result` containing an `ServerError` if the operation fails.
983 ///
984 /// # Known limitation
985 ///
986 /// Send and recv are posted fire-and-forget on two devices and block for
987 /// each other, so a send that refuses — a source whose writer failed,
988 /// above all — leaves the peer's already-posted recv waiting on its
989 /// communication stream with no way to recall it from here. The refusal
990 /// is still right: completing the send would launder stale bytes onto a
991 /// handle that carries no claim on the other device. Cross-device
992 /// failure propagation needs a design pass of its own.
993 ///
994 /// # Errors
995 ///
996 /// The default returns [`ServerError::NoDeviceTransport`].
997 #[allow(unused_variables)]
998 fn send(
999 &mut self,
1000 desc: CopyDescriptor,
1001 dtype: ElemType,
1002 stream_id: StreamId,
1003 device_id_dst: DeviceId,
1004 ) -> Result<(), ServerError> {
1005 Err(ServerError::no_device_transport(Collective::Send))
1006 }
1007
1008 /// Receive data from another server.
1009 ///
1010 /// # Arguments
1011 ///
1012 /// * `handle` - The handle in which the received data is written.
1013 /// * `dtype` - The element type of the data being sent.
1014 /// * `stream_id` - The stream ID associated with the server's operation.
1015 /// * `device_id_src` - ID of the device sending the data.
1016 ///
1017 /// # Returns
1018 ///
1019 /// Returns a `Result` containing an `ServerError` if the operation fails.
1020 ///
1021 /// # Errors
1022 ///
1023 /// The default returns [`ServerError::NoDeviceTransport`].
1024 #[allow(unused_variables)]
1025 fn recv(
1026 &mut self,
1027 handle: Handle,
1028 dtype: ElemType,
1029 stream_id: StreamId,
1030 device_id_src: DeviceId,
1031 ) -> Result<(), ServerError> {
1032 Err(ServerError::no_device_transport(Collective::Recv))
1033 }
1034}
1035
1036#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
1037/// Profiling identification so that the server can support recursive and overlapping profilings.
1038pub struct ProfilingToken {
1039 /// The token value.
1040 pub id: u64,
1041}
1042
1043/// Type of allocation, either contiguous or optimized (row-aligned when possible)
1044#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
1045pub enum MemoryLayoutStrategy {
1046 /// Contiguous layout, with no padding
1047 Contiguous,
1048 /// Optimized for access speed. In practice this means row-aligned with padding for runtimes
1049 /// that support it.
1050 Optimized,
1051}
1052
1053/// Descriptor for a new tensor allocation
1054#[derive(new, Debug, Clone)]
1055pub struct MemoryLayoutDescriptor {
1056 /// Strategy used to create the memory layout.
1057 pub strategy: MemoryLayoutStrategy,
1058 /// Shape of the tensor
1059 pub shape: Shape,
1060 /// Size of each element in the tensor (used for conversion of shape to bytes)
1061 pub elem_size: usize,
1062}
1063
1064impl MemoryLayoutDescriptor {
1065 /// Create an optimized allocation descriptor
1066 pub fn optimized(shape: Shape, elem_size: usize) -> Self {
1067 MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Optimized, shape, elem_size)
1068 }
1069
1070 /// Create a contiguous allocation descriptor
1071 pub fn contiguous(shape: Shape, elem_size: usize) -> Self {
1072 MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Contiguous, shape, elem_size)
1073 }
1074}
1075
1076/// An allocation with associated strides. Strides depend on tensor layout.
1077#[derive(Debug, Clone)]
1078pub struct MemoryLayout {
1079 /// The handle for the memory resource
1080 pub memory: Handle,
1081 /// TODO: `Strides` should become `Layout`.
1082 ///
1083 /// The strides of the tensor
1084 pub strides: Strides,
1085}
1086
1087impl MemoryLayout {
1088 /// Create a new memory layout.
1089 pub fn new(handle: Handle, strides: impl Into<Strides>) -> Self {
1090 MemoryLayout {
1091 memory: handle,
1092 strides: strides.into(),
1093 }
1094 }
1095}
1096
1097/// A reason for an error.
1098#[derive(Default, Clone)]
1099pub struct Reason {
1100 inner: ReasonInner,
1101}
1102
1103#[cfg(serializable)]
1104mod _reason_serde {
1105 use super::*;
1106
1107 use alloc::string::ToString;
1108 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1109
1110 impl Serialize for Reason {
1111 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1112 where
1113 S: Serializer,
1114 {
1115 // Use the Display implementation (via to_string) to flatten the enum
1116 serializer.serialize_str(&self.to_string())
1117 }
1118 }
1119
1120 impl<'de> Deserialize<'de> for Reason {
1121 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1122 where
1123 D: Deserializer<'de>,
1124 {
1125 // Deserialize into a standard String first
1126 let s = String::deserialize(deserializer)?;
1127
1128 // Wrap it in the Dynamic variant since we can't safely
1129 // reconstruct a 'static str from a runtime string.
1130 Ok(Reason {
1131 inner: ReasonInner::Dynamic(Arc::new(s)),
1132 })
1133 }
1134 }
1135}
1136
1137#[derive(Default, Clone)]
1138enum ReasonInner {
1139 Static(&'static str),
1140 Dynamic(Arc<String>),
1141 #[default]
1142 NotProvided,
1143}
1144
1145impl core::fmt::Display for Reason {
1146 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1147 match &self.inner {
1148 ReasonInner::Static(content) => f.write_str(content),
1149 ReasonInner::Dynamic(content) => f.write_str(content),
1150 ReasonInner::NotProvided => f.write_str("No reason provided for the error"),
1151 }
1152 }
1153}
1154
1155impl core::fmt::Debug for Reason {
1156 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1157 core::fmt::Display::fmt(&self, f)
1158 }
1159}
1160
1161impl From<&'static str> for Reason {
1162 fn from(value: &'static str) -> Self {
1163 Self {
1164 inner: ReasonInner::Static(value),
1165 }
1166 }
1167}
1168
1169impl From<String> for Reason {
1170 fn from(value: String) -> Self {
1171 Self {
1172 inner: ReasonInner::Dynamic(Arc::new(value)),
1173 }
1174 }
1175}
1176
1177/// Error returned from `create`/`read`/`write` functions. Due to async execution not all errors
1178/// are able to be caught, so some IO errors will still panic.
1179#[derive(Error, Clone)]
1180#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
1181pub enum IoError {
1182 /// Buffer size exceeds the max available
1183 #[error("can't allocate buffer of size: {size}\n{backtrace}")]
1184 BufferTooBig {
1185 /// The size of the buffer in bytes.
1186 size: u64,
1187 /// The captured backtrace.
1188 #[cfg_attr(serializable, serde(skip))]
1189 backtrace: BackTrace,
1190 },
1191
1192 /// The device had no memory left for this allocation.
1193 ///
1194 /// Unlike [`IoError::BufferTooBig`] (the allocation can *never* fit), this
1195 /// describes the device at one moment: pool pages whose slices have all
1196 /// been dropped are still resident, and the frees that would release them
1197 /// may not have reached the driver yet. Reclaiming and retrying is a
1198 /// reasonable response, which is why a storage backend must not report a
1199 /// driver out-of-memory as `BufferTooBig`: that tells every caller the
1200 /// allocation is hopeless when it is merely untimely.
1201 #[error("out of device memory allocating {size} bytes\n{backtrace}")]
1202 OutOfMemory {
1203 /// The size of the failed allocation in bytes.
1204 size: u64,
1205 /// The captured backtrace.
1206 #[cfg_attr(serializable, serde(skip))]
1207 backtrace: BackTrace,
1208 },
1209
1210 /// No room for the allocation in the pages the memory holds, and the
1211 /// reservation was not allowed to add one.
1212 ///
1213 /// A stream recording a graph reserves this way: an allocation would
1214 /// become part of the recording, and a hardware graph holding one cannot
1215 /// be relaunched. The warmup before a capture is what leaves the pools
1216 /// holding what the recorded run asks for.
1217 #[error(
1218 "No room for {size} bytes in the memory held, and no page may be added (a graph capture is recording: warm up with the same workload first)\n{backtrace}"
1219 )]
1220 PageUpdateForbidden {
1221 /// The size of the allocation in bytes.
1222 size: u64,
1223 /// The captured backtrace.
1224 #[cfg_attr(serializable, serde(skip))]
1225 backtrace: BackTrace,
1226 },
1227
1228 /// An allocation needs pages of a new size, and the memory already holds
1229 /// as many page sizes as it keeps track of.
1230 ///
1231 /// Every size a workload grew through keeps a pool until what lives on it
1232 /// is freed or relocated. A cleanup relocates what it can, so reclaiming
1233 /// and retrying is a reasonable response.
1234 #[error(
1235 "Can't allocate {size} bytes: every page size the memory tracks still holds live allocations\n{backtrace}"
1236 )]
1237 PageSizesExhausted {
1238 /// The size of the allocation in bytes.
1239 size: u64,
1240 /// The captured backtrace.
1241 #[cfg_attr(serializable, serde(skip))]
1242 backtrace: BackTrace,
1243 },
1244
1245 /// Strides aren't supported for this copy operation on this runtime
1246 #[error("the provided strides are not supported for this operation\n{backtrace}")]
1247 UnsupportedStrides {
1248 /// The backtrace.
1249 #[cfg_attr(serializable, serde(skip))]
1250 backtrace: BackTrace,
1251 },
1252
1253 /// Memory wasn't found in the memory pool
1254 #[error("couldn't find resource for that handle: {reason}\n{backtrace}")]
1255 NotFound {
1256 /// The backtrace.
1257 #[cfg_attr(serializable, serde(skip))]
1258 backtrace: BackTrace,
1259 /// The reason the handle is invalid.
1260 reason: Reason,
1261 },
1262
1263 /// The storage backend holds no allocation for a handle's storage id.
1264 ///
1265 /// One layer below [`IoError::NotFound`]: there the memory manager could
1266 /// not route a binding to a slice, here the routing succeeded and the
1267 /// allocation the slice names is gone. A handle outliving its page, or a
1268 /// storage id a deallocation retired, reaches the storage this way.
1269 #[error("the storage holds no allocation for that handle: {reason}\n{backtrace}")]
1270 StorageHandleNotFound {
1271 /// Which id was looked up, and in which storage.
1272 reason: Reason,
1273 /// The backtrace.
1274 #[cfg_attr(serializable, serde(skip))]
1275 backtrace: BackTrace,
1276 },
1277
1278 /// An allocation carved lazily under a [`DryRun`](crate::dry_run::DryRun)
1279 /// could not be given real device backing when it was finally resolved.
1280 ///
1281 /// Distinct from the same failure at reservation time, and the distinction
1282 /// is what a caller acts on: the memory was promised earlier, by a pass
1283 /// that measured a plan without paying for it, and is only now being
1284 /// charged for. A plan whose replay hits this was measured against more
1285 /// device memory than the replay has — warm the tune caches first, or
1286 /// measure a smaller one.
1287 #[error(
1288 "couldn't map storage for a deferred allocation of {size} bytes\nCaused by:\n {source}"
1289 )]
1290 StorageMappingFailed {
1291 /// The size of the allocation that could not be backed, in bytes.
1292 size: u64,
1293 /// Why the device allocation failed.
1294 source: Box<IoError>,
1295 /// The backtrace.
1296 #[cfg_attr(serializable, serde(skip))]
1297 backtrace: BackTrace,
1298 },
1299
1300 /// The device is poisoned: a kernel faulted in a way the driver makes
1301 /// sticky (an illegal address, a trap, a hardware fault, etc.)
1302 #[error("The device is poisoned: {0}")]
1303 DevicePoisoned(#[from] DevicePoison),
1304
1305 /// Unknown error happened during execution
1306 #[error("Unknown error happened during execution: {description}\n{backtrace}")]
1307 Unknown {
1308 /// Details of the error
1309 description: String,
1310 /// The backtrace.
1311 #[cfg_attr(serializable, serde(skip))]
1312 backtrace: BackTrace,
1313 },
1314
1315 /// The current IO operation is not supported
1316 #[error("The current IO operation is not supported\n{backtrace}")]
1317 UnsupportedIoOperation {
1318 /// The backtrace.
1319 #[cfg_attr(serializable, serde(skip))]
1320 backtrace: BackTrace,
1321 },
1322}
1323
1324impl IoError {
1325 /// Whether the device that emitted the error is poisoned.
1326 pub fn is_device_poisoned(&self) -> bool {
1327 match self {
1328 Self::DevicePoisoned(_) => true,
1329 Self::StorageMappingFailed { source, .. } => source.is_device_poisoned(),
1330 _ => false,
1331 }
1332 }
1333
1334 /// Whether reclaiming memory could still make this allocation succeed.
1335 ///
1336 /// Out of memory *right now* is not out of memory for good: pool pages
1337 /// whose slices have all been dropped are still resident, and the frees
1338 /// that would release them may sit in a deferred drop queue. A transient
1339 /// peak — a model build holding float weights while their quantized copies
1340 /// allocate, an autotune sample on a full device — is rescued by a reclaim
1341 /// and a second attempt.
1342 ///
1343 /// A buffer larger than any page the device can hold is the exception. It
1344 /// never fits, so reclaiming would only spend the time. So is an
1345 /// allocation while a graph records, where nothing may be released.
1346 pub fn may_succeed_after_reclaim(&self) -> bool {
1347 !matches!(
1348 self,
1349 IoError::BufferTooBig { .. } | IoError::PageUpdateForbidden { .. }
1350 )
1351 }
1352}
1353
1354impl core::fmt::Debug for IoError {
1355 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1356 f.write_fmt(format_args!("{self}"))
1357 }
1358}
1359
1360/// Arguments to execute a kernel.
1361#[derive(Debug, Default)]
1362pub struct KernelArguments {
1363 /// Kernel bindings
1364 pub resources: Vec<KernelResource>,
1365 /// What the caller declared each resource is for, indexed like
1366 /// `resources`.
1367 ///
1368 /// The compiled kernel's own answer is better when it exists — the
1369 /// visibility analysis can prove a buffer write-only or dead, which a
1370 /// caller cannot — but it only exists once the kernel compiles. This one
1371 /// is stamped at the launch site from what the caller can see (a launch
1372 /// generated from `&Tensor` versus `&mut Tensor` knows it statically), so
1373 /// it survives the compile failing, which is exactly when it is needed: a
1374 /// launch that never ran must not taint the buffers it was only going to
1375 /// read. Missing entries read as [`ReadWrite`](BufferIOAttr::ReadWrite),
1376 /// so a caller that declares nothing keeps the loud fallback.
1377 pub declared_io: Vec<BufferIOAttr>,
1378 /// Packed scalars and metadata. First scalars sorted by type, then static metadata,
1379 /// then dynamic metadata.
1380 pub info: MetadataBindingInfo,
1381}
1382
1383impl core::fmt::Display for KernelArguments {
1384 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1385 f.write_str("KernelArguments")?;
1386 for b in self.resources.iter() {
1387 f.write_fmt(format_args!("\n - buffer: {b:?}\n"))?;
1388 }
1389
1390 Ok(())
1391 }
1392}
1393
1394impl KernelArguments {
1395 /// Create a new bindings struct
1396 pub fn new() -> Self {
1397 Self::default()
1398 }
1399
1400 /// Add a buffer binding
1401 pub fn with_buffer(mut self, binding: BufferBinding) -> Self {
1402 self.resources.push(KernelResource::Buffer(binding));
1403 self
1404 }
1405
1406 /// Add a buffer binding, declaring what the kernel does with it.
1407 ///
1408 /// The declaration is what a launch that fails before running — a kernel
1409 /// that does not compile above all — falls back on: only declared-writable
1410 /// buffers take the failure, so the ones the kernel was only going to read
1411 /// stay readable. Resources added without a declaration read as
1412 /// [`ReadWrite`](BufferIOAttr::ReadWrite), and mixing the two keeps every
1413 /// declaration on the resource it was made for.
1414 pub fn with_buffer_io(mut self, binding: BufferBinding, io: BufferIOAttr) -> Self {
1415 self.declared_io
1416 .resize(self.resources.len(), BufferIOAttr::ReadWrite);
1417 self.resources.push(KernelResource::Buffer(binding));
1418 self.declared_io.push(io);
1419 self
1420 }
1421
1422 /// Extend the buffers with `bindings`
1423 pub fn with_buffers(mut self, bindings: Vec<BufferBinding>) -> Self {
1424 let bindings = bindings.into_iter().map(KernelResource::Buffer);
1425 self.resources.extend(bindings);
1426 self
1427 }
1428
1429 /// Set the info to `info`
1430 pub fn with_info(mut self, info: MetadataBindingInfo) -> Self {
1431 self.info = info;
1432 self
1433 }
1434
1435 /// Extend the tensor maps with `bindings`
1436 pub fn with_tensor_maps(mut self, bindings: Vec<TensorMapBinding>) -> Self {
1437 let bindings = bindings.into_iter().map(KernelResource::TensorMap);
1438 self.resources.extend(bindings);
1439 self
1440 }
1441
1442 /// The buffers this launch was given.
1443 pub fn buffers(&self) -> impl Iterator<Item = &BufferBinding> {
1444 self.resources.iter().map(|resource| match resource {
1445 KernelResource::Buffer(binding) => binding,
1446 KernelResource::TensorMap(tensor_map) => &tensor_map.binding,
1447 })
1448 }
1449
1450 /// The memory this launch was given.
1451 pub fn memory_ids(&self) -> impl Iterator<Item = ManagedMemoryId> + '_ {
1452 self.buffers().map(|binding| binding.memory.id())
1453 }
1454
1455 /// The buffers this launch was given that the kernel writes, per the
1456 /// compiled kernel's own answer — the ones a launch that fails taints,
1457 /// and nothing else.
1458 ///
1459 /// `io` is what the compiler recorded from its visibility analysis,
1460 /// indexed like `resources` (see
1461 /// [`BufferIOAttr`](crate::kernel::BufferIOAttr)). An index it has no
1462 /// answer for falls back to the caller's declaration in `declared_io` —
1463 /// which is how a kernel that never compiled still taints only its
1464 /// outputs — and an index neither answers reads as written: naming a
1465 /// buffer the kernel only read fails a read that would have been fine,
1466 /// loudly; missing one it writes hands back the bytes that were there
1467 /// before, silently — so the last-resort fallback over-names.
1468 pub fn buffers_written<'a>(
1469 &'a self,
1470 io: Option<&'a [BufferIOAttr]>,
1471 ) -> impl Iterator<Item = &'a BufferBinding> {
1472 self.buffers()
1473 .enumerate()
1474 .filter_map(move |(index, binding)| {
1475 let written = self
1476 .io_attr(io, index)
1477 .map(|io| io.is_writable())
1478 .unwrap_or(true);
1479 written.then_some(binding)
1480 })
1481 }
1482
1483 /// The buffers this launch was given that the kernel reads — the ones
1484 /// whose contents have to be trustworthy before the launch runs, and the
1485 /// only ones checked: a pure output is not read, so a relaunch into a
1486 /// tainted buffer is exactly how the buffer gets repaired.
1487 ///
1488 /// The same fallback chain as [`buffers_written`](Self::buffers_written):
1489 /// compiled answer, then the caller's declaration, then read — so a
1490 /// kernel nobody kept an answer for is checked on everything rather than
1491 /// checked on nothing.
1492 pub fn buffers_read<'a>(
1493 &'a self,
1494 io: Option<&'a [BufferIOAttr]>,
1495 ) -> impl Iterator<Item = &'a BufferBinding> {
1496 self.buffers()
1497 .enumerate()
1498 .filter_map(move |(index, binding)| {
1499 let read = self
1500 .io_attr(io, index)
1501 .map(|io| io.is_readable())
1502 .unwrap_or(true);
1503 read.then_some(binding)
1504 })
1505 }
1506
1507 /// The answer for one resource: the compiled kernel's when it kept one,
1508 /// the caller's declaration otherwise, `None` when neither answered.
1509 fn io_attr(&self, compiled: Option<&[BufferIOAttr]>, index: usize) -> Option<BufferIOAttr> {
1510 compiled
1511 .and_then(|io| io.get(index))
1512 .or_else(|| self.declared_io.get(index))
1513 .copied()
1514 }
1515}
1516
1517/// Binding of a set of scalars of the same type to execute a kernel.
1518///
1519/// The [`Server`] is responsible to convert those info into actual [`Binding`] when launching
1520/// kernels.
1521#[derive(new, Debug, Default)]
1522pub struct MetadataBindingInfo {
1523 /// Scalar and metadata values
1524 pub data: Vec<u64>,
1525 /// Start of the dynamically sized portion of the metadata, relative to the entire info buffer
1526 pub dynamic_metadata_offset: usize,
1527}
1528
1529impl MetadataBindingInfo {
1530 /// Create a new binding info for custom data, for externally compiled kernels.
1531 pub fn custom(data: Vec<u64>) -> Self {
1532 Self::new(data, 0)
1533 }
1534}
1535
1536/// A binding with shape and stride info for non-contiguous reading
1537#[derive(new, Debug)]
1538pub struct CopyDescriptor {
1539 /// Binding for the memory resource
1540 pub handle: BufferBinding,
1541 /// Shape of the resource
1542 pub shape: Shape,
1543 /// Strides of the resource
1544 pub strides: Strides,
1545 /// Size of each element in the resource
1546 pub elem_size: usize,
1547}
1548
1549/// A tensor map used with TMA ops
1550#[derive(new, Clone, Debug)]
1551pub struct TensorMapBinding {
1552 /// The binding for the backing tensor
1553 pub binding: BufferBinding,
1554 /// The tensormap metadata
1555 pub map: TensorMapMeta,
1556}
1557
1558/// `TensorMap` metadata for the opaque proxy used in TMA copies
1559#[derive(Debug, Clone)]
1560pub struct TensorMapMeta {
1561 /// Tensormap format (tiled or im2col)
1562 pub format: TensorMapFormat,
1563 /// Metadata of the backing tensor
1564 pub metadata: Metadata,
1565 /// Element stride, usually 1 but may be 2 for complex tensors
1566 /// For im2col, this is equivalent to the kernel stride
1567 pub elem_stride: Strides,
1568 /// Interleave mode
1569 pub interleave: TensorMapInterleave,
1570 /// Swizzle mode
1571 pub swizzle: TensorMapSwizzle,
1572 /// Prefetch settings
1573 pub prefetch: TensorMapPrefetch,
1574 /// OOB fill value
1575 pub oob_fill: OobFill,
1576 /// Element type
1577 pub elem_ty: ElemType,
1578}
1579
1580/// Specifieds the number of cubes to be dispatched for a kernel.
1581///
1582/// This translates to eg. a grid for CUDA, or to `num_workgroups` for wgsl.
1583#[allow(clippy::large_enum_variant)]
1584pub enum CubeCount {
1585 /// Dispatch a known count of x, y, z cubes.
1586 Static(u32, u32, u32),
1587 /// Dispatch an amount based on the values in this buffer. The buffer should contain a u32 array [x, y, z].
1588 Dynamic(BufferBinding),
1589}
1590
1591/// Defines how to select cube count based on the number of cubes required.
1592pub enum CubeCountSelection {
1593 /// If the number of cubes is the same as required.
1594 Exact(CubeCount),
1595 /// If the number of cubes isn't the same as required.
1596 ///
1597 /// This can happen based on the hardware limit, requiring the kernel to perform OOB checks.
1598 Approx(CubeCount, u32),
1599}
1600
1601impl CubeCountSelection {
1602 /// Creates a [`CubeCount`] while respecting the hardware limits.
1603 pub fn new(client: &Client, num_cubes: u32) -> Self {
1604 let cube_count = cube_count_spread(&client.properties().hardware.max_cube_count, num_cubes);
1605
1606 let num_cubes_actual = cube_count[0] * cube_count[1] * cube_count[2];
1607 let cube_count = CubeCount::Static(cube_count[0], cube_count[1], cube_count[2]);
1608
1609 match num_cubes_actual == num_cubes {
1610 true => CubeCountSelection::Exact(cube_count),
1611 false => CubeCountSelection::Approx(cube_count, num_cubes_actual),
1612 }
1613 }
1614
1615 /// If some cubes will be idle.
1616 pub fn has_idle(&self) -> bool {
1617 matches!(self, Self::Approx(..))
1618 }
1619
1620 /// Converts into [`CubeCount`].
1621 pub fn cube_count(self) -> CubeCount {
1622 match self {
1623 CubeCountSelection::Exact(cube_count) => cube_count,
1624 CubeCountSelection::Approx(cube_count, _) => cube_count,
1625 }
1626 }
1627}
1628
1629impl From<CubeCountSelection> for CubeCount {
1630 fn from(value: CubeCountSelection) -> Self {
1631 value.cube_count()
1632 }
1633}
1634
1635impl CubeCount {
1636 /// Create a new static cube count with the given x = y = z = 1.
1637 pub fn new_single() -> Self {
1638 CubeCount::Static(1, 1, 1)
1639 }
1640
1641 /// Create a new static cube count with the given x, and y = z = 1.
1642 pub fn new_1d(x: u32) -> Self {
1643 CubeCount::Static(x, 1, 1)
1644 }
1645
1646 /// Create a new static cube count with the given x and y, and z = 1.
1647 pub fn new_2d(x: u32, y: u32) -> Self {
1648 CubeCount::Static(x, y, 1)
1649 }
1650
1651 /// Create a new static cube count with the given x, y and z.
1652 pub fn new_3d(x: u32, y: u32, z: u32) -> Self {
1653 CubeCount::Static(x, y, z)
1654 }
1655
1656 /// Checks whether the cube count is definitely empty, i.e. has 0 dispatches.
1657 pub fn is_empty(&self) -> bool {
1658 match self {
1659 Self::Static(x, y, z) => *x == 0 || *y == 0 || *z == 0,
1660 Self::Dynamic(_) => false,
1661 }
1662 }
1663}
1664
1665impl Debug for CubeCount {
1666 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1667 match self {
1668 CubeCount::Static(x, y, z) => f.write_fmt(format_args!("({x}, {y}, {z})")),
1669 CubeCount::Dynamic(_) => f.write_str("binding"),
1670 }
1671 }
1672}
1673
1674impl Clone for CubeCount {
1675 fn clone(&self) -> Self {
1676 match self {
1677 Self::Static(x, y, z) => Self::Static(*x, *y, *z),
1678 Self::Dynamic(binding) => Self::Dynamic(binding.clone()),
1679 }
1680 }
1681}
1682
1683#[derive(Debug, From, PartialEq, Eq, Clone, Copy, Hash, Deref, DerefMut)]
1684#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
1685#[allow(missing_docs)]
1686/// The number of units across all 3 axis totalling to the number of working units in a cube.
1687pub struct CubeDim(pub Dim3);
1688
1689impl CubeDim {
1690 /// Creates a new [`CubeDim`] based on the maximum number of tasks that can be parellalized by units, in other words,
1691 /// by the maximum number of working units.
1692 ///
1693 /// # Notes
1694 ///
1695 /// For complex problems, you probably want to have your own logic function to create the
1696 /// [`CubeDim`], but for simpler problems such as elemwise-operation, this is a great default.
1697 pub fn new(client: &Client, working_units: usize) -> Self {
1698 let properties = client.properties();
1699 let plane_size = properties.hardware.plane_size_max;
1700 let plane_count = Self::calculate_plane_count_per_cube(
1701 working_units as u32,
1702 plane_size,
1703 properties.hardware.num_cpu_cores,
1704 );
1705
1706 // Make sure it respects the max units per cube (especially on wasm)
1707 let limit = properties.hardware.max_units_per_cube / plane_size;
1708
1709 // Ensure at least 1 plane so CubeDim is always valid (num_elems() > 0).
1710 Self::new_2d(plane_size, u32::min(limit, plane_count).max(1))
1711 }
1712
1713 fn calculate_plane_count_per_cube(
1714 working_units: u32,
1715 plane_dim: u32,
1716 num_cpu_cores: Option<u32>,
1717 ) -> u32 {
1718 match num_cpu_cores {
1719 Some(num_cores) => core::cmp::min(num_cores, working_units),
1720 None => {
1721 let plane_count_max = core::cmp::max(1, working_units / plane_dim);
1722
1723 // Ensures `plane_count` is a power of 2.
1724 const NUM_PLANE_MAX: u32 = 8u32;
1725 const NUM_PLANE_MAX_LOG2: u32 = NUM_PLANE_MAX.ilog2();
1726 let plane_count_max_log2 =
1727 core::cmp::min(NUM_PLANE_MAX_LOG2, u32::ilog2(plane_count_max));
1728 2u32.pow(plane_count_max_log2)
1729 }
1730 }
1731 }
1732
1733 /// Create a new cube dim with x = y = z = 1.
1734 pub const fn new_single() -> Self {
1735 Self(Dim3::new_single())
1736 }
1737
1738 /// Create a new cube dim with the given x, and y = z = 1.
1739 pub const fn new_1d(x: u32) -> Self {
1740 Self(Dim3::new_1d(x))
1741 }
1742
1743 /// Create a new cube dim with the given x and y, and z = 1.
1744 pub const fn new_2d(x: u32, y: u32) -> Self {
1745 Self(Dim3::new_2d(x, y))
1746 }
1747
1748 /// Create a new cube dim with the given x, y and z.
1749 /// This is equivalent to the [new](CubeDim::new) function.
1750 pub const fn new_3d(x: u32, y: u32, z: u32) -> Self {
1751 Self(Dim3::new_3d(x, y, z))
1752 }
1753
1754 /// Total numbers of units per cube
1755 pub const fn num_elems(&self) -> u32 {
1756 self.0.num_elems()
1757 }
1758
1759 /// Whether this `CubeDim` can fully contain `other`
1760 pub const fn can_contain(&self, other: CubeDim) -> bool {
1761 self.0.can_contain(other.0)
1762 }
1763}
1764
1765impl From<(u32, u32, u32)> for CubeDim {
1766 fn from(value: (u32, u32, u32)) -> Self {
1767 CubeDim::new_3d(value.0, value.1, value.2)
1768 }
1769}
1770
1771impl From<CubeDim> for (u32, u32, u32) {
1772 fn from(val: CubeDim) -> Self {
1773 (val.x, val.y, val.z)
1774 }
1775}
1776
1777impl From<CubeDim> for Dim3 {
1778 fn from(value: CubeDim) -> Self {
1779 value.0
1780 }
1781}
1782
1783fn cube_count_spread(max: &(u32, u32, u32), num_cubes: u32) -> [u32; 3] {
1784 let max_cube_counts = [max.0, max.1, max.2];
1785 let mut num_cubes = [num_cubes, 1, 1];
1786 let base = 2;
1787
1788 let mut reduce_count = |i: usize| {
1789 if num_cubes[i] <= max_cube_counts[i] {
1790 return true;
1791 }
1792
1793 loop {
1794 num_cubes[i] = num_cubes[i].div_ceil(base);
1795 num_cubes[i + 1] *= base;
1796
1797 if num_cubes[i] <= max_cube_counts[i] {
1798 return false;
1799 }
1800 }
1801 };
1802
1803 for i in 0..2 {
1804 if reduce_count(i) {
1805 break;
1806 }
1807 }
1808
1809 num_cubes
1810}
1811
1812#[cfg(test)]
1813mod tests {
1814 use super::*;
1815 use alloc::vec;
1816 use alloc::vec::Vec;
1817
1818 /// A service for handles that never reach a device.
1819 fn service() -> cubecl_common::device::ServiceId {
1820 cubecl_common::device::ServiceId::of::<()>(cubecl_common::device::DeviceId::new(0, 0))
1821 }
1822
1823 #[test_log::test]
1824 fn safe_num_cubes_even() {
1825 let max = (32, 32, 32);
1826 let required = 2048;
1827
1828 let actual = cube_count_spread(&max, required);
1829 let expected = [32, 32, 2];
1830 assert_eq!(actual, expected);
1831 }
1832
1833 #[test_log::test]
1834 fn safe_num_cubes_odd() {
1835 let max = (48, 32, 16);
1836 let required = 3177;
1837
1838 let actual = cube_count_spread(&max, required);
1839 let expected = [25, 32, 4];
1840 assert_eq!(actual, expected);
1841 }
1842
1843 /// The compiled kernel's answer drives both sets exactly, in resource
1844 /// order.
1845 #[test_log::test]
1846 fn buffer_io_drives_the_read_and_write_sets() {
1847 use crate::kernel::BufferIOAttr;
1848 use cubecl_environment::stream::StreamId;
1849
1850 let stream = StreamId { value: 0 };
1851 let args = KernelArguments::new().with_buffers(vec![
1852 Handle::new(service(), stream, 8).binding(),
1853 Handle::new(service(), stream, 8).binding(),
1854 Handle::new(service(), stream, 8).binding(),
1855 Handle::new(service(), stream, 8).binding(),
1856 ]);
1857 let io = [
1858 BufferIOAttr::ReadOnly,
1859 BufferIOAttr::WriteOnly,
1860 BufferIOAttr::ReadWrite,
1861 BufferIOAttr::Dead,
1862 ];
1863
1864 let written: Vec<_> = args.buffers_written(Some(&io)).collect();
1865 assert_eq!(written.len(), 2, "WriteOnly and ReadWrite are written");
1866 assert!(core::ptr::eq(written[0], args.buffers().nth(1).unwrap()));
1867 assert!(core::ptr::eq(written[1], args.buffers().nth(2).unwrap()));
1868
1869 let read: Vec<_> = args.buffers_read(Some(&io)).collect();
1870 assert_eq!(read.len(), 2, "ReadOnly and ReadWrite are read");
1871 assert!(core::ptr::eq(read[0], args.buffers().next().unwrap()));
1872 assert!(core::ptr::eq(read[1], args.buffers().nth(2).unwrap()));
1873 }
1874
1875 /// A poisoned device is the one failure a caller must not retry.
1876 ///
1877 /// Loading the next kernel is the case that matters most: it arrives as a
1878 /// compilation error, and read as a refusal it would have an autotuner
1879 /// quietly drop the candidate and try the next one on a dead device.
1880 #[test_log::test]
1881 fn poisoned_device_read_through_every_path() {
1882 use crate::server::{IoError, LaunchError};
1883
1884 let poison = || DevicePoison {
1885 reason: String::from("cuEventSynchronize failed with status 700"),
1886 backtrace: Default::default(),
1887 };
1888 let poisoned = [
1889 ServerError::DevicePoisoned(poison()),
1890 ServerError::Launch(LaunchError::DevicePoisoned(poison())),
1891 ServerError::Launch(LaunchError::CompilationError(
1892 CompilationError::DevicePoisoned(poison()),
1893 )),
1894 ServerError::Io(IoError::DevicePoisoned(poison())),
1895 ServerError::Io(IoError::StorageMappingFailed {
1896 size: 1 << 20,
1897 source: alloc::boxed::Box::new(IoError::DevicePoisoned(poison())),
1898 backtrace: Default::default(),
1899 }),
1900 ];
1901 let refused =
1902 ServerError::Launch(LaunchError::CompilationError(CompilationError::Generic {
1903 reason: "no such intrinsic on this target".into(),
1904 backtrace: Default::default(),
1905 }));
1906 let transient = ServerError::Generic {
1907 reason: "the copy failed".into(),
1908 backtrace: Default::default(),
1909 };
1910
1911 for error in &poisoned {
1912 assert!(error.is_device_poisoned(), "{error}");
1913 assert!(
1914 !error.is_refusal(),
1915 "a poisoned device is not a refusal: {error}"
1916 );
1917 }
1918 assert!(!refused.is_device_poisoned());
1919 assert!(!transient.is_device_poisoned());
1920
1921 let unwritten = ServerError::Unwritten {
1922 failure: 1,
1923 claimed: 1,
1924 chain: Vec::new(),
1925 root: alloc::boxed::Box::new(poisoned[1].clone()),
1926 backtrace: Default::default(),
1927 };
1928 assert!(unwritten.is_device_poisoned());
1929 let group = ServerError::Several {
1930 errors: vec![refused, unwritten],
1931 backtrace: Default::default(),
1932 };
1933 assert!(group.is_device_poisoned());
1934 assert!(!group.is_refusal());
1935 }
1936
1937 /// A refusal is the kernel being turned down, and nothing else is.
1938 ///
1939 /// The direction that matters is the false positive: a harness that takes
1940 /// a device fault for a refusal reports a broken run as a skipped one, and
1941 /// the test goes green. So a group answers yes only when every root does.
1942 #[test_log::test]
1943 fn only_a_refused_kernel_reads_as_a_refusal() {
1944 use crate::server::{LaunchError, ResourceLimitError};
1945
1946 let refused =
1947 ServerError::Launch(LaunchError::CompilationError(CompilationError::Generic {
1948 reason: "no such intrinsic on this target".into(),
1949 backtrace: Default::default(),
1950 }));
1951 let over_budget = ServerError::Launch(LaunchError::TooManyResources(
1952 ResourceLimitError::SharedMemory {
1953 requested: 1 << 20,
1954 max: 1 << 15,
1955 backtrace: Default::default(),
1956 },
1957 ));
1958 let fault = ServerError::Generic {
1959 reason: "the device faulted".into(),
1960 backtrace: Default::default(),
1961 };
1962
1963 let panicked = ServerError::Launch(LaunchError::CompilationError(
1964 CompilationError::ExpansionPanicked {
1965 kernel: "kernel".into(),
1966 message: "an assertion failed".into(),
1967 backtrace: Default::default(),
1968 },
1969 ));
1970
1971 assert!(refused.is_refusal());
1972 assert!(over_budget.is_refusal());
1973 assert!(!fault.is_refusal(), "a fault is not a refusal");
1974 assert!(
1975 !panicked.is_refusal(),
1976 "a kernel that panicked while expanding is a defect, not a refusal"
1977 );
1978
1979 // A read reports the failure that stopped the buffer's writer, so the
1980 // question has to reach through the report to the root.
1981 let unwritten = |root: &ServerError| ServerError::Unwritten {
1982 failure: 1,
1983 claimed: 1,
1984 chain: Vec::new(),
1985 root: alloc::boxed::Box::new(root.clone()),
1986 backtrace: Default::default(),
1987 };
1988 assert!(unwritten(&refused).is_refusal());
1989 assert!(!unwritten(&fault).is_refusal());
1990 assert!(!unwritten(&panicked).is_refusal());
1991
1992 let group = |errors: Vec<ServerError>| ServerError::Several {
1993 errors,
1994 backtrace: Default::default(),
1995 };
1996 assert!(group(vec![unwritten(&refused), unwritten(&over_budget)]).is_refusal());
1997 assert!(
1998 !group(vec![unwritten(&refused), unwritten(&fault)]).is_refusal(),
1999 "one real failure among refusals is still a real failure"
2000 );
2001 assert!(
2002 !group(Vec::new()).is_refusal(),
2003 "an empty group refuses nothing"
2004 );
2005 }
2006
2007 /// Every fallback over-names: a kernel the compiler kept no answer for,
2008 /// and a resource past what the answer covers, read as both read and
2009 /// written. Naming a buffer the kernel only read fails a read that would
2010 /// have been fine, loudly; missing one it writes hands back the bytes
2011 /// that were there before, silently.
2012 #[test_log::test]
2013 fn missing_io_reads_as_everything_read_and_written() {
2014 use crate::kernel::BufferIOAttr;
2015 use cubecl_environment::stream::StreamId;
2016
2017 let stream = StreamId { value: 0 };
2018 let args = KernelArguments::new().with_buffers(vec![
2019 Handle::new(service(), stream, 8).binding(),
2020 Handle::new(service(), stream, 8).binding(),
2021 ]);
2022
2023 assert_eq!(args.buffers_written(None).count(), 2);
2024 assert_eq!(args.buffers_read(None).count(), 2);
2025
2026 let short = [BufferIOAttr::Dead];
2027 assert_eq!(
2028 args.buffers_written(Some(&short)).count(),
2029 1,
2030 "the uncovered resource reads as written"
2031 );
2032 assert_eq!(args.buffers_read(Some(&short)).count(), 1);
2033 }
2034
2035 /// The caller's declaration answers when the compiled kernel kept none —
2036 /// which is what a launch that fails to compile falls back on, so it
2037 /// taints only its declared outputs — and the compiled answer still wins
2038 /// where it exists, since only the visibility analysis can prove a buffer
2039 /// write-only or dead.
2040 #[test_log::test]
2041 fn declared_io_answers_when_the_compiled_kernel_kept_none() {
2042 use crate::kernel::BufferIOAttr;
2043 use cubecl_environment::stream::StreamId;
2044
2045 let stream = StreamId { value: 0 };
2046 let args = KernelArguments::new()
2047 .with_buffer_io(
2048 Handle::new(service(), stream, 8).binding(),
2049 BufferIOAttr::ReadOnly,
2050 )
2051 .with_buffer_io(
2052 Handle::new(service(), stream, 8).binding(),
2053 BufferIOAttr::ReadOnly,
2054 )
2055 .with_buffer_io(
2056 Handle::new(service(), stream, 8).binding(),
2057 BufferIOAttr::WriteOnly,
2058 );
2059
2060 // No compiled answer: the declaration decides. The inputs are not
2061 // written, so a failed compile leaves them readable.
2062 let written: Vec<_> = args.buffers_written(None).collect();
2063 assert_eq!(written.len(), 1, "only the declared output is written");
2064 assert!(core::ptr::eq(written[0], args.buffers().nth(2).unwrap()));
2065 assert_eq!(args.buffers_read(None).count(), 2);
2066
2067 // A compiled answer overrides the declaration where it has one and
2068 // falls back to it where it does not.
2069 let compiled = [BufferIOAttr::ReadWrite];
2070 let written: Vec<_> = args.buffers_written(Some(&compiled)).collect();
2071 assert_eq!(written.len(), 2, "compiled ReadWrite plus declared output");
2072 assert!(core::ptr::eq(written[0], args.buffers().next().unwrap()));
2073 assert!(core::ptr::eq(written[1], args.buffers().nth(2).unwrap()));
2074 }
2075
2076 /// Declarations stay on the resource they were made for when declared and
2077 /// undeclared resources mix, and the undeclared ones keep the loud
2078 /// fallback.
2079 #[test_log::test]
2080 fn an_undeclared_resource_among_declared_ones_over_names() {
2081 use crate::kernel::BufferIOAttr;
2082 use cubecl_environment::stream::StreamId;
2083
2084 let stream = StreamId { value: 0 };
2085 let args = KernelArguments::new()
2086 .with_buffer(Handle::new(service(), stream, 8).binding())
2087 .with_buffer_io(
2088 Handle::new(service(), stream, 8).binding(),
2089 BufferIOAttr::ReadOnly,
2090 );
2091
2092 let written: Vec<_> = args.buffers_written(None).collect();
2093 assert_eq!(written.len(), 1, "the undeclared resource reads as written");
2094 assert!(core::ptr::eq(written[0], args.buffers().next().unwrap()));
2095 assert_eq!(args.buffers_read(None).count(), 2);
2096 }
2097}