use super::{CaptureStatus, DeviceCaptures, Handle};
use crate::kernel::BufferIOAttr;
use crate::{
client::Client,
compiler::CompilationError,
config::{CubeClRuntimeConfig, RuntimeConfig, compilation::BoundsCheckMode},
dry_run::LaunchMode,
id::GraphId,
kernel::CubeKernel,
logging::ServerLogger,
memory_management::{
ManagedMemoryHandle, ManagedMemoryId, MemoryAllocationMode, StreamMemoryReport,
},
poison::DevicePoison,
server::{BufferBinding, KernelResource},
storage::{ComputeStorage, ManagedResource},
tma::{OobFill, TensorMapFormat, TensorMapInterleave, TensorMapPrefetch, TensorMapSwizzle},
};
use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::{
fmt::{Debug, Display},
hash::{BuildHasher, Hash},
};
use cubecl_common::{
bytes::Bytes,
device::{self, DeviceId, ServiceId},
profile::ProfileDuration,
};
use cubecl_environment::backtrace::BackTrace;
use cubecl_environment::collections::HashSet;
use cubecl_environment::future::DynFut;
use cubecl_environment::stream::StreamId;
use cubecl_environment::sync::RwLock;
use cubecl_ir::{DeviceProperties, ElemType, TargetProperties, settings::Dim3};
use cubecl_zspace::{Shape, Strides, metadata::Metadata};
use derive_more::{Deref, DerefMut, From};
use foldhash::fast::FixedState;
use itertools::Itertools;
use thiserror::Error;
#[derive(Error, Clone)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum ProfileError {
#[error(
"An unknown error happened during profiling\nCaused by:\n {reason}\nBacktrace:\n{backtrace}"
)]
Unknown {
reason: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("No profiling registered\nBacktrace:\n{backtrace}")]
NotRegistered {
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("The profiled window resolved no device timing\nBacktrace:\n{backtrace}")]
NotMeasured {
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("A launch error happened during profiling\nCaused by:\n {0}")]
Launch(#[from] LaunchError),
#[error("An execution error happened during profiling\nCaused by:\n {0}")]
Server(#[from] Box<ServerError>),
}
impl From<&ServerError> for ProfileError {
fn from(error: &ServerError) -> Self {
ProfileError::Server(Box::new(error.clone()))
}
}
impl core::fmt::Debug for ProfileError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!("{self}"))
}
}
pub struct ServerUtilities {
#[cfg(feature = "profile-tracy")]
pub epoch_time: cubecl_environment::time::Instant,
#[cfg(feature = "profile-tracy")]
pub gpu_client: tracy_client::GpuContext,
pub service: ServiceId,
pub name: &'static str,
pub properties: Arc<DeviceProperties>,
pub properties_hash: u64,
pub target_properties: Arc<TargetProperties>,
pub logger: Arc<ServerLogger>,
pub layout_policy: Box<dyn MemoryLayoutPolicy>,
pub server_comm_enabled: bool,
pub check_mode: BoundsCheckMode,
pub initialized_comms: RwLock<HashSet<CommunicationId>>,
pub captures: CaptureStatus,
}
pub trait MemoryLayoutPolicy: Send + Sync + 'static {
fn apply(
&self,
service: ServiceId,
stream_id: StreamId,
descriptors: &[MemoryLayoutDescriptor],
) -> (Handle, Vec<MemoryLayout>);
}
impl core::fmt::Debug for ServerUtilities {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
f.debug_struct("ServerUtilities")
.field("properties", &self.properties)
.field("name", &self.name)
.field("logger", &self.logger)
.finish()
}
}
impl ServerUtilities {
pub fn init(
service: ServiceId,
name: &'static str,
properties: DeviceProperties,
target_properties: TargetProperties,
logger: Arc<ServerLogger>,
allocator: impl MemoryLayoutPolicy,
) -> (Self, DeviceCaptures) {
let captures = DeviceCaptures::default();
#[cfg(feature = "profile-tracy")]
let client = tracy_client::Client::start();
let utilities = Self {
service,
name,
properties_hash: properties.checksum(),
properties: Arc::new(properties),
target_properties: Arc::new(target_properties),
logger,
#[cfg(feature = "profile-tracy")]
gpu_client: client
.clone()
.new_gpu_context(
Some(name),
tracy_client::GpuContextType::Invalid,
0, 1.0, )
.unwrap(),
#[cfg(feature = "profile-tracy")]
epoch_time: cubecl_environment::time::Instant::now(),
layout_policy: Box::new(allocator),
server_comm_enabled: false,
check_mode: CubeClRuntimeConfig::get().compilation.check_mode,
initialized_comms: RwLock::new(HashSet::default()),
captures: captures.status(),
};
(utilities, captures)
}
}
#[derive(Error, Clone)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum LaunchError {
#[error("A compilation error happened during launch\nCaused by:\n {0}")]
CompilationError(#[from] CompilationError),
#[error(
"An out-of-memory error happened during launch\nCaused by:\n {reason}\nBacktrace\n{backtrace}"
)]
OutOfMemory {
reason: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("Too many resources were requested during launch\n{0}")]
TooManyResources(#[from] ResourceLimitError),
#[error("The device was poisoned during launch\nCaused by:\n {0}")]
DevicePoisoned(#[from] DevicePoison),
#[error(
"An unknown error happened during launch\nCaused by:\n {reason}\nBacktrace\n{backtrace}"
)]
Unknown {
reason: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
}
#[derive(Error, Clone)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum ResourceLimitError {
#[error(
"Too much shared memory requested.\nRequested {requested} bytes, maximum {max} bytes available.\nBacktrace\n{backtrace}"
)]
SharedMemory {
requested: usize,
max: usize,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"Total unit count exceeds maximum.\nRequested {requested} units, max units is {max}.\nBacktrace\n{backtrace}"
)]
Units {
requested: u32,
max: u32,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"Cube dim exceeds maximum bounds.\nRequested {requested:?}, max is {max:?}.\nBacktrace\n{backtrace}"
)]
CubeDim {
requested: (u32, u32, u32),
max: (u32, u32, u32),
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
}
impl LaunchError {
pub fn is_device_poisoned(&self) -> bool {
match self {
Self::DevicePoisoned(_) => true,
Self::CompilationError(error) => error.is_device_poisoned(),
_ => false,
}
}
pub fn is_refusal(&self) -> bool {
match self {
Self::CompilationError(error) => error.is_refusal(),
Self::TooManyResources(_) => true,
_ => false,
}
}
}
impl core::fmt::Debug for LaunchError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!("{self}"))
}
}
impl core::fmt::Debug for ResourceLimitError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!("{self}"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum Collective {
CommInit,
AllReduce,
Send,
Recv,
SyncCollective,
}
impl Display for Collective {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Self::CommInit => "comm_init",
Self::AllReduce => "all_reduce",
Self::Send => "send",
Self::Recv => "recv",
Self::SyncCollective => "sync_collective",
})
}
}
#[derive(Error, Clone)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum ServerError {
#[error(
"A validation error happened during execution\nCaused by:\n {message}\nBacktrace:\n{backtrace}"
)]
Validation {
message: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("An error happened during execution\nCaused by:\n {reason}\nBacktrace:\n{backtrace}")]
Generic {
reason: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"{operation} needs a transport between devices, and this runtime has none\nBacktrace:\n{backtrace}"
)]
NoDeviceTransport {
operation: Collective,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("A handle of {handle} was used on {client}\nBacktrace:\n{backtrace}")]
ForeignHandle {
handle: String,
client: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("The client reaches {client}, not a {requested}\nBacktrace:\n{backtrace}")]
ServiceMismatch {
client: String,
requested: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("The device is poisoned\nCaused by:\n {0}")]
DevicePoisoned(#[from] DevicePoison),
#[error("A launch error happened\nCaused by:\n {0}")]
Launch(#[from] LaunchError),
#[error("An IO error happened\nCaused by:\n {0}")]
Io(#[from] IoError),
#[error(
"The work writing this buffer was torn down before it could say what went wrong: its \
write scope never reached the exit that names the real failure, which a panic \
mid-launch explains"
)]
TornDown,
#[error(
"The bytes were never written (failure #{failure}, still claiming {claimed} buffer(s))\n{}Caused by:\n {root}\nAsked at:\n{backtrace}",
chain.iter().map(|hop| alloc::format!(" {hop}\n")).collect::<String>()
)]
Unwritten {
failure: u64,
claimed: u32,
chain: Vec<String>,
root: Box<ServerError>,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"The work was skipped: an input carried a failure, and the work's outputs claim it now \
— a read of one of them names the root cause"
)]
Skipped,
#[error("Several failures at once\nCaused by:\n {}", errors.iter().join("\n"))]
Several {
errors: Vec<Self>,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
}
impl Debug for ServerError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{self}")
}
}
impl ServerError {
pub fn no_device_transport(operation: Collective) -> Self {
Self::NoDeviceTransport {
operation,
backtrace: BackTrace::capture(),
}
}
pub fn is_refusal(&self) -> bool {
match self {
Self::Launch(error) => error.is_refusal(),
Self::Unwritten { root, .. } => root.is_refusal(),
Self::Several { errors, .. } => {
!errors.is_empty() && errors.iter().all(Self::is_refusal)
}
_ => false,
}
}
pub fn is_device_poisoned(&self) -> bool {
match self {
Self::DevicePoisoned(_) => true,
Self::Launch(error) => error.is_device_poisoned(),
Self::Io(error) => error.is_device_poisoned(),
Self::Unwritten { root, .. } => root.is_device_poisoned(),
Self::Several { errors, .. } => errors.iter().any(Self::is_device_poisoned),
_ => false,
}
}
pub fn graph_state(reason: impl Into<String>) -> Self {
Self::Generic {
reason: reason.into(),
backtrace: BackTrace::capture(),
}
}
pub fn graph_capture_unsupported() -> Self {
Self::graph_state("graph capture is not supported by this backend")
}
}
pub trait Server:
core::any::Any + Send + core::fmt::Debug + ServerCommunication + device::DeviceService + 'static
{
fn initialize_memory(
&mut self,
memory: ManagedMemoryHandle,
size: u64,
stream_id: StreamId,
) -> Result<(), ServerError>;
fn staging(
&mut self,
_sizes: &[usize],
_stream_id: StreamId,
) -> Result<Vec<Bytes>, ServerError> {
Err(IoError::UnsupportedIoOperation {
backtrace: BackTrace::capture(),
}
.into())
}
fn logger(&self) -> Arc<ServerLogger>;
fn utilities(&self) -> Arc<ServerUtilities>;
fn read(
&mut self,
descriptors: Vec<CopyDescriptor>,
stream_id: StreamId,
) -> DynFut<Result<Vec<Bytes>, ServerError>>;
fn write(&mut self, descriptors: Vec<(CopyDescriptor, Bytes)>, stream_id: StreamId);
fn sync(
&mut self,
handles: Vec<BufferBinding>,
stream_id: StreamId,
) -> DynFut<Result<(), ServerError>>;
fn check(
&mut self,
handles: Vec<BufferBinding>,
stream_id: StreamId,
) -> Result<(), ServerError>;
unsafe fn launch(
&mut self,
kernel: Box<dyn CubeKernel>,
count: CubeCount,
bindings: KernelArguments,
stream_id: StreamId,
launch_mode: LaunchMode,
);
fn flush(&mut self, stream_id: StreamId) -> Result<(), ServerError>;
fn graph_prepare(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
let _ = stream_id;
Ok(())
}
fn begin_capture(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
let _ = stream_id;
Err(ServerError::graph_capture_unsupported())
}
fn end_capture(&mut self, stream_id: StreamId) -> Result<GraphId, ServerError> {
let _ = stream_id;
Err(ServerError::graph_capture_unsupported())
}
fn replay(&mut self, graph: GraphId, stream_id: StreamId) -> Result<(), ServerError> {
let _ = (graph, stream_id);
Err(ServerError::graph_capture_unsupported())
}
fn graph_destroy(&mut self, graph: GraphId, stream_id: StreamId) {
let _ = (graph, stream_id);
}
fn memory_report(&mut self, stream_id: StreamId) -> Option<StreamMemoryReport>;
fn stream_ids(&self) -> Vec<StreamId> {
Vec::from([StreamId::current()])
}
fn memory_cleanup(&mut self, stream_id: StreamId) -> Result<(), ServerError>;
fn start_profile(&mut self, stream_id: StreamId) -> Result<ProfilingToken, ServerError>;
fn end_profile(
&mut self,
stream_id: StreamId,
token: ProfilingToken,
) -> Result<ProfileDuration, ProfileError>;
fn abandon_profile(&mut self, stream_id: StreamId, token: ProfilingToken) {
let _ = self.end_profile(stream_id, token);
}
fn allocation_mode(&mut self, mode: MemoryAllocationMode, stream_id: StreamId);
}
pub trait ServerStorage: Server {
type Storage: ComputeStorage;
fn get_resource(
&mut self,
binding: BufferBinding,
stream_id: StreamId,
) -> Result<ManagedResource<<Self::Storage as ComputeStorage>::Resource>, ServerError>;
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct CommunicationId {
pub id: u64,
}
impl From<Vec<DeviceId>> for CommunicationId {
fn from(mut value: Vec<DeviceId>) -> Self {
value.sort();
CommunicationId {
id: FixedState::default().hash_one(value),
}
}
}
pub enum ReduceOperation {
Sum,
Mean,
}
pub trait ServerCommunication {
#[allow(unused_variables)]
fn sync_collective(&mut self, stream_id: StreamId) -> Result<(), ServerError> {
Err(ServerError::no_device_transport(Collective::SyncCollective))
}
#[allow(unused_variables)]
fn comm_init(&mut self, device_ids: Vec<DeviceId>) -> Result<(), ServerError> {
Err(ServerError::no_device_transport(Collective::CommInit))
}
#[allow(unused_variables)]
fn all_reduce(
&mut self,
src: BufferBinding,
dst: BufferBinding,
dtype: ElemType,
stream_id: StreamId,
op: ReduceOperation,
device_ids: Vec<DeviceId>,
) -> Result<(), ServerError> {
Err(ServerError::no_device_transport(Collective::AllReduce))
}
#[allow(unused_variables)]
fn send(
&mut self,
desc: CopyDescriptor,
dtype: ElemType,
stream_id: StreamId,
device_id_dst: DeviceId,
) -> Result<(), ServerError> {
Err(ServerError::no_device_transport(Collective::Send))
}
#[allow(unused_variables)]
fn recv(
&mut self,
handle: Handle,
dtype: ElemType,
stream_id: StreamId,
device_id_src: DeviceId,
) -> Result<(), ServerError> {
Err(ServerError::no_device_transport(Collective::Recv))
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct ProfilingToken {
pub id: u64,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum MemoryLayoutStrategy {
Contiguous,
Optimized,
}
#[derive(new, Debug, Clone)]
pub struct MemoryLayoutDescriptor {
pub strategy: MemoryLayoutStrategy,
pub shape: Shape,
pub elem_size: usize,
}
impl MemoryLayoutDescriptor {
pub fn optimized(shape: Shape, elem_size: usize) -> Self {
MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Optimized, shape, elem_size)
}
pub fn contiguous(shape: Shape, elem_size: usize) -> Self {
MemoryLayoutDescriptor::new(MemoryLayoutStrategy::Contiguous, shape, elem_size)
}
}
#[derive(Debug, Clone)]
pub struct MemoryLayout {
pub memory: Handle,
pub strides: Strides,
}
impl MemoryLayout {
pub fn new(handle: Handle, strides: impl Into<Strides>) -> Self {
MemoryLayout {
memory: handle,
strides: strides.into(),
}
}
}
#[derive(Default, Clone)]
pub struct Reason {
inner: ReasonInner,
}
#[cfg(serializable)]
mod _reason_serde {
use super::*;
use alloc::string::ToString;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for Reason {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Reason {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Reason {
inner: ReasonInner::Dynamic(Arc::new(s)),
})
}
}
}
#[derive(Default, Clone)]
enum ReasonInner {
Static(&'static str),
Dynamic(Arc<String>),
#[default]
NotProvided,
}
impl core::fmt::Display for Reason {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match &self.inner {
ReasonInner::Static(content) => f.write_str(content),
ReasonInner::Dynamic(content) => f.write_str(content),
ReasonInner::NotProvided => f.write_str("No reason provided for the error"),
}
}
}
impl core::fmt::Debug for Reason {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::Display::fmt(&self, f)
}
}
impl From<&'static str> for Reason {
fn from(value: &'static str) -> Self {
Self {
inner: ReasonInner::Static(value),
}
}
}
impl From<String> for Reason {
fn from(value: String) -> Self {
Self {
inner: ReasonInner::Dynamic(Arc::new(value)),
}
}
}
#[derive(Error, Clone)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
pub enum IoError {
#[error("can't allocate buffer of size: {size}\n{backtrace}")]
BufferTooBig {
size: u64,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("out of device memory allocating {size} bytes\n{backtrace}")]
OutOfMemory {
size: u64,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"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}"
)]
PageUpdateForbidden {
size: u64,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"Can't allocate {size} bytes: every page size the memory tracks still holds live allocations\n{backtrace}"
)]
PageSizesExhausted {
size: u64,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("the provided strides are not supported for this operation\n{backtrace}")]
UnsupportedStrides {
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("couldn't find resource for that handle: {reason}\n{backtrace}")]
NotFound {
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
reason: Reason,
},
#[error("the storage holds no allocation for that handle: {reason}\n{backtrace}")]
StorageHandleNotFound {
reason: Reason,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error(
"couldn't map storage for a deferred allocation of {size} bytes\nCaused by:\n {source}"
)]
StorageMappingFailed {
size: u64,
source: Box<IoError>,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("The device is poisoned: {0}")]
DevicePoisoned(#[from] DevicePoison),
#[error("Unknown error happened during execution: {description}\n{backtrace}")]
Unknown {
description: String,
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
#[error("The current IO operation is not supported\n{backtrace}")]
UnsupportedIoOperation {
#[cfg_attr(serializable, serde(skip))]
backtrace: BackTrace,
},
}
impl IoError {
pub fn is_device_poisoned(&self) -> bool {
match self {
Self::DevicePoisoned(_) => true,
Self::StorageMappingFailed { source, .. } => source.is_device_poisoned(),
_ => false,
}
}
pub fn may_succeed_after_reclaim(&self) -> bool {
!matches!(
self,
IoError::BufferTooBig { .. } | IoError::PageUpdateForbidden { .. }
)
}
}
impl core::fmt::Debug for IoError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_fmt(format_args!("{self}"))
}
}
#[derive(Debug, Default)]
pub struct KernelArguments {
pub resources: Vec<KernelResource>,
pub declared_io: Vec<BufferIOAttr>,
pub info: MetadataBindingInfo,
}
impl core::fmt::Display for KernelArguments {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("KernelArguments")?;
for b in self.resources.iter() {
f.write_fmt(format_args!("\n - buffer: {b:?}\n"))?;
}
Ok(())
}
}
impl KernelArguments {
pub fn new() -> Self {
Self::default()
}
pub fn with_buffer(mut self, binding: BufferBinding) -> Self {
self.resources.push(KernelResource::Buffer(binding));
self
}
pub fn with_buffer_io(mut self, binding: BufferBinding, io: BufferIOAttr) -> Self {
self.declared_io
.resize(self.resources.len(), BufferIOAttr::ReadWrite);
self.resources.push(KernelResource::Buffer(binding));
self.declared_io.push(io);
self
}
pub fn with_buffers(mut self, bindings: Vec<BufferBinding>) -> Self {
let bindings = bindings.into_iter().map(KernelResource::Buffer);
self.resources.extend(bindings);
self
}
pub fn with_info(mut self, info: MetadataBindingInfo) -> Self {
self.info = info;
self
}
pub fn with_tensor_maps(mut self, bindings: Vec<TensorMapBinding>) -> Self {
let bindings = bindings.into_iter().map(KernelResource::TensorMap);
self.resources.extend(bindings);
self
}
pub fn buffers(&self) -> impl Iterator<Item = &BufferBinding> {
self.resources.iter().map(|resource| match resource {
KernelResource::Buffer(binding) => binding,
KernelResource::TensorMap(tensor_map) => &tensor_map.binding,
})
}
pub fn memory_ids(&self) -> impl Iterator<Item = ManagedMemoryId> + '_ {
self.buffers().map(|binding| binding.memory.id())
}
pub fn buffers_written<'a>(
&'a self,
io: Option<&'a [BufferIOAttr]>,
) -> impl Iterator<Item = &'a BufferBinding> {
self.buffers()
.enumerate()
.filter_map(move |(index, binding)| {
let written = self
.io_attr(io, index)
.map(|io| io.is_writable())
.unwrap_or(true);
written.then_some(binding)
})
}
pub fn buffers_read<'a>(
&'a self,
io: Option<&'a [BufferIOAttr]>,
) -> impl Iterator<Item = &'a BufferBinding> {
self.buffers()
.enumerate()
.filter_map(move |(index, binding)| {
let read = self
.io_attr(io, index)
.map(|io| io.is_readable())
.unwrap_or(true);
read.then_some(binding)
})
}
fn io_attr(&self, compiled: Option<&[BufferIOAttr]>, index: usize) -> Option<BufferIOAttr> {
compiled
.and_then(|io| io.get(index))
.or_else(|| self.declared_io.get(index))
.copied()
}
}
#[derive(new, Debug, Default)]
pub struct MetadataBindingInfo {
pub data: Vec<u64>,
pub dynamic_metadata_offset: usize,
}
impl MetadataBindingInfo {
pub fn custom(data: Vec<u64>) -> Self {
Self::new(data, 0)
}
}
#[derive(new, Debug)]
pub struct CopyDescriptor {
pub handle: BufferBinding,
pub shape: Shape,
pub strides: Strides,
pub elem_size: usize,
}
#[derive(new, Clone, Debug)]
pub struct TensorMapBinding {
pub binding: BufferBinding,
pub map: TensorMapMeta,
}
#[derive(Debug, Clone)]
pub struct TensorMapMeta {
pub format: TensorMapFormat,
pub metadata: Metadata,
pub elem_stride: Strides,
pub interleave: TensorMapInterleave,
pub swizzle: TensorMapSwizzle,
pub prefetch: TensorMapPrefetch,
pub oob_fill: OobFill,
pub elem_ty: ElemType,
}
#[allow(clippy::large_enum_variant)]
pub enum CubeCount {
Static(u32, u32, u32),
Dynamic(BufferBinding),
}
pub enum CubeCountSelection {
Exact(CubeCount),
Approx(CubeCount, u32),
}
impl CubeCountSelection {
pub fn new(client: &Client, num_cubes: u32) -> Self {
let cube_count = cube_count_spread(&client.properties().hardware.max_cube_count, num_cubes);
let num_cubes_actual = cube_count[0] * cube_count[1] * cube_count[2];
let cube_count = CubeCount::Static(cube_count[0], cube_count[1], cube_count[2]);
match num_cubes_actual == num_cubes {
true => CubeCountSelection::Exact(cube_count),
false => CubeCountSelection::Approx(cube_count, num_cubes_actual),
}
}
pub fn has_idle(&self) -> bool {
matches!(self, Self::Approx(..))
}
pub fn cube_count(self) -> CubeCount {
match self {
CubeCountSelection::Exact(cube_count) => cube_count,
CubeCountSelection::Approx(cube_count, _) => cube_count,
}
}
}
impl From<CubeCountSelection> for CubeCount {
fn from(value: CubeCountSelection) -> Self {
value.cube_count()
}
}
impl CubeCount {
pub fn new_single() -> Self {
CubeCount::Static(1, 1, 1)
}
pub fn new_1d(x: u32) -> Self {
CubeCount::Static(x, 1, 1)
}
pub fn new_2d(x: u32, y: u32) -> Self {
CubeCount::Static(x, y, 1)
}
pub fn new_3d(x: u32, y: u32, z: u32) -> Self {
CubeCount::Static(x, y, z)
}
pub fn is_empty(&self) -> bool {
match self {
Self::Static(x, y, z) => *x == 0 || *y == 0 || *z == 0,
Self::Dynamic(_) => false,
}
}
}
impl Debug for CubeCount {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CubeCount::Static(x, y, z) => f.write_fmt(format_args!("({x}, {y}, {z})")),
CubeCount::Dynamic(_) => f.write_str("binding"),
}
}
}
impl Clone for CubeCount {
fn clone(&self) -> Self {
match self {
Self::Static(x, y, z) => Self::Static(*x, *y, *z),
Self::Dynamic(binding) => Self::Dynamic(binding.clone()),
}
}
}
#[derive(Debug, From, PartialEq, Eq, Clone, Copy, Hash, Deref, DerefMut)]
#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
#[allow(missing_docs)]
pub struct CubeDim(pub Dim3);
impl CubeDim {
pub fn new(client: &Client, working_units: usize) -> Self {
let properties = client.properties();
let plane_size = properties.hardware.plane_size_max;
let plane_count = Self::calculate_plane_count_per_cube(
working_units as u32,
plane_size,
properties.hardware.num_cpu_cores,
);
let limit = properties.hardware.max_units_per_cube / plane_size;
Self::new_2d(plane_size, u32::min(limit, plane_count).max(1))
}
fn calculate_plane_count_per_cube(
working_units: u32,
plane_dim: u32,
num_cpu_cores: Option<u32>,
) -> u32 {
match num_cpu_cores {
Some(num_cores) => core::cmp::min(num_cores, working_units),
None => {
let plane_count_max = core::cmp::max(1, working_units / plane_dim);
const NUM_PLANE_MAX: u32 = 8u32;
const NUM_PLANE_MAX_LOG2: u32 = NUM_PLANE_MAX.ilog2();
let plane_count_max_log2 =
core::cmp::min(NUM_PLANE_MAX_LOG2, u32::ilog2(plane_count_max));
2u32.pow(plane_count_max_log2)
}
}
}
pub const fn new_single() -> Self {
Self(Dim3::new_single())
}
pub const fn new_1d(x: u32) -> Self {
Self(Dim3::new_1d(x))
}
pub const fn new_2d(x: u32, y: u32) -> Self {
Self(Dim3::new_2d(x, y))
}
pub const fn new_3d(x: u32, y: u32, z: u32) -> Self {
Self(Dim3::new_3d(x, y, z))
}
pub const fn num_elems(&self) -> u32 {
self.0.num_elems()
}
pub const fn can_contain(&self, other: CubeDim) -> bool {
self.0.can_contain(other.0)
}
}
impl From<(u32, u32, u32)> for CubeDim {
fn from(value: (u32, u32, u32)) -> Self {
CubeDim::new_3d(value.0, value.1, value.2)
}
}
impl From<CubeDim> for (u32, u32, u32) {
fn from(val: CubeDim) -> Self {
(val.x, val.y, val.z)
}
}
impl From<CubeDim> for Dim3 {
fn from(value: CubeDim) -> Self {
value.0
}
}
fn cube_count_spread(max: &(u32, u32, u32), num_cubes: u32) -> [u32; 3] {
let max_cube_counts = [max.0, max.1, max.2];
let mut num_cubes = [num_cubes, 1, 1];
let base = 2;
let mut reduce_count = |i: usize| {
if num_cubes[i] <= max_cube_counts[i] {
return true;
}
loop {
num_cubes[i] = num_cubes[i].div_ceil(base);
num_cubes[i + 1] *= base;
if num_cubes[i] <= max_cube_counts[i] {
return false;
}
}
};
for i in 0..2 {
if reduce_count(i) {
break;
}
}
num_cubes
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
fn service() -> cubecl_common::device::ServiceId {
cubecl_common::device::ServiceId::of::<()>(cubecl_common::device::DeviceId::new(0, 0))
}
#[test_log::test]
fn safe_num_cubes_even() {
let max = (32, 32, 32);
let required = 2048;
let actual = cube_count_spread(&max, required);
let expected = [32, 32, 2];
assert_eq!(actual, expected);
}
#[test_log::test]
fn safe_num_cubes_odd() {
let max = (48, 32, 16);
let required = 3177;
let actual = cube_count_spread(&max, required);
let expected = [25, 32, 4];
assert_eq!(actual, expected);
}
#[test_log::test]
fn buffer_io_drives_the_read_and_write_sets() {
use crate::kernel::BufferIOAttr;
use cubecl_environment::stream::StreamId;
let stream = StreamId { value: 0 };
let args = KernelArguments::new().with_buffers(vec![
Handle::new(service(), stream, 8).binding(),
Handle::new(service(), stream, 8).binding(),
Handle::new(service(), stream, 8).binding(),
Handle::new(service(), stream, 8).binding(),
]);
let io = [
BufferIOAttr::ReadOnly,
BufferIOAttr::WriteOnly,
BufferIOAttr::ReadWrite,
BufferIOAttr::Dead,
];
let written: Vec<_> = args.buffers_written(Some(&io)).collect();
assert_eq!(written.len(), 2, "WriteOnly and ReadWrite are written");
assert!(core::ptr::eq(written[0], args.buffers().nth(1).unwrap()));
assert!(core::ptr::eq(written[1], args.buffers().nth(2).unwrap()));
let read: Vec<_> = args.buffers_read(Some(&io)).collect();
assert_eq!(read.len(), 2, "ReadOnly and ReadWrite are read");
assert!(core::ptr::eq(read[0], args.buffers().next().unwrap()));
assert!(core::ptr::eq(read[1], args.buffers().nth(2).unwrap()));
}
#[test_log::test]
fn poisoned_device_read_through_every_path() {
use crate::server::{IoError, LaunchError};
let poison = || DevicePoison {
reason: String::from("cuEventSynchronize failed with status 700"),
backtrace: Default::default(),
};
let poisoned = [
ServerError::DevicePoisoned(poison()),
ServerError::Launch(LaunchError::DevicePoisoned(poison())),
ServerError::Launch(LaunchError::CompilationError(
CompilationError::DevicePoisoned(poison()),
)),
ServerError::Io(IoError::DevicePoisoned(poison())),
ServerError::Io(IoError::StorageMappingFailed {
size: 1 << 20,
source: alloc::boxed::Box::new(IoError::DevicePoisoned(poison())),
backtrace: Default::default(),
}),
];
let refused =
ServerError::Launch(LaunchError::CompilationError(CompilationError::Generic {
reason: "no such intrinsic on this target".into(),
backtrace: Default::default(),
}));
let transient = ServerError::Generic {
reason: "the copy failed".into(),
backtrace: Default::default(),
};
for error in &poisoned {
assert!(error.is_device_poisoned(), "{error}");
assert!(
!error.is_refusal(),
"a poisoned device is not a refusal: {error}"
);
}
assert!(!refused.is_device_poisoned());
assert!(!transient.is_device_poisoned());
let unwritten = ServerError::Unwritten {
failure: 1,
claimed: 1,
chain: Vec::new(),
root: alloc::boxed::Box::new(poisoned[1].clone()),
backtrace: Default::default(),
};
assert!(unwritten.is_device_poisoned());
let group = ServerError::Several {
errors: vec![refused, unwritten],
backtrace: Default::default(),
};
assert!(group.is_device_poisoned());
assert!(!group.is_refusal());
}
#[test_log::test]
fn only_a_refused_kernel_reads_as_a_refusal() {
use crate::server::{LaunchError, ResourceLimitError};
let refused =
ServerError::Launch(LaunchError::CompilationError(CompilationError::Generic {
reason: "no such intrinsic on this target".into(),
backtrace: Default::default(),
}));
let over_budget = ServerError::Launch(LaunchError::TooManyResources(
ResourceLimitError::SharedMemory {
requested: 1 << 20,
max: 1 << 15,
backtrace: Default::default(),
},
));
let fault = ServerError::Generic {
reason: "the device faulted".into(),
backtrace: Default::default(),
};
let panicked = ServerError::Launch(LaunchError::CompilationError(
CompilationError::ExpansionPanicked {
kernel: "kernel".into(),
message: "an assertion failed".into(),
backtrace: Default::default(),
},
));
assert!(refused.is_refusal());
assert!(over_budget.is_refusal());
assert!(!fault.is_refusal(), "a fault is not a refusal");
assert!(
!panicked.is_refusal(),
"a kernel that panicked while expanding is a defect, not a refusal"
);
let unwritten = |root: &ServerError| ServerError::Unwritten {
failure: 1,
claimed: 1,
chain: Vec::new(),
root: alloc::boxed::Box::new(root.clone()),
backtrace: Default::default(),
};
assert!(unwritten(&refused).is_refusal());
assert!(!unwritten(&fault).is_refusal());
assert!(!unwritten(&panicked).is_refusal());
let group = |errors: Vec<ServerError>| ServerError::Several {
errors,
backtrace: Default::default(),
};
assert!(group(vec![unwritten(&refused), unwritten(&over_budget)]).is_refusal());
assert!(
!group(vec![unwritten(&refused), unwritten(&fault)]).is_refusal(),
"one real failure among refusals is still a real failure"
);
assert!(
!group(Vec::new()).is_refusal(),
"an empty group refuses nothing"
);
}
#[test_log::test]
fn missing_io_reads_as_everything_read_and_written() {
use crate::kernel::BufferIOAttr;
use cubecl_environment::stream::StreamId;
let stream = StreamId { value: 0 };
let args = KernelArguments::new().with_buffers(vec![
Handle::new(service(), stream, 8).binding(),
Handle::new(service(), stream, 8).binding(),
]);
assert_eq!(args.buffers_written(None).count(), 2);
assert_eq!(args.buffers_read(None).count(), 2);
let short = [BufferIOAttr::Dead];
assert_eq!(
args.buffers_written(Some(&short)).count(),
1,
"the uncovered resource reads as written"
);
assert_eq!(args.buffers_read(Some(&short)).count(), 1);
}
#[test_log::test]
fn declared_io_answers_when_the_compiled_kernel_kept_none() {
use crate::kernel::BufferIOAttr;
use cubecl_environment::stream::StreamId;
let stream = StreamId { value: 0 };
let args = KernelArguments::new()
.with_buffer_io(
Handle::new(service(), stream, 8).binding(),
BufferIOAttr::ReadOnly,
)
.with_buffer_io(
Handle::new(service(), stream, 8).binding(),
BufferIOAttr::ReadOnly,
)
.with_buffer_io(
Handle::new(service(), stream, 8).binding(),
BufferIOAttr::WriteOnly,
);
let written: Vec<_> = args.buffers_written(None).collect();
assert_eq!(written.len(), 1, "only the declared output is written");
assert!(core::ptr::eq(written[0], args.buffers().nth(2).unwrap()));
assert_eq!(args.buffers_read(None).count(), 2);
let compiled = [BufferIOAttr::ReadWrite];
let written: Vec<_> = args.buffers_written(Some(&compiled)).collect();
assert_eq!(written.len(), 2, "compiled ReadWrite plus declared output");
assert!(core::ptr::eq(written[0], args.buffers().next().unwrap()));
assert!(core::ptr::eq(written[1], args.buffers().nth(2).unwrap()));
}
#[test_log::test]
fn an_undeclared_resource_among_declared_ones_over_names() {
use crate::kernel::BufferIOAttr;
use cubecl_environment::stream::StreamId;
let stream = StreamId { value: 0 };
let args = KernelArguments::new()
.with_buffer(Handle::new(service(), stream, 8).binding())
.with_buffer_io(
Handle::new(service(), stream, 8).binding(),
BufferIOAttr::ReadOnly,
);
let written: Vec<_> = args.buffers_written(None).collect();
assert_eq!(written.len(), 1, "the undeclared resource reads as written");
assert!(core::ptr::eq(written[0], args.buffers().next().unwrap()));
assert_eq!(args.buffers_read(None).count(), 2);
}
}