Skip to main content

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}