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