Skip to main content

cubecl_runtime/server/
base.rs

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