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