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