Skip to main content

cubecl_runtime/server/
base.rs

1use super::Handle;
2use crate::{
3    client::ComputeClient,
4    compiler::CompilationError,
5    config::{CubeClRuntimeConfig, RuntimeConfig, compilation::BoundsCheckMode},
6    id::GraphId,
7    kernel::KernelMetadata,
8    logging::ServerLogger,
9    memory_management::{
10        ManagedMemoryHandle, MemoryAllocationMode, MemoryConfiguration, MemoryUsage,
11    },
12    runtime::Runtime,
13    server::Binding,
14    storage::{ComputeStorage, ManagedResource},
15    tma::{OobFill, TensorMapFormat, TensorMapInterleave, TensorMapPrefetch, TensorMapSwizzle},
16};
17use ahash::AHasher;
18use alloc::boxed::Box;
19#[cfg(feature = "profile-tracy")]
20use alloc::format;
21use alloc::string::String;
22use alloc::sync::Arc;
23use alloc::vec::Vec;
24use core::{
25    fmt::Debug,
26    hash::{Hash, Hasher},
27};
28use cubecl_common::{
29    backtrace::BackTrace,
30    bytes::Bytes,
31    device::{self, DeviceId},
32    future::DynFut,
33    profile::ProfileDuration,
34    stream_id::StreamId,
35    stub::RwLock,
36};
37use cubecl_ir::{DeviceProperties, ElemType, StorageType};
38use cubecl_zspace::{Shape, Strides, metadata::Metadata};
39use hashbrown::HashSet;
40use itertools::Itertools;
41use thiserror::Error;
42
43#[derive(Error, Clone)]
44#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
45/// An error during profiling.
46pub enum ProfileError {
47    /// An unknown error happened during profiling
48    #[error(
49        "An unknown error happened during profiling\nCaused by:\n  {reason}\nBacktrace:\n{backtrace}"
50    )]
51    Unknown {
52        /// The caused of the error
53        reason: String,
54        /// The captured backtrace.
55        #[cfg_attr(std_io, serde(skip))]
56        backtrace: BackTrace,
57    },
58
59    /// No profiling was registered
60    #[error("No profiling registered\nBacktrace:\n{backtrace}")]
61    NotRegistered {
62        /// The captured backtrace.
63        #[cfg_attr(std_io, serde(skip))]
64        backtrace: BackTrace,
65    },
66
67    /// A launch error happened during profiling
68    #[error("A launch error happened during profiling\nCaused by:\n  {0}")]
69    Launch(#[from] LaunchError),
70
71    /// An execution error happened during profiling
72    #[error("An execution error happened during profiling\nCaused by:\n  {0}")]
73    Server(#[from] Box<ServerError>),
74}
75
76impl core::fmt::Debug for ProfileError {
77    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
78        f.write_fmt(format_args!("{self}"))
79    }
80}
81
82/// Contains many different types that are useful for server implementations and compute clients.
83pub struct ServerUtilities<Server: ComputeServer> {
84    /// The time when `profile-tracy` is activated.
85    #[cfg(feature = "profile-tracy")]
86    pub epoch_time: web_time::Instant,
87    /// The GPU client when `profile-tracy` is activated.
88    #[cfg(feature = "profile-tracy")]
89    pub gpu_client: tracy_client::GpuContext,
90    /// Information shared between all servers.
91    pub properties: DeviceProperties,
92    /// Stable hash of the device properties
93    pub properties_hash: u64,
94    /// Information specific to the current server.
95    pub info: Server::Info,
96    /// The logger based on global cubecl configs.
97    pub logger: Arc<ServerLogger>,
98    /// How to create the allocation.
99    pub layout_policy: Server::MemoryLayoutPolicy,
100    /// How to enforce bounds checking on kernels.
101    pub check_mode: BoundsCheckMode,
102    /// A set containing the ids for which the inter-device communication has already been initialized.
103    pub initialized_comms: RwLock<HashSet<CommunicationId>>,
104}
105
106/// Defines how the memory layout is determined.
107pub trait MemoryLayoutPolicy: Send + Sync + 'static {
108    /// Applies the memory layout policy to a list of descriptors.
109    ///
110    /// Returns a vector of `MemoryLayout`, one per descriptor, with layouts that share a
111    /// single `Binding`.
112    fn apply(
113        &self,
114        stream_id: StreamId,
115        descriptors: &[MemoryLayoutDescriptor],
116    ) -> (Handle, Vec<MemoryLayout>);
117}
118
119impl<Server: core::fmt::Debug> core::fmt::Debug for ServerUtilities<Server>
120where
121    Server: ComputeServer,
122    Server::Info: core::fmt::Debug,
123{
124    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
125        f.debug_struct("ServerUtilities")
126            .field("properties", &self.properties)
127            .field("info", &self.info)
128            .field("logger", &self.logger)
129            .finish()
130    }
131}
132
133impl<S: ComputeServer> ServerUtilities<S> {
134    /// Creates a new server utilities.
135    pub fn new(
136        properties: DeviceProperties,
137        logger: Arc<ServerLogger>,
138        info: S::Info,
139        allocator: S::MemoryLayoutPolicy,
140    ) -> Self {
141        // Start a tracy client if needed.
142        #[cfg(feature = "profile-tracy")]
143        let client = tracy_client::Client::start();
144
145        Self {
146            properties_hash: properties.checksum(),
147            properties,
148            logger,
149            // Create the GPU client if needed.
150            #[cfg(feature = "profile-tracy")]
151            gpu_client: client
152                .clone()
153                .new_gpu_context(
154                    Some(&format!("{info:?}")),
155                    // 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).
156                    tracy_client::GpuContextType::Invalid,
157                    0,   // Timestamps are manually aligned to this epoch so start at 0.
158                    1.0, // Timestamps are manually converted to be nanoseconds so period is 1.
159                )
160                .unwrap(),
161            #[cfg(feature = "profile-tracy")]
162            epoch_time: web_time::Instant::now(),
163            info,
164            layout_policy: allocator,
165            check_mode: CubeClRuntimeConfig::get().compilation.check_mode,
166            initialized_comms: RwLock::new(HashSet::default()),
167        }
168    }
169}
170
171/// Kernel Launch Errors.
172#[derive(Error, Clone)]
173#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
174pub enum LaunchError {
175    /// The given kernel can't be compiled.
176    #[error("A compilation error happened during launch\nCaused by:\n  {0}")]
177    CompilationError(#[from] CompilationError),
178
179    /// The server is out of memory.
180    #[error(
181        "An out-of-memory error happened during launch\nCaused by:\n  {reason}\nBacktrace\n{backtrace}"
182    )]
183    OutOfMemory {
184        /// The caused of the memory error.
185        reason: String,
186        /// The backtrace for this error.
187        #[cfg_attr(std_io, serde(skip))]
188        backtrace: BackTrace,
189    },
190
191    /// Too many resources were requested
192    #[error("Too many resources were requested during launch\n{0}")]
193    TooManyResources(#[from] ResourceLimitError),
194
195    /// Unknown launch error.
196    #[error(
197        "An unknown error happened during launch\nCaused by:\n  {reason}\nBacktrace\n{backtrace}"
198    )]
199    Unknown {
200        /// The caused of the unknown error.
201        reason: String,
202        /// The backtrace for this error.
203        #[cfg_attr(std_io, serde(skip))]
204        backtrace: BackTrace,
205    },
206
207    /// Can't launch because of an IO Error.
208    #[error("An io error happened during launch\nCaused by:\n  {0}")]
209    IoError(#[from] IoError),
210}
211
212/// Resource limit errors.
213#[derive(Error, Clone)]
214#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
215pub enum ResourceLimitError {
216    /// Shared memory exceeds maximum
217    #[error(
218        "Too much shared memory requested.\nRequested {requested} bytes, maximum {max} bytes available.\nBacktrace\n{backtrace}"
219    )]
220    SharedMemory {
221        /// Value requested
222        requested: usize,
223        /// Maximum value
224        max: usize,
225        /// The backtrace for this error.
226        #[cfg_attr(std_io, serde(skip))]
227        backtrace: BackTrace,
228    },
229    /// Total units exceeds maximum
230    #[error(
231        "Total unit count exceeds maximum.\nRequested {requested} units, max units is {max}.\nBacktrace\n{backtrace}"
232    )]
233    Units {
234        /// Requested value
235        requested: u32,
236        /// Maximum value
237        max: u32,
238        /// The backtrace for this error.
239        #[cfg_attr(std_io, serde(skip))]
240        backtrace: BackTrace,
241    },
242    /// `CubeDim` exceeds maximum
243    #[error(
244        "Cube dim exceeds maximum bounds.\nRequested {requested:?}, max is {max:?}.\nBacktrace\n{backtrace}"
245    )]
246    CubeDim {
247        /// Requested value
248        requested: (u32, u32, u32),
249        /// Maximum value
250        max: (u32, u32, u32),
251        /// The backtrace for this error.
252        #[cfg_attr(std_io, serde(skip))]
253        backtrace: BackTrace,
254    },
255    /// Total of cube dim `CubeDim` exceeds maximum
256    #[error(
257        "Max units per cube exceeds maximum bounds.\nRequested {requested}, max is {max}.\nBacktrace\n{backtrace}"
258    )]
259    MaxUnitPerCube {
260        /// Requested value
261        requested: u32,
262        /// Maximum value
263        max: u32,
264        /// The backtrace for this error.
265        #[cfg_attr(std_io, serde(skip))]
266        backtrace: BackTrace,
267    },
268}
269
270impl core::fmt::Debug for LaunchError {
271    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
272        f.write_fmt(format_args!("{self}"))
273    }
274}
275
276impl core::fmt::Debug for ResourceLimitError {
277    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
278        f.write_fmt(format_args!("{self}"))
279    }
280}
281
282/// The error returned by the default (unsupported) graph-capture methods.
283fn graph_capture_unsupported() -> ServerError {
284    ServerError::Generic {
285        reason: alloc::string::String::from("graph capture is not supported by this backend"),
286        backtrace: BackTrace::capture(),
287    }
288}
289
290/// Error that can happen asynchronously while executing registered kernels.
291#[derive(Error, Clone)]
292#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
293pub enum ServerError {
294    /// A runtime validation error
295    #[error(
296        "A validation error happened during execution\nCaused by:\n  {message}\nBacktrace:\n{backtrace}"
297    )]
298    Validation {
299        /// The details of the validation error.
300        message: String,
301        /// The backtrace for this error.
302        #[cfg_attr(std_io, serde(skip))]
303        backtrace: BackTrace,
304    },
305
306    /// A generic runtime error.
307    #[error("An error happened during execution\nCaused by:\n  {reason}\nBacktrace:\n{backtrace}")]
308    Generic {
309        /// The details of the generic error.
310        reason: String,
311        /// The backtrace for this error.
312        #[cfg_attr(std_io, serde(skip))]
313        backtrace: BackTrace,
314    },
315
316    /// A launch error happened
317    #[error("A launch error happened\nCaused by:\n  {0}")]
318    Launch(#[from] LaunchError),
319
320    /// An execution error happened during profiling
321    #[error("An execution error happened during profiling\nCaused by:\n  {0}")]
322    Profile(#[from] ProfileError),
323
324    /// An IO error happened
325    #[error("An IO error happened\nCaused by:\n  {0}")]
326    Io(#[from] IoError),
327
328    /// The server is an invalid state.
329    #[error("The server is in an invalid state\nCaused by:\n  {}", errors.iter().join("\n"))]
330    ServerUnhealthy {
331        /// The details of the generic error.
332        errors: Vec<Self>,
333        /// The backtrace for this error.
334        #[cfg_attr(std_io, serde(skip))]
335        backtrace: BackTrace,
336    },
337}
338
339impl Debug for ServerError {
340    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341        write!(f, "{self}")
342    }
343}
344
345/// How errors are handled in a stream when executing a task.
346#[derive(Clone, Copy)]
347pub struct StreamErrorMode {
348    /// Whether the task still executes even if the stream is in error.
349    pub ignore: bool,
350    /// Whether the errors are flushed by the current task.
351    pub flush: bool,
352}
353
354/// The compute server is responsible for handling resources and computations over resources.
355///
356/// Everything in the server is mutable, therefore it should be solely accessed through the
357/// [`ComputeClient`] for thread safety.
358pub trait ComputeServer:
359    Send + core::fmt::Debug + ServerCommunication + device::DeviceService + 'static
360where
361    Self: Sized,
362{
363    /// The kernel type defines the computation algorithms.
364    type Kernel: KernelMetadata;
365    /// Information that can be retrieved for the runtime.
366    type Info: Debug + Send + Sync;
367    /// Manages how allocations are performed for a server.
368    type MemoryLayoutPolicy: MemoryLayoutPolicy;
369    /// The [storage](ComputeStorage) type defines how data is stored and accessed.
370    type Storage: ComputeStorage;
371
372    /// Initializes [memory](ManagedMemoryHandle) on the given [stream](StreamId) with the given size.
373    fn initialize_memory(&mut self, memory: ManagedMemoryHandle, size: u64, stream_id: StreamId);
374
375    /// Reserves N [Bytes] of the provided sizes to be used as staging to load data.
376    fn staging(
377        &mut self,
378        _sizes: &[usize],
379        _stream_id: StreamId,
380    ) -> Result<Vec<Bytes>, ServerError> {
381        Err(IoError::UnsupportedIoOperation {
382            backtrace: BackTrace::capture(),
383        }
384        .into())
385    }
386
387    /// Retrieve the server logger.
388    fn logger(&self) -> Arc<ServerLogger>;
389
390    /// Retrieve the server utilities.
391    fn utilities(&self) -> Arc<ServerUtilities<Self>>;
392
393    /// Given bindings, returns the owned resources as bytes.
394    fn read(
395        &mut self,
396        descriptors: Vec<CopyDescriptor>,
397        stream_id: StreamId,
398    ) -> DynFut<Result<Vec<Bytes>, ServerError>>;
399
400    /// Writes the specified bytes into the buffers given
401    fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId);
402
403    /// Wait for the completion of every task in the server.
404    fn sync(&mut self, stream_id: StreamId) -> DynFut<Result<(), ServerError>>;
405
406    /// Given a resource handle, returns the storage resource.
407    fn get_resource(
408        &mut self,
409        binding: Binding,
410        stream_id: StreamId,
411    ) -> Result<ManagedResource<<Self::Storage as ComputeStorage>::Resource>, ServerError>;
412
413    /// Executes the `kernel` over the given memory `handles`.
414    ///
415    /// Kernels have mutable access to every resource they are given
416    /// and are responsible of determining which should be read or written.
417    ///
418    /// # Safety
419    ///
420    /// When executing with mode [`ExecutionMode::Unchecked`], out-of-bound reads and writes can happen.
421    unsafe fn launch(
422        &mut self,
423        kernel: Self::Kernel,
424        count: CubeCount,
425        bindings: KernelArguments,
426        kind: ExecutionMode,
427        stream_id: StreamId,
428    );
429
430    /// Flush all outstanding tasks in the server.
431    fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError>;
432
433    /// Prepare `stream_id` for an upcoming graph capture: route allocations
434    /// into a stable pool and snapshot it, so every buffer allocated between
435    /// here and [`end_capture`](ComputeServer::end_capture) can be pinned for
436    /// the graph's lifetime. Call this **before** the warmup run so the capture
437    /// window itself needs no fresh device allocation — a device malloc inside
438    /// the capture is illegal.
439    ///
440    /// Prefer having kernels already **autotuned before** this call: any
441    /// transient benchmark buffers autotune allocates while the window is armed
442    /// are forced into the persistent pool and pinned to the graph, so a graph
443    /// captured over a cold autotune cache retains more device memory than it
444    /// replays against. Warm the autotune cache first, then `graph_prepare` and
445    /// warm up only to populate the pool.
446    ///
447    /// A no-op by default (harmless on backends without graph support); a
448    /// hardware-graph backend enables its persistent pool + capture recording.
449    fn graph_prepare(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
450        let _ = stream_id;
451        Ok(())
452    }
453
454    /// Begin recording the launches issued on `stream_id` into a graph instead
455    /// of executing them, so the sequence can later be [replayed](ComputeServer::replay)
456    /// as a single dispatch. Between this call and [`end_capture`](ComputeServer::end_capture)
457    /// the stream must not synchronize or allocate fresh device memory — call
458    /// [`graph_prepare`](ComputeServer::graph_prepare) and warm up first.
459    ///
460    /// The default is unsupported; a backend with hardware graph support (CUDA,
461    /// HIP) overrides these methods.
462    fn begin_capture(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
463        let _ = stream_id;
464        Err(graph_capture_unsupported())
465    }
466
467    /// Stop recording (see [`begin_capture`](ComputeServer::begin_capture)),
468    /// store the captured graph in the backend's registry, and return its
469    /// [`GraphId`], ready to [replay](ComputeServer::replay).
470    fn end_capture(&mut self, stream_id: StreamId) -> Result<GraphId, ServerError> {
471        let _ = stream_id;
472        Err(graph_capture_unsupported())
473    }
474
475    /// Replay the graph identified by `graph` on `stream_id` — one dispatch that
476    /// re-runs the whole recorded launch sequence against its original buffers.
477    ///
478    /// Fire-and-forget, like [`launch`](ComputeServer::launch): the call enqueues
479    /// the dispatch and returns without waiting, so a failure is **not** returned
480    /// here — it is pushed onto the stream's error queue and surfaces on the next
481    /// [`flush`](ComputeServer::flush)/[`sync`](ComputeServer::sync), which leaves
482    /// the server unhealthy until drained. A no-op by default: a [`GraphId`] can
483    /// only come from [`end_capture`](ComputeServer::end_capture), unsupported here.
484    fn replay(&mut self, graph: GraphId, stream_id: StreamId) {
485        let _ = (graph, stream_id);
486    }
487
488    /// Release the graph identified by `graph`, destroying its executable and
489    /// unpinning the buffers it retained. The backend must ensure any in-flight
490    /// replay on `stream_id` has completed first (replay returns at enqueue
491    /// time). A no-op by default and for an unknown id.
492    fn graph_destroy(&mut self, graph: GraphId, stream_id: StreamId) {
493        let _ = (graph, stream_id);
494    }
495
496    /// Memory usage of the given stream.
497    fn memory_usage(&mut self, stream_id: StreamId) -> Result<MemoryUsage, ServerError>;
498
499    /// Stream ids the client should iterate to aggregate across the device.
500    ///
501    /// Default is just the calling stream, which is correct for
502    /// non-multi-stream backends; multi-stream backends override to
503    /// return one id per initialized stream pool slot.
504    fn stream_ids(&self) -> Vec<StreamId> {
505        Vec::from([StreamId::current()])
506    }
507
508    /// Ask the server to release memory that it can release.
509    fn memory_cleanup(&mut self, stream_id: StreamId);
510
511    /// Install a new dynamic-pool layout for the device's **main GPU** memory.
512    ///
513    /// The calling stream's pools are rebuilt in place (see
514    /// [`MemoryManagement::configure`](crate::memory_management::MemoryManagement::configure)
515    /// — a rebuild only happens when nothing is live in them), and the layout
516    /// becomes the one every stream created afterwards is built with. Pool
517    /// layouts are a purely programmatic, runtime setting — there is no
518    /// config-file pathway — so callers size them per workload (e.g. per model,
519    /// just before loading it).
520    ///
521    /// Returns `true` when the calling stream's pools were rebuilt now, and
522    /// `false` when they kept the old layout (something was still live in
523    /// them); the layout still applies to streams created afterwards.
524    ///
525    /// The default is a no-op returning `false` for servers without
526    /// configurable pools.
527    fn configure_memory_pools(&mut self, config: MemoryConfiguration, stream_id: StreamId) -> bool {
528        let _ = (config, stream_id);
529        log::warn!("Memory pool configuration isn't supported by this server; keeping defaults");
530        false
531    }
532
533    /// Enable collecting timestamps.
534    fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError>;
535
536    /// Disable collecting timestamps.
537    fn end_profile(
538        &mut self,
539        stream_id: StreamId,
540        token: ProfilingToken,
541    ) -> Result<ProfileDuration, ProfileError>;
542
543    /// Update the memory mode of allocation in the server.
544    fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId);
545}
546
547/// An ID unique to any unordered combination of devices.
548#[derive(Clone, Debug, Hash, Eq, PartialEq)]
549pub struct CommunicationId {
550    /// The ID as a `String`.
551    pub id: u64,
552}
553
554impl From<Vec<DeviceId>> for CommunicationId {
555    fn from(mut value: Vec<DeviceId>) -> Self {
556        // Make sure that device ids are sorted so that any combination of the same devices uses the same communicator.
557        value.sort();
558        let mut hasher = AHasher::default();
559        value.hash(&mut hasher);
560        CommunicationId {
561            id: hasher.finish(),
562        }
563    }
564}
565
566/// Different reduce operations.
567pub enum ReduceOperation {
568    /// Sum.
569    Sum,
570    /// Mean.
571    Mean,
572}
573
574/// Defines functions for optimized data transfer between servers, supporting custom communication
575/// mechanisms such as peer-to-peer communication or specialized implementations.
576pub trait ServerCommunication {
577    /// Indicates whether server-to-server communication is enabled for this implementation.
578    const SERVER_COMM_ENABLED: bool;
579
580    /// Ensure that all queued collective operations have been executed.
581    ///
582    /// # Arguments
583    ///
584    /// * `stream_id` - The [`StreamId`] of the stream waiting for the sync.
585    ///
586    /// # Returns
587    ///
588    /// Returns a `Result` containing an `ServerError` if the operation fails.
589    #[allow(unused_variables)]
590    fn sync_collective(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
591        todo!() // For backends other than cuda.
592    }
593
594    /// Initialize the communication between the devices in `device_ids`.
595    ///
596    /// # Arguments
597    ///
598    /// * `device_ids` - The IDs of the devices that need communication.
599    ///
600    /// # Returns
601    ///
602    /// Returns a `Result` containing an `ServerError` if the operation fails.
603    #[allow(unused_variables)]
604    fn comm_init(&mut self, device_ids: Vec<DeviceId>) -> Result<(), ServerError> {
605        unimplemented!()
606    }
607
608    /// Performs an `all_reduce` operation on the input data and writes it to the output buffer.
609    /// see <https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html#allreduce>
610    ///
611    /// # Arguments
612    ///
613    /// * `src` - The data to be reduced.
614    /// * `dst` - Where to write the result.
615    /// * `dtype` - The element type of the data being reduced
616    /// * `stream_id` - The data's stream id.
617    /// * `op` - The reduce's aggregation operation e.g. mean, sum, etc.
618    /// * `device_ids` - The list of device ids from which to `all_reduce`.
619    ///
620    /// # Returns
621    ///
622    /// Returns a `Result` containing an `ServerError` if the operation fails.
623    #[allow(unused_variables)]
624    fn all_reduce(
625        &mut self,
626        src: Binding,
627        dst: Binding,
628        dtype: ElemType,
629        stream_id: StreamId,
630        op: ReduceOperation,
631        device_ids: Vec<DeviceId>,
632    ) -> Result<(), ServerError> {
633        unimplemented!()
634    }
635
636    /// Sends data from this server to a destination server.
637    ///
638    /// # Arguments
639    ///
640    /// * `desc` - A descriptor specifying the data to be sent, including shape, strides, and binding.
641    /// * `dtype` - The element type of the data being sent.
642    /// * `stream_id` - The stream ID associated with the server's operation.
643    /// * `device_id_dst` - ID of the device receiving the data.
644    ///
645    /// # Returns
646    ///
647    /// Returns a `Result` containing an `ServerError` if the operation fails.
648    #[allow(unused_variables)]
649    fn send(
650        &mut self,
651        desc: CopyDescriptor,
652        dtype: ElemType,
653        stream_id: StreamId,
654        device_id_dst: DeviceId,
655    ) -> Result<(), ServerError> {
656        unimplemented!()
657    }
658
659    /// Receive data from another server.
660    ///
661    /// # Arguments
662    ///
663    /// * `handle` - The handle in which the received data is written.
664    /// * `dtype` - The element type of the data being sent.
665    /// * `stream_id` - The stream ID associated with the server's operation.
666    /// * `device_id_src` - ID of the device sending the data.
667    ///
668    /// # Returns
669    ///
670    /// Returns a `Result` containing an `ServerError` if the operation fails.
671    #[allow(unused_variables)]
672    fn recv(
673        &mut self,
674        handle: Handle,
675        dtype: ElemType,
676        stream_id: StreamId,
677        device_id_src: DeviceId,
678    ) -> Result<(), ServerError> {
679        unimplemented!()
680    }
681}
682
683#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
684/// Profiling identification so that the server can support recursive and overlapping profilings.
685pub struct ProfilingToken {
686    /// The token value.
687    pub id: u64,
688}
689
690/// Type of allocation, either contiguous or optimized (row-aligned when possible)
691#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
692pub enum MemoryLayoutStrategy {
693    /// Contiguous layout, with no padding
694    Contiguous,
695    /// Optimized for access speed. In practice this means row-aligned with padding for runtimes
696    /// that support it.
697    Optimized,
698}
699
700/// Descriptor for a new tensor allocation
701#[derive(new, Debug, Clone)]
702pub struct MemoryLayoutDescriptor {
703    /// Strategy used to create the memory layout.
704    pub strategy: MemoryLayoutStrategy,
705    /// Shape of the tensor
706    pub shape: Shape,
707    /// Size of each element in the tensor (used for conversion of shape to bytes)
708    pub elem_size: usize,
709}
710
711impl MemoryLayoutDescriptor {
712    /// Create an optimized allocation descriptor
713    pub fn optimized(shape: Shape, elem_size: usize) -> Self {
714        MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Optimized, shape, elem_size)
715    }
716
717    /// Create a contiguous allocation descriptor
718    pub fn contiguous(shape: Shape, elem_size: usize) -> Self {
719        MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Contiguous, shape, elem_size)
720    }
721}
722
723/// An allocation with associated strides. Strides depend on tensor layout.
724#[derive(Debug, Clone)]
725pub struct MemoryLayout {
726    /// The handle for the memory resource
727    pub memory: Handle,
728    /// TODO: `Strides` should become `Layout`.
729    ///
730    /// The strides of the tensor
731    pub strides: Strides,
732}
733
734impl MemoryLayout {
735    /// Create a new memory layout.
736    pub fn new(handle: Handle, strides: impl Into<Strides>) -> Self {
737        MemoryLayout {
738            memory: handle,
739            strides: strides.into(),
740        }
741    }
742}
743
744/// A reason for an error.
745#[derive(Default, Clone)]
746pub struct Reason {
747    inner: ReasonInner,
748}
749
750#[cfg(std_io)]
751mod _reason_serde {
752    use super::*;
753
754    use alloc::string::ToString;
755    use serde::{Deserialize, Deserializer, Serialize, Serializer};
756
757    impl Serialize for Reason {
758        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
759        where
760            S: Serializer,
761        {
762            // Use the Display implementation (via to_string) to flatten the enum
763            serializer.serialize_str(&self.to_string())
764        }
765    }
766
767    impl<'de> Deserialize<'de> for Reason {
768        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
769        where
770            D: Deserializer<'de>,
771        {
772            // Deserialize into a standard String first
773            let s = String::deserialize(deserializer)?;
774
775            // Wrap it in the Dynamic variant since we can't safely
776            // reconstruct a 'static str from a runtime string.
777            Ok(Reason {
778                inner: ReasonInner::Dynamic(Arc::new(s)),
779            })
780        }
781    }
782}
783
784#[derive(Default, Clone)]
785enum ReasonInner {
786    Static(&'static str),
787    Dynamic(Arc<String>),
788    #[default]
789    NotProvided,
790}
791
792impl core::fmt::Display for Reason {
793    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
794        match &self.inner {
795            ReasonInner::Static(content) => f.write_str(content),
796            ReasonInner::Dynamic(content) => f.write_str(content),
797            ReasonInner::NotProvided => f.write_str("No reason provided for the error"),
798        }
799    }
800}
801
802impl core::fmt::Debug for Reason {
803    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
804        core::fmt::Display::fmt(&self, f)
805    }
806}
807
808impl From<&'static str> for Reason {
809    fn from(value: &'static str) -> Self {
810        Self {
811            inner: ReasonInner::Static(value),
812        }
813    }
814}
815
816impl From<String> for Reason {
817    fn from(value: String) -> Self {
818        Self {
819            inner: ReasonInner::Dynamic(Arc::new(value)),
820        }
821    }
822}
823
824/// Error returned from `create`/`read`/`write` functions. Due to async execution not all errors
825/// are able to be caught, so some IO errors will still panic.
826#[derive(Error, Clone)]
827#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
828pub enum IoError {
829    /// Buffer size exceeds the max available
830    #[error("can't allocate buffer of size: {size}\n{backtrace}")]
831    BufferTooBig {
832        /// The size of the buffer in bytes.
833        size: u64,
834        /// The captured backtrace.
835        #[cfg_attr(std_io, serde(skip))]
836        backtrace: BackTrace,
837    },
838
839    /// A memory pool with a fixed capacity cap is exhausted.
840    ///
841    /// Unlike [`IoError::BufferTooBig`] (the allocation can *never* fit), this
842    /// means the working set exceeded the configured budget. Server execution
843    /// paths treat it as fatal — the budget is a hard contract, so failing
844    /// early beats silently growing — but callers that manage their own
845    /// working set may free pool memory and retry.
846    #[error(
847        "memory pool capacity exceeded: failed to reserve {size} bytes, pool is capped at {capacity} bytes ({in_use} bytes in use)\n{backtrace}"
848    )]
849    PoolCapacityExceeded {
850        /// The size of the failed reservation in bytes.
851        size: u64,
852        /// The configured pool capacity in bytes (whole pages).
853        capacity: u64,
854        /// Bytes currently in use in the pool.
855        in_use: u64,
856        /// The captured backtrace.
857        #[cfg_attr(std_io, serde(skip))]
858        backtrace: BackTrace,
859    },
860
861    /// Strides aren't supported for this copy operation on this runtime
862    #[error("the provided strides are not supported for this operation\n{backtrace}")]
863    UnsupportedStrides {
864        /// The backtrace.
865        #[cfg_attr(std_io, serde(skip))]
866        backtrace: BackTrace,
867    },
868
869    /// Memory wasn't found in the memory pool
870    #[error("couldn't find resource for that handle: {reason}\n{backtrace}")]
871    NotFound {
872        /// The backtrace.
873        #[cfg_attr(std_io, serde(skip))]
874        backtrace: BackTrace,
875        /// The reason the handle is invalid.
876        reason: Reason,
877    },
878
879    /// Handle wasn't found in the memory pool
880    #[error("couldn't free the handle, since it is currently in used. \n{backtrace}")]
881    FreeError {
882        /// The backtrace.
883        #[cfg_attr(std_io, serde(skip))]
884        backtrace: BackTrace,
885    },
886
887    /// Unknown error happened during execution
888    #[error("Unknown error happened during execution\n{backtrace}")]
889    Unknown {
890        /// Details of the error
891        description: String,
892        /// The backtrace.
893        #[cfg_attr(std_io, serde(skip))]
894        backtrace: BackTrace,
895    },
896
897    /// The current IO operation is not supported
898    #[error("The current IO operation is not supported\n{backtrace}")]
899    UnsupportedIoOperation {
900        /// The backtrace.
901        #[cfg_attr(std_io, serde(skip))]
902        backtrace: BackTrace,
903    },
904
905    /// Can't perform the IO operation because of a runtime error.
906    #[error("Can't perform the IO operation because of a runtime error: {0}")]
907    Execution(#[from] Box<ServerError>),
908}
909
910impl core::fmt::Debug for IoError {
911    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
912        f.write_fmt(format_args!("{self}"))
913    }
914}
915
916/// Arguments to execute a kernel.
917#[derive(Debug, Default)]
918pub struct KernelArguments {
919    /// Buffer bindings
920    pub buffers: Vec<Binding>,
921    /// Packed scalars and metadata. First scalars sorted by type, then static metadata,
922    /// then dynamic metadata.
923    pub info: MetadataBindingInfo,
924    /// Tensor map bindings
925    pub tensor_maps: Vec<TensorMapBinding>,
926}
927
928impl core::fmt::Display for KernelArguments {
929    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
930        f.write_str("KernelArguments")?;
931        for b in self.buffers.iter() {
932            f.write_fmt(format_args!("\n - buffer: {b:?}\n"))?;
933        }
934
935        Ok(())
936    }
937}
938
939impl KernelArguments {
940    /// Create a new bindings struct
941    pub fn new() -> Self {
942        Self::default()
943    }
944
945    /// Add a buffer binding
946    pub fn with_buffer(mut self, binding: Binding) -> Self {
947        self.buffers.push(binding);
948        self
949    }
950
951    /// Extend the buffers with `bindings`
952    pub fn with_buffers(mut self, bindings: Vec<Binding>) -> Self {
953        self.buffers.extend(bindings);
954        self
955    }
956
957    /// Set the info to `info`
958    pub fn with_info(mut self, info: MetadataBindingInfo) -> Self {
959        self.info = info;
960        self
961    }
962
963    /// Extend the tensor maps with `bindings`
964    pub fn with_tensor_maps(mut self, bindings: Vec<TensorMapBinding>) -> Self {
965        self.tensor_maps.extend(bindings);
966        self
967    }
968}
969
970/// Binding of a set of scalars of the same type to execute a kernel.
971///
972/// The [`ComputeServer`] is responsible to convert those info into actual [`Binding`] when launching
973/// kernels.
974#[derive(new, Debug, Default)]
975pub struct MetadataBindingInfo {
976    /// Scalar and metadata values
977    pub data: Vec<u64>,
978    /// Start of the dynamically sized portion of the metadata, relative to the entire info buffer
979    pub dynamic_metadata_offset: usize,
980}
981
982impl MetadataBindingInfo {
983    /// Create a new binding info for custom data, for externally compiled kernels.
984    pub fn custom(data: Vec<u64>) -> Self {
985        Self::new(data, 0)
986    }
987}
988
989/// A binding with shape and stride info for non-contiguous reading
990#[derive(new, Debug)]
991pub struct CopyDescriptor {
992    /// Binding for the memory resource
993    pub handle: Binding,
994    /// Shape of the resource
995    pub shape: Shape,
996    /// Strides of the resource
997    pub strides: Strides,
998    /// Size of each element in the resource
999    pub elem_size: usize,
1000}
1001
1002/// A tensor map used with TMA ops
1003#[derive(new, Debug)]
1004pub struct TensorMapBinding {
1005    /// The binding for the backing tensor
1006    pub binding: Binding,
1007    /// The tensormap metadata
1008    pub map: TensorMapMeta,
1009}
1010
1011/// `TensorMap` metadata for the opaque proxy used in TMA copies
1012#[derive(Debug, Clone)]
1013pub struct TensorMapMeta {
1014    /// Tensormap format (tiled or im2col)
1015    pub format: TensorMapFormat,
1016    /// Metadata of the backing tensor
1017    pub metadata: Metadata,
1018    /// Element stride, usually 1 but may be 2 for complex tensors
1019    /// For im2col, this is equivalent to the kernel stride
1020    pub elem_stride: Strides,
1021    /// Interleave mode
1022    pub interleave: TensorMapInterleave,
1023    /// Swizzle mode
1024    pub swizzle: TensorMapSwizzle,
1025    /// Prefetch settings
1026    pub prefetch: TensorMapPrefetch,
1027    /// OOB fill value
1028    pub oob_fill: OobFill,
1029    /// Storage type
1030    pub storage_ty: StorageType,
1031}
1032
1033/// Specifieds the number of cubes to be dispatched for a kernel.
1034///
1035/// This translates to eg. a grid for CUDA, or to `num_workgroups` for wgsl.
1036#[allow(clippy::large_enum_variant)]
1037pub enum CubeCount {
1038    /// Dispatch a known count of x, y, z cubes.
1039    Static(u32, u32, u32),
1040    /// Dispatch an amount based on the values in this buffer. The buffer should contain a u32 array [x, y, z].
1041    Dynamic(Binding),
1042}
1043
1044/// Defines how to select cube count based on the number of cubes required.
1045pub enum CubeCountSelection {
1046    /// If the number of cubes is the same as required.
1047    Exact(CubeCount),
1048    /// If the number of cubes isn't the same as required.
1049    ///
1050    /// This can happen based on the hardware limit, requiring the kernel to perform OOB checks.
1051    Approx(CubeCount, u32),
1052}
1053
1054impl CubeCountSelection {
1055    /// Creates a [`CubeCount`] while respecting the hardware limits.
1056    pub fn new<R: Runtime>(client: &ComputeClient<R>, num_cubes: u32) -> Self {
1057        let cube_count = cube_count_spread(&client.properties().hardware.max_cube_count, num_cubes);
1058
1059        let num_cubes_actual = cube_count[0] * cube_count[1] * cube_count[2];
1060        let cube_count = CubeCount::Static(cube_count[0], cube_count[1], cube_count[2]);
1061
1062        match num_cubes_actual == num_cubes {
1063            true => CubeCountSelection::Exact(cube_count),
1064            false => CubeCountSelection::Approx(cube_count, num_cubes_actual),
1065        }
1066    }
1067
1068    /// If some cubes will be idle.
1069    pub fn has_idle(&self) -> bool {
1070        matches!(self, Self::Approx(..))
1071    }
1072
1073    /// Converts into [`CubeCount`].
1074    pub fn cube_count(self) -> CubeCount {
1075        match self {
1076            CubeCountSelection::Exact(cube_count) => cube_count,
1077            CubeCountSelection::Approx(cube_count, _) => cube_count,
1078        }
1079    }
1080}
1081
1082impl From<CubeCountSelection> for CubeCount {
1083    fn from(value: CubeCountSelection) -> Self {
1084        value.cube_count()
1085    }
1086}
1087
1088impl CubeCount {
1089    /// Create a new static cube count with the given x = y = z = 1.
1090    pub fn new_single() -> Self {
1091        CubeCount::Static(1, 1, 1)
1092    }
1093
1094    /// Create a new static cube count with the given x, and y = z = 1.
1095    pub fn new_1d(x: u32) -> Self {
1096        CubeCount::Static(x, 1, 1)
1097    }
1098
1099    /// Create a new static cube count with the given x and y, and z = 1.
1100    pub fn new_2d(x: u32, y: u32) -> Self {
1101        CubeCount::Static(x, y, 1)
1102    }
1103
1104    /// Create a new static cube count with the given x, y and z.
1105    pub fn new_3d(x: u32, y: u32, z: u32) -> Self {
1106        CubeCount::Static(x, y, z)
1107    }
1108
1109    /// Checks whether the cube count is definitely empty, i.e. has 0 dispatches.
1110    pub fn is_empty(&self) -> bool {
1111        match self {
1112            Self::Static(x, y, z) => *x == 0 || *y == 0 || *z == 0,
1113            Self::Dynamic(_) => false,
1114        }
1115    }
1116}
1117
1118impl Debug for CubeCount {
1119    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1120        match self {
1121            CubeCount::Static(x, y, z) => f.write_fmt(format_args!("({x}, {y}, {z})")),
1122            CubeCount::Dynamic(_) => f.write_str("binding"),
1123        }
1124    }
1125}
1126
1127impl Clone for CubeCount {
1128    fn clone(&self) -> Self {
1129        match self {
1130            Self::Static(x, y, z) => Self::Static(*x, *y, *z),
1131            Self::Dynamic(binding) => Self::Dynamic(binding.clone()),
1132        }
1133    }
1134}
1135
1136#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
1137#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
1138#[allow(missing_docs)]
1139/// The number of units across all 3 axis totalling to the number of working units in a cube.
1140pub struct CubeDim {
1141    /// The number of units in the x axis.
1142    pub x: u32,
1143    /// The number of units in the y axis.
1144    pub y: u32,
1145    /// The number of units in the z axis.
1146    pub z: u32,
1147}
1148
1149impl CubeDim {
1150    /// Creates a new [`CubeDim`] based on the maximum number of tasks that can be parellalized by units, in other words,
1151    /// by the maximum number of working units.
1152    ///
1153    /// # Notes
1154    ///
1155    /// For complex problems, you probably want to have your own logic function to create the
1156    /// [`CubeDim`], but for simpler problems such as elemwise-operation, this is a great default.
1157    pub fn new<R: Runtime>(client: &ComputeClient<R>, working_units: usize) -> Self {
1158        let properties = client.properties();
1159        let plane_size = properties.hardware.plane_size_max;
1160        let plane_count = Self::calculate_plane_count_per_cube(
1161            working_units as u32,
1162            plane_size,
1163            properties.hardware.num_cpu_cores,
1164        );
1165
1166        // Make sure it respects the max units per cube (especially on wasm)
1167        let limit = properties.hardware.max_units_per_cube / plane_size;
1168
1169        // Ensure at least 1 plane so CubeDim is always valid (num_elems() > 0).
1170        Self::new_2d(plane_size, u32::min(limit, plane_count).max(1))
1171    }
1172
1173    fn calculate_plane_count_per_cube(
1174        working_units: u32,
1175        plane_dim: u32,
1176        num_cpu_cores: Option<u32>,
1177    ) -> u32 {
1178        match num_cpu_cores {
1179            Some(num_cores) => core::cmp::min(num_cores, working_units),
1180            None => {
1181                let plane_count_max = core::cmp::max(1, working_units / plane_dim);
1182
1183                // Ensures `plane_count` is a power of 2.
1184                const NUM_PLANE_MAX: u32 = 8u32;
1185                const NUM_PLANE_MAX_LOG2: u32 = NUM_PLANE_MAX.ilog2();
1186                let plane_count_max_log2 =
1187                    core::cmp::min(NUM_PLANE_MAX_LOG2, u32::ilog2(plane_count_max));
1188                2u32.pow(plane_count_max_log2)
1189            }
1190        }
1191    }
1192
1193    /// Create a new cube dim with x = y = z = 1.
1194    pub const fn new_single() -> Self {
1195        Self { x: 1, y: 1, z: 1 }
1196    }
1197
1198    /// Create a new cube dim with the given x, and y = z = 1.
1199    pub const fn new_1d(x: u32) -> Self {
1200        Self { x, y: 1, z: 1 }
1201    }
1202
1203    /// Create a new cube dim with the given x and y, and z = 1.
1204    pub const fn new_2d(x: u32, y: u32) -> Self {
1205        Self { x, y, z: 1 }
1206    }
1207
1208    /// Create a new cube dim with the given x, y and z.
1209    /// This is equivalent to the [new](CubeDim::new) function.
1210    pub const fn new_3d(x: u32, y: u32, z: u32) -> Self {
1211        Self { x, y, z }
1212    }
1213
1214    /// Total numbers of units per cube
1215    pub const fn num_elems(&self) -> u32 {
1216        self.x * self.y * self.z
1217    }
1218
1219    /// Whether this `CubeDim` can fully contain `other`
1220    pub const fn can_contain(&self, other: CubeDim) -> bool {
1221        self.x >= other.x && self.y >= other.y && self.z >= other.z
1222    }
1223}
1224
1225impl From<(u32, u32, u32)> for CubeDim {
1226    fn from(value: (u32, u32, u32)) -> Self {
1227        CubeDim::new_3d(value.0, value.1, value.2)
1228    }
1229}
1230
1231impl From<CubeDim> for (u32, u32, u32) {
1232    fn from(val: CubeDim) -> Self {
1233        (val.x, val.y, val.z)
1234    }
1235}
1236
1237/// The kind of execution to be performed.
1238#[derive(Default, Hash, PartialEq, Eq, Clone, Debug, Copy)]
1239#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
1240pub enum ExecutionMode {
1241    /// Checked kernels are safe.
1242    #[default]
1243    Checked,
1244    /// Validate OOB and alert if OOB access occurs
1245    Validate,
1246    /// Unchecked kernels are unsafe.
1247    Unchecked,
1248}
1249
1250fn cube_count_spread(max: &(u32, u32, u32), num_cubes: u32) -> [u32; 3] {
1251    let max_cube_counts = [max.0, max.1, max.2];
1252    let mut num_cubes = [num_cubes, 1, 1];
1253    let base = 2;
1254
1255    let mut reduce_count = |i: usize| {
1256        if num_cubes[i] <= max_cube_counts[i] {
1257            return true;
1258        }
1259
1260        loop {
1261            num_cubes[i] = num_cubes[i].div_ceil(base);
1262            num_cubes[i + 1] *= base;
1263
1264            if num_cubes[i] <= max_cube_counts[i] {
1265                return false;
1266            }
1267        }
1268    };
1269
1270    for i in 0..2 {
1271        if reduce_count(i) {
1272            break;
1273        }
1274    }
1275
1276    num_cubes
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281    use super::*;
1282
1283    #[test_log::test]
1284    fn safe_num_cubes_even() {
1285        let max = (32, 32, 32);
1286        let required = 2048;
1287
1288        let actual = cube_count_spread(&max, required);
1289        let expected = [32, 32, 2];
1290        assert_eq!(actual, expected);
1291    }
1292
1293    #[test_log::test]
1294    fn safe_num_cubes_odd() {
1295        let max = (48, 32, 16);
1296        let required = 3177;
1297
1298        let actual = cube_count_spread(&max, required);
1299        let expected = [25, 32, 4];
1300        assert_eq!(actual, expected);
1301    }
1302}