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