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