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