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}