use crate::{CudaFunction, CudaStream, DriverError};
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
#[derive(Clone, Copy, Debug)]
pub struct LaunchConfig {
pub grid_dim: (u32, u32, u32),
pub block_dim: (u32, u32, u32),
pub shared_mem_bytes: u32,
}
impl LaunchConfig {
pub fn for_num_elems(n: u32) -> Self {
const DEFAULT_BLOCK_SIZE: u32 = 256;
let grid_x = n.div_ceil(DEFAULT_BLOCK_SIZE);
LaunchConfig {
grid_dim: (grid_x, 1, 1),
block_dim: (DEFAULT_BLOCK_SIZE, 1, 1),
shared_mem_bytes: 0,
}
}
}
mod sealed {
pub trait Sealed {}
}
pub trait KernelLaunchConfig: sealed::Sealed + Copy {
#[doc(hidden)]
fn __raw(self) -> LaunchConfig;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchConfig1D {
grid_x: u32,
block_x: u32,
shared_mem_bytes: u32,
}
impl LaunchConfig1D {
pub const fn new(grid_x: u32, block_x: u32, shared_mem_bytes: u32) -> Self {
Self {
grid_x,
block_x,
shared_mem_bytes,
}
}
}
impl sealed::Sealed for LaunchConfig1D {}
impl KernelLaunchConfig for LaunchConfig1D {
fn __raw(self) -> LaunchConfig {
LaunchConfig {
grid_dim: (self.grid_x, 1, 1),
block_dim: (self.block_x, 1, 1),
shared_mem_bytes: self.shared_mem_bytes,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchConfig2D {
grid: (u32, u32),
block: (u32, u32),
shared_mem_bytes: u32,
}
impl LaunchConfig2D {
pub const fn new(grid: (u32, u32), block: (u32, u32), shared_mem_bytes: u32) -> Self {
Self {
grid,
block,
shared_mem_bytes,
}
}
}
impl sealed::Sealed for LaunchConfig2D {}
impl KernelLaunchConfig for LaunchConfig2D {
fn __raw(self) -> LaunchConfig {
LaunchConfig {
grid_dim: (self.grid.0, self.grid.1, 1),
block_dim: (self.block.0, self.block.1, 1),
shared_mem_bytes: self.shared_mem_bytes,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchConfig3D {
grid: (u32, u32, u32),
block: (u32, u32, u32),
shared_mem_bytes: u32,
}
impl LaunchConfig3D {
pub const fn new(grid: (u32, u32, u32), block: (u32, u32, u32), shared_mem_bytes: u32) -> Self {
Self {
grid,
block,
shared_mem_bytes,
}
}
}
impl sealed::Sealed for LaunchConfig3D {}
impl KernelLaunchConfig for LaunchConfig3D {
fn __raw(self) -> LaunchConfig {
LaunchConfig {
grid_dim: self.grid,
block_dim: self.block,
shared_mem_bytes: self.shared_mem_bytes,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlockRequirement {
Exact((u32, u32, u32)),
MaxThreads(u32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DynamicSharedMemoryRequirement {
Exact {
bytes: u32,
min_alignment: u32,
},
Range {
min_bytes: u32,
max_bytes: u32,
min_alignment: u32,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CoordinateRequirement {
#[default]
Native,
U32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchContractSpec {
kernel_name: &'static str,
block: BlockRequirement,
dynamic_shared_memory: DynamicSharedMemoryRequirement,
cluster: Option<(u32, u32, u32)>,
cooperative: bool,
min_compute_capability: Option<(u32, u32)>,
coordinates: CoordinateRequirement,
}
impl LaunchContractSpec {
pub const fn new(
kernel_name: &'static str,
block: BlockRequirement,
dynamic_shared_memory: DynamicSharedMemoryRequirement,
) -> Self {
Self {
kernel_name,
block,
dynamic_shared_memory,
cluster: None,
cooperative: false,
min_compute_capability: None,
coordinates: CoordinateRequirement::Native,
}
}
#[must_use]
pub const fn with_cluster(mut self, cluster: (u32, u32, u32)) -> Self {
self.cluster = Some(cluster);
self
}
#[must_use]
pub const fn with_cooperative(mut self) -> Self {
self.cooperative = true;
self
}
#[must_use]
pub const fn with_min_compute_capability(mut self, major: u32, minor: u32) -> Self {
self.min_compute_capability = Some((major, minor));
self
}
#[must_use]
pub const fn with_u32_coordinates(mut self) -> Self {
self.coordinates = CoordinateRequirement::U32;
self
}
pub const fn kernel_name(&self) -> &'static str {
self.kernel_name
}
pub const fn block(&self) -> BlockRequirement {
self.block
}
pub const fn dynamic_shared_memory(&self) -> DynamicSharedMemoryRequirement {
self.dynamic_shared_memory
}
pub const fn cluster(&self) -> Option<(u32, u32, u32)> {
self.cluster
}
pub const fn cooperative(&self) -> bool {
self.cooperative
}
pub const fn min_compute_capability(&self) -> Option<(u32, u32)> {
self.min_compute_capability
}
pub const fn coordinates(&self) -> CoordinateRequirement {
self.coordinates
}
}
pub trait KernelLaunchContract {
type Config: KernelLaunchConfig;
const SPEC: LaunchContractSpec;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DeviceLaunchLimits {
pub(crate) max_threads_per_block: u32,
pub(crate) max_block_dim: (u32, u32, u32),
pub(crate) max_grid_dim: (u32, u32, u32),
pub(crate) max_shared_memory_per_block: u32,
}
impl DeviceLaunchLimits {
pub const fn max_threads_per_block(&self) -> u32 {
self.max_threads_per_block
}
pub const fn max_block_dim(&self) -> (u32, u32, u32) {
self.max_block_dim
}
pub const fn max_grid_dim(&self) -> (u32, u32, u32) {
self.max_grid_dim
}
pub const fn max_shared_memory_per_block(&self) -> u32 {
self.max_shared_memory_per_block
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LaunchDimension {
Grid,
Block,
Cluster,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LaunchAxis {
X,
Y,
Z,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum LaunchContractError {
EmptyKernelName,
ZeroDimension {
kernel: &'static str,
dimension: LaunchDimension,
axis: LaunchAxis,
},
DimensionProductOverflow {
kernel: &'static str,
dimension: LaunchDimension,
},
CoordinateRangeExceedsU32 {
kernel: &'static str,
axis: LaunchAxis,
grid: u32,
block: u32,
positions: u64,
},
InvalidSharedMemoryAlignment {
kernel: &'static str,
alignment: u32,
},
InvalidSharedMemoryRange {
kernel: &'static str,
min_bytes: u32,
max_bytes: u32,
},
BlockShapeMismatch {
kernel: &'static str,
required: (u32, u32, u32),
actual: (u32, u32, u32),
},
BlockThreadsExceedContract {
kernel: &'static str,
actual: u64,
max: u32,
},
DynamicSharedMemoryExactMismatch {
kernel: &'static str,
required: u32,
actual: u32,
},
DynamicSharedMemoryOutsideRange {
kernel: &'static str,
min: u32,
max: u32,
actual: u32,
},
ClusterDoesNotDivideGrid {
kernel: &'static str,
axis: LaunchAxis,
grid: u32,
cluster: u32,
},
DeviceDimensionExceeded {
kernel: &'static str,
dimension: LaunchDimension,
axis: LaunchAxis,
actual: u32,
max: u32,
},
DeviceThreadsPerBlockExceeded {
kernel: &'static str,
actual: u64,
max: u32,
},
FunctionThreadsPerBlockExceeded {
kernel: &'static str,
actual: u64,
max: u32,
},
SharedMemoryTotalOverflow {
kernel: &'static str,
static_bytes: u32,
dynamic_bytes: u32,
},
DeviceSharedMemoryExceeded {
kernel: &'static str,
total: u64,
max: u32,
opt_in: bool,
},
ComputeCapabilityTooLow {
kernel: &'static str,
required: (u32, u32),
actual: (u32, u32),
},
CooperativeLaunchUnsupported {
kernel: &'static str,
},
ClusterLaunchUnsupported {
kernel: &'static str,
},
ClusterSizeExceeded {
kernel: &'static str,
blocks: u64,
max: u32,
},
FunctionClusterShapeMismatch {
kernel: &'static str,
declared: (u32, u32, u32),
required: (u32, u32, u32),
},
RequiredClusterDimensionsMissing {
kernel: &'static str,
declared: (u32, u32, u32),
},
ClusterShapeUnsupported {
kernel: &'static str,
cluster: (u32, u32, u32),
},
ClusterHasNoResidency {
kernel: &'static str,
cluster: (u32, u32, u32),
},
CooperativeGridTooLarge {
kernel: &'static str,
blocks: u64,
resident_capacity: u64,
},
ContextMismatch {
kernel: &'static str,
function_device: usize,
stream_device: usize,
},
SizeRequirementViolated {
kernel: &'static str,
relation: &'static str,
lhs: u64,
rhs: u64,
},
SizeRequirementOverflow {
kernel: &'static str,
relation: &'static str,
},
Driver(DriverError),
}
impl Display for LaunchContractError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyKernelName => write!(f, "kernel launch contract has an empty name"),
Self::ZeroDimension {
kernel,
dimension,
axis,
} => write!(f, "{kernel}: {dimension:?}.{axis:?} must be nonzero"),
Self::DimensionProductOverflow { kernel, dimension } => {
write!(f, "{kernel}: {dimension:?} dimension product overflowed")
}
Self::CoordinateRangeExceedsU32 {
kernel,
axis,
grid,
block,
positions,
} => write!(
f,
"{kernel}: Grid.{axis:?} {grid} * Block.{axis:?} {block} requires {positions} coordinates, exceeding the u32 range"
),
Self::InvalidSharedMemoryAlignment { kernel, alignment } => write!(
f,
"{kernel}: dynamic shared-memory alignment {alignment} is not a nonzero power of two"
),
Self::InvalidSharedMemoryRange {
kernel,
min_bytes,
max_bytes,
} => write!(
f,
"{kernel}: dynamic shared-memory range {min_bytes}..={max_bytes} is invalid"
),
Self::BlockShapeMismatch {
kernel,
required,
actual,
} => write!(
f,
"{kernel}: block {actual:?} does not match required block {required:?}"
),
Self::BlockThreadsExceedContract {
kernel,
actual,
max,
} => write!(
f,
"{kernel}: block has {actual} threads; contract maximum is {max}"
),
Self::DynamicSharedMemoryExactMismatch {
kernel,
required,
actual,
} => write!(
f,
"{kernel}: dynamic shared memory is {actual} bytes; contract requires {required}"
),
Self::DynamicSharedMemoryOutsideRange {
kernel,
min,
max,
actual,
} => write!(
f,
"{kernel}: dynamic shared memory is {actual} bytes; contract permits {min}..={max}"
),
Self::ClusterDoesNotDivideGrid {
kernel,
axis,
grid,
cluster,
} => write!(
f,
"{kernel}: cluster {axis:?} size {cluster} does not divide grid size {grid}"
),
Self::DeviceDimensionExceeded {
kernel,
dimension,
axis,
actual,
max,
} => write!(
f,
"{kernel}: {dimension:?}.{axis:?} size {actual} exceeds device maximum {max}"
),
Self::DeviceThreadsPerBlockExceeded {
kernel,
actual,
max,
} => write!(
f,
"{kernel}: block has {actual} threads; device maximum is {max}"
),
Self::FunctionThreadsPerBlockExceeded {
kernel,
actual,
max,
} => write!(
f,
"{kernel}: block has {actual} threads; function maximum is {max}"
),
Self::SharedMemoryTotalOverflow {
kernel,
static_bytes,
dynamic_bytes,
} => write!(
f,
"{kernel}: {static_bytes} static + {dynamic_bytes} dynamic shared-memory bytes overflowed"
),
Self::DeviceSharedMemoryExceeded {
kernel,
total,
max,
opt_in,
} => write!(
f,
"{kernel}: {total} shared-memory bytes exceed the device {} limit {max}",
if *opt_in { "opt-in" } else { "portable" }
),
Self::ComputeCapabilityTooLow {
kernel,
required,
actual,
} => write!(
f,
"{kernel}: compute capability {}.{} is below required {}.{}",
actual.0, actual.1, required.0, required.1
),
Self::CooperativeLaunchUnsupported { kernel } => {
write!(f, "{kernel}: device does not support cooperative launch")
}
Self::ClusterLaunchUnsupported { kernel } => {
write!(f, "{kernel}: device does not support cluster launch")
}
Self::ClusterSizeExceeded {
kernel,
blocks,
max,
} => write!(
f,
"{kernel}: cluster has {blocks} blocks; live maximum is {max}"
),
Self::FunctionClusterShapeMismatch {
kernel,
declared,
required,
} => write!(
f,
"{kernel}: declared cluster {declared:?} does not match function-required cluster {required:?}"
),
Self::RequiredClusterDimensionsMissing { kernel, declared } => write!(
f,
"{kernel}: declared cluster {declared:?} is missing from the compiled function metadata"
),
Self::ClusterShapeUnsupported { kernel, cluster } => write!(
f,
"{kernel}: cluster shape {cluster:?} is unsupported for this launch"
),
Self::ClusterHasNoResidency { kernel, cluster } => write!(
f,
"{kernel}: no cluster with shape {cluster:?} can be resident"
),
Self::CooperativeGridTooLarge {
kernel,
blocks,
resident_capacity,
} => write!(
f,
"{kernel}: cooperative grid has {blocks} blocks but only {resident_capacity} can be resident"
),
Self::ContextMismatch {
kernel,
function_device,
stream_device,
} => write!(
f,
"{kernel}: function is on device {function_device}, stream is on device {stream_device}"
),
Self::SizeRequirementViolated {
kernel,
relation,
lhs,
rhs,
} => write!(
f,
"{kernel}: size requirement `{relation}` violated: left-hand side is {lhs}, right-hand side is {rhs}"
),
Self::SizeRequirementOverflow { kernel, relation } => write!(
f,
"{kernel}: arithmetic in size requirement `{relation}` overflowed the u64 range"
),
Self::Driver(error) => Display::fmt(error, f),
}
}
}
impl Error for LaunchContractError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Driver(error) => Some(error),
_ => None,
}
}
}
impl From<DriverError> for LaunchContractError {
fn from(value: DriverError) -> Self {
Self::Driver(value)
}
}
pub struct PreparedLaunch<C: KernelLaunchContract> {
function: CudaFunction,
config: LaunchConfig,
_contract: PhantomData<fn(C) -> C>,
}
impl<C: KernelLaunchContract> Clone for PreparedLaunch<C> {
fn clone(&self) -> Self {
Self {
function: self.function.clone(),
config: self.config,
_contract: PhantomData,
}
}
}
impl<C: KernelLaunchContract> PreparedLaunch<C> {
#[doc(hidden)]
pub unsafe fn __prepare(
function: CudaFunction,
config: C::Config,
) -> Result<Self, LaunchContractError> {
let raw = config.__raw();
validate_static(C::SPEC, raw)?;
let context = function.context();
let limits = context.launch_limits()?;
let function_max_threads = function.max_threads_per_block()?;
let static_shared = function.static_shared_memory_bytes()?;
let function_max_dynamic = function.max_dynamic_shared_memory_bytes()?;
validate_live_shape(C::SPEC, raw, limits, function_max_threads)?;
let contract_dynamic_max = dynamic_shared_memory_max(C::SPEC.dynamic_shared_memory);
let total_shared =
shared_memory_total(C::SPEC.kernel_name, static_shared, contract_dynamic_max)?;
if validate_shared_memory_limit(
C::SPEC.kernel_name,
total_shared,
limits.max_shared_memory_per_block,
false,
)
.is_err()
{
let opt_in_max = context.max_opt_in_shared_memory_per_block()?;
validate_shared_memory_limit(C::SPEC.kernel_name, total_shared, opt_in_max, true)?;
}
if let Some(required) = C::SPEC.min_compute_capability {
let (major, minor) = context.compute_capability()?;
let actual = (
u32::try_from(major).map_err(|_| {
DriverError(cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_VALUE)
})?,
u32::try_from(minor).map_err(|_| {
DriverError(cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_VALUE)
})?,
);
validate_compute_capability(C::SPEC.kernel_name, required, actual)?;
}
if let Some(cluster) = C::SPEC.cluster {
validate_cluster_support(C::SPEC.kernel_name, context.supports_cluster_launch()?)?;
validate_required_cluster(
C::SPEC.kernel_name,
cluster,
function.required_cluster_dimensions()?,
)?;
}
if C::SPEC.cooperative {
validate_cooperative_support(
C::SPEC.kernel_name,
context.supports_cooperative_launch()?,
)?;
}
if contract_dynamic_max > function_max_dynamic {
function.set_max_dynamic_shared_memory_bytes(contract_dynamic_max)?;
}
let mut clustered_resident_blocks: Option<u64> = None;
if let Some(cluster) = C::SPEC.cluster {
let max_cluster_size = function.max_potential_cluster_size(
raw.grid_dim,
raw.block_dim,
raw.shared_mem_bytes,
)?;
let cluster_blocks =
shape_product(C::SPEC.kernel_name, LaunchDimension::Cluster, cluster)?;
validate_cluster_size(C::SPEC.kernel_name, cluster_blocks, max_cluster_size)?;
let active_clusters = match function.max_active_clusters(
raw.grid_dim,
raw.block_dim,
raw.shared_mem_bytes,
cluster,
) {
Ok(active_clusters) => active_clusters,
Err(error)
if error.0 == cuda_bindings::cudaError_enum_CUDA_ERROR_INVALID_CLUSTER_SIZE =>
{
return Err(LaunchContractError::ClusterShapeUnsupported {
kernel: C::SPEC.kernel_name,
cluster,
});
}
Err(error) => return Err(error.into()),
};
validate_cluster_residency(C::SPEC.kernel_name, cluster, active_clusters)?;
clustered_resident_blocks = Some(
u64::from(active_clusters)
.checked_mul(cluster_blocks)
.ok_or(LaunchContractError::DimensionProductOverflow {
kernel: C::SPEC.kernel_name,
dimension: LaunchDimension::Cluster,
})?,
);
}
if C::SPEC.cooperative {
let blocks = shape_product(C::SPEC.kernel_name, LaunchDimension::Grid, raw.grid_dim)?;
let resident_capacity = if let Some(capacity) = clustered_resident_blocks {
capacity
} else {
let block_threads =
shape_product(C::SPEC.kernel_name, LaunchDimension::Block, raw.block_dim)?;
let active_per_sm = function.max_active_blocks_per_multiprocessor(
block_threads as u32,
raw.shared_mem_bytes,
)?;
let multiprocessors = context.multiprocessor_count()?;
u64::from(active_per_sm) * u64::from(multiprocessors)
};
validate_cooperative_residency(C::SPEC.kernel_name, blocks, resident_capacity)?;
}
Ok(Self {
function,
config: raw,
_contract: PhantomData,
})
}
pub fn function(&self) -> &CudaFunction {
&self.function
}
#[doc(hidden)]
pub fn __raw_config(&self) -> LaunchConfig {
self.config
}
pub fn validate_stream(&self, stream: &CudaStream) -> Result<(), LaunchContractError> {
let function_context = self.function.context();
let stream_context = stream.context();
if function_context.cu_ctx() == stream_context.cu_ctx() {
Ok(())
} else {
Err(LaunchContractError::ContextMismatch {
kernel: C::SPEC.kernel_name,
function_device: function_context.ordinal(),
stream_device: stream_context.ordinal(),
})
}
}
}
fn validate_static(
spec: LaunchContractSpec,
config: LaunchConfig,
) -> Result<(), LaunchContractError> {
if spec.kernel_name.trim().is_empty() {
return Err(LaunchContractError::EmptyKernelName);
}
validate_shape(spec.kernel_name, LaunchDimension::Grid, config.grid_dim)?;
validate_shape(spec.kernel_name, LaunchDimension::Block, config.block_dim)?;
if spec.coordinates == CoordinateRequirement::U32 {
validate_u32_coordinates(spec.kernel_name, config.grid_dim, config.block_dim)?;
}
match spec.block {
BlockRequirement::Exact(required) => {
validate_shape(spec.kernel_name, LaunchDimension::Block, required)?;
if config.block_dim != required {
return Err(LaunchContractError::BlockShapeMismatch {
kernel: spec.kernel_name,
required,
actual: config.block_dim,
});
}
}
BlockRequirement::MaxThreads(max) => {
let actual = shape_product(spec.kernel_name, LaunchDimension::Block, config.block_dim)?;
if actual > u64::from(max) {
return Err(LaunchContractError::BlockThreadsExceedContract {
kernel: spec.kernel_name,
actual,
max,
});
}
}
}
match spec.dynamic_shared_memory {
DynamicSharedMemoryRequirement::Exact {
bytes,
min_alignment,
} => {
validate_alignment(spec.kernel_name, min_alignment)?;
if config.shared_mem_bytes != bytes {
return Err(LaunchContractError::DynamicSharedMemoryExactMismatch {
kernel: spec.kernel_name,
required: bytes,
actual: config.shared_mem_bytes,
});
}
}
DynamicSharedMemoryRequirement::Range {
min_bytes,
max_bytes,
min_alignment,
} => {
validate_alignment(spec.kernel_name, min_alignment)?;
if min_bytes > max_bytes {
return Err(LaunchContractError::InvalidSharedMemoryRange {
kernel: spec.kernel_name,
min_bytes,
max_bytes,
});
}
if !(min_bytes..=max_bytes).contains(&config.shared_mem_bytes) {
return Err(LaunchContractError::DynamicSharedMemoryOutsideRange {
kernel: spec.kernel_name,
min: min_bytes,
max: max_bytes,
actual: config.shared_mem_bytes,
});
}
}
}
if let Some(cluster) = spec.cluster {
validate_shape(spec.kernel_name, LaunchDimension::Cluster, cluster)?;
for (axis, grid, cluster) in axes(config.grid_dim, cluster) {
if grid % cluster != 0 {
return Err(LaunchContractError::ClusterDoesNotDivideGrid {
kernel: spec.kernel_name,
axis,
grid,
cluster,
});
}
}
}
Ok(())
}
fn validate_u32_coordinates(
kernel: &'static str,
grid: (u32, u32, u32),
block: (u32, u32, u32),
) -> Result<(), LaunchContractError> {
const U32_COORDINATE_COUNT: u64 = u32::MAX as u64 + 1;
for (axis, grid, block) in axes(grid, block) {
let positions = u64::from(grid) * u64::from(block);
if positions > U32_COORDINATE_COUNT {
return Err(LaunchContractError::CoordinateRangeExceedsU32 {
kernel,
axis,
grid,
block,
positions,
});
}
}
Ok(())
}
fn validate_live_shape(
spec: LaunchContractSpec,
config: LaunchConfig,
limits: DeviceLaunchLimits,
function_max_threads: u32,
) -> Result<(), LaunchContractError> {
validate_axes(config.grid_dim, limits.max_grid_dim, |axis, actual, max| {
LaunchContractError::DeviceDimensionExceeded {
kernel: spec.kernel_name,
dimension: LaunchDimension::Grid,
axis,
actual,
max,
}
})?;
validate_axes(
config.block_dim,
limits.max_block_dim,
|axis, actual, max| LaunchContractError::DeviceDimensionExceeded {
kernel: spec.kernel_name,
dimension: LaunchDimension::Block,
axis,
actual,
max,
},
)?;
let threads = shape_product(spec.kernel_name, LaunchDimension::Block, config.block_dim)?;
if threads > u64::from(limits.max_threads_per_block) {
return Err(LaunchContractError::DeviceThreadsPerBlockExceeded {
kernel: spec.kernel_name,
actual: threads,
max: limits.max_threads_per_block,
});
}
if threads > u64::from(function_max_threads) {
return Err(LaunchContractError::FunctionThreadsPerBlockExceeded {
kernel: spec.kernel_name,
actual: threads,
max: function_max_threads,
});
}
Ok(())
}
fn shared_memory_total(
kernel: &'static str,
static_bytes: u32,
dynamic_bytes: u32,
) -> Result<u64, LaunchContractError> {
static_bytes
.checked_add(dynamic_bytes)
.map(u64::from)
.ok_or(LaunchContractError::SharedMemoryTotalOverflow {
kernel,
static_bytes,
dynamic_bytes,
})
}
const fn dynamic_shared_memory_max(requirement: DynamicSharedMemoryRequirement) -> u32 {
match requirement {
DynamicSharedMemoryRequirement::Exact { bytes, .. } => bytes,
DynamicSharedMemoryRequirement::Range { max_bytes, .. } => max_bytes,
}
}
fn validate_shared_memory_limit(
kernel: &'static str,
total: u64,
max: u32,
opt_in: bool,
) -> Result<(), LaunchContractError> {
if total <= u64::from(max) {
Ok(())
} else {
Err(LaunchContractError::DeviceSharedMemoryExceeded {
kernel,
total,
max,
opt_in,
})
}
}
fn validate_compute_capability(
kernel: &'static str,
required: (u32, u32),
actual: (u32, u32),
) -> Result<(), LaunchContractError> {
if actual >= required {
Ok(())
} else {
Err(LaunchContractError::ComputeCapabilityTooLow {
kernel,
required,
actual,
})
}
}
fn validate_cluster_support(
kernel: &'static str,
supported: bool,
) -> Result<(), LaunchContractError> {
if supported {
Ok(())
} else {
Err(LaunchContractError::ClusterLaunchUnsupported { kernel })
}
}
fn validate_cluster_size(
kernel: &'static str,
blocks: u64,
max: u32,
) -> Result<(), LaunchContractError> {
if blocks <= u64::from(max) {
Ok(())
} else {
Err(LaunchContractError::ClusterSizeExceeded {
kernel,
blocks,
max,
})
}
}
fn validate_required_cluster(
kernel: &'static str,
declared: (u32, u32, u32),
required: Option<(u32, u32, u32)>,
) -> Result<(), LaunchContractError> {
match required {
Some(required) if declared == required => Ok(()),
Some(required) => Err(LaunchContractError::FunctionClusterShapeMismatch {
kernel,
declared,
required,
}),
None => Err(LaunchContractError::RequiredClusterDimensionsMissing { kernel, declared }),
}
}
fn validate_cluster_residency(
kernel: &'static str,
cluster: (u32, u32, u32),
active_clusters: u32,
) -> Result<(), LaunchContractError> {
if active_clusters != 0 {
Ok(())
} else {
Err(LaunchContractError::ClusterHasNoResidency { kernel, cluster })
}
}
fn validate_cooperative_support(
kernel: &'static str,
supported: bool,
) -> Result<(), LaunchContractError> {
if supported {
Ok(())
} else {
Err(LaunchContractError::CooperativeLaunchUnsupported { kernel })
}
}
fn validate_cooperative_residency(
kernel: &'static str,
blocks: u64,
resident_capacity: u64,
) -> Result<(), LaunchContractError> {
if blocks <= resident_capacity {
Ok(())
} else {
Err(LaunchContractError::CooperativeGridTooLarge {
kernel,
blocks,
resident_capacity,
})
}
}
fn validate_alignment(kernel: &'static str, alignment: u32) -> Result<(), LaunchContractError> {
if alignment.is_power_of_two() {
Ok(())
} else {
Err(LaunchContractError::InvalidSharedMemoryAlignment { kernel, alignment })
}
}
fn validate_shape(
kernel: &'static str,
dimension: LaunchDimension,
shape: (u32, u32, u32),
) -> Result<(), LaunchContractError> {
for (axis, value, _) in axes(shape, shape) {
if value == 0 {
return Err(LaunchContractError::ZeroDimension {
kernel,
dimension,
axis,
});
}
}
shape_product(kernel, dimension, shape)?;
Ok(())
}
fn shape_product(
kernel: &'static str,
dimension: LaunchDimension,
shape: (u32, u32, u32),
) -> Result<u64, LaunchContractError> {
u64::from(shape.0)
.checked_mul(u64::from(shape.1))
.and_then(|xy| xy.checked_mul(u64::from(shape.2)))
.ok_or(LaunchContractError::DimensionProductOverflow { kernel, dimension })
}
fn axes(actual: (u32, u32, u32), limit: (u32, u32, u32)) -> [(LaunchAxis, u32, u32); 3] {
[
(LaunchAxis::X, actual.0, limit.0),
(LaunchAxis::Y, actual.1, limit.1),
(LaunchAxis::Z, actual.2, limit.2),
]
}
fn validate_axes(
actual: (u32, u32, u32),
limit: (u32, u32, u32),
error: impl Fn(LaunchAxis, u32, u32) -> LaunchContractError,
) -> Result<(), LaunchContractError> {
for (axis, actual, max) in axes(actual, limit) {
if actual > max {
return Err(error(axis, actual, max));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const KERNEL: &str = "test_kernel";
struct NonCloneContract;
impl KernelLaunchContract for NonCloneContract {
type Config = LaunchConfig1D;
const SPEC: LaunchContractSpec = LaunchContractSpec::new(
"non_clone",
BlockRequirement::Exact((1, 1, 1)),
DynamicSharedMemoryRequirement::Exact {
bytes: 0,
min_alignment: 1,
},
);
}
fn exact_spec(block: (u32, u32, u32)) -> LaunchContractSpec {
LaunchContractSpec::new(
KERNEL,
BlockRequirement::Exact(block),
DynamicSharedMemoryRequirement::Exact {
bytes: 0,
min_alignment: 1,
},
)
}
fn raw(
grid_dim: (u32, u32, u32),
block_dim: (u32, u32, u32),
shared_mem_bytes: u32,
) -> LaunchConfig {
LaunchConfig {
grid_dim,
block_dim,
shared_mem_bytes,
}
}
fn generous_limits() -> DeviceLaunchLimits {
DeviceLaunchLimits {
max_threads_per_block: 1024,
max_block_dim: (1024, 1024, 64),
max_grid_dim: (u32::MAX, 65_535, 65_535),
max_shared_memory_per_block: 48 * 1024,
}
}
#[test]
fn typed_configs_fix_trailing_dimensions() {
let one = LaunchConfig1D::new(7, 64, 16).__raw();
assert_eq!(one.grid_dim, (7, 1, 1));
assert_eq!(one.block_dim, (64, 1, 1));
assert_eq!(one.shared_mem_bytes, 16);
let two = LaunchConfig2D::new((7, 5), (16, 8), 32).__raw();
assert_eq!(two.grid_dim, (7, 5, 1));
assert_eq!(two.block_dim, (16, 8, 1));
let three = LaunchConfig3D::new((7, 5, 3), (16, 8, 2), 64).__raw();
assert_eq!(three.grid_dim, (7, 5, 3));
assert_eq!(three.block_dim, (16, 8, 2));
}
#[test]
fn prepared_launch_clone_does_not_require_clone_brand() {
fn assert_clone<T: Clone>() {}
assert_clone::<PreparedLaunch<NonCloneContract>>();
}
#[test]
fn rejects_zero_and_overflowing_shapes() {
let zero = validate_static(exact_spec((32, 1, 1)), raw((0, 1, 1), (32, 1, 1), 0));
assert!(matches!(
zero,
Err(LaunchContractError::ZeroDimension {
dimension: LaunchDimension::Grid,
axis: LaunchAxis::X,
..
})
));
let overflow = validate_static(
exact_spec((1, 1, 1)),
raw((u32::MAX, u32::MAX, 2), (1, 1, 1), 0),
);
assert!(matches!(
overflow,
Err(LaunchContractError::DimensionProductOverflow {
dimension: LaunchDimension::Grid,
..
})
));
}
#[test]
fn exact_block_requires_the_whole_shape() {
let result = validate_static(exact_spec((32, 4, 1)), raw((1, 1, 1), (64, 2, 1), 0));
assert!(matches!(
result,
Err(LaunchContractError::BlockShapeMismatch {
required: (32, 4, 1),
actual: (64, 2, 1),
..
})
));
}
#[test]
fn max_threads_applies_to_the_block_product_and_checks_overflow() {
let spec = LaunchContractSpec::new(
KERNEL,
BlockRequirement::MaxThreads(256),
DynamicSharedMemoryRequirement::Exact {
bytes: 0,
min_alignment: 1,
},
);
assert!(validate_static(spec, raw((1, 1, 1), (128, 1, 1), 0)).is_ok());
assert!(validate_static(spec, raw((1, 1, 1), (16, 16, 1), 0)).is_ok());
assert!(matches!(
validate_static(spec, raw((1, 1, 1), (17, 16, 1), 0)),
Err(LaunchContractError::BlockThreadsExceedContract {
actual: 272,
max: 256,
..
})
));
let overflowing_spec = LaunchContractSpec::new(
KERNEL,
BlockRequirement::MaxThreads(u32::MAX),
DynamicSharedMemoryRequirement::Exact {
bytes: 0,
min_alignment: 1,
},
);
assert!(matches!(
validate_static(overflowing_spec, raw((1, 1, 1), (u32::MAX, u32::MAX, 2), 0),),
Err(LaunchContractError::DimensionProductOverflow {
dimension: LaunchDimension::Block,
..
})
));
}
#[test]
fn u32_coordinate_contract_accepts_exact_range_and_rejects_larger_axis() {
let spec = exact_spec((2, 1, 1)).with_u32_coordinates();
assert!(validate_static(spec, raw((1 << 31, 1, 1), (2, 1, 1), 0)).is_ok());
assert!(matches!(
validate_static(spec, raw(((1 << 31) + 1, 1, 1), (2, 1, 1), 0)),
Err(LaunchContractError::CoordinateRangeExceedsU32 {
axis: LaunchAxis::X,
positions: 4_294_967_298,
..
})
));
}
#[test]
fn validates_dynamic_shared_memory_exact_range_and_alignment() {
let exact = LaunchContractSpec::new(
KERNEL,
BlockRequirement::Exact((32, 1, 1)),
DynamicSharedMemoryRequirement::Exact {
bytes: 1024,
min_alignment: 16,
},
);
assert!(validate_static(exact, raw((1, 1, 1), (32, 1, 1), 1024)).is_ok());
assert!(matches!(
validate_static(exact, raw((1, 1, 1), (32, 1, 1), 512)),
Err(LaunchContractError::DynamicSharedMemoryExactMismatch { .. })
));
let range = LaunchContractSpec::new(
KERNEL,
BlockRequirement::Exact((32, 1, 1)),
DynamicSharedMemoryRequirement::Range {
min_bytes: 512,
max_bytes: 2048,
min_alignment: 32,
},
);
assert!(validate_static(range, raw((1, 1, 1), (32, 1, 1), 512)).is_ok());
assert!(validate_static(range, raw((1, 1, 1), (32, 1, 1), 2048)).is_ok());
assert!(matches!(
validate_static(range, raw((1, 1, 1), (32, 1, 1), 256)),
Err(LaunchContractError::DynamicSharedMemoryOutsideRange { .. })
));
let reversed = LaunchContractSpec::new(
KERNEL,
BlockRequirement::Exact((32, 1, 1)),
DynamicSharedMemoryRequirement::Range {
min_bytes: 2,
max_bytes: 1,
min_alignment: 1,
},
);
assert!(matches!(
validate_static(reversed, raw((1, 1, 1), (32, 1, 1), 1)),
Err(LaunchContractError::InvalidSharedMemoryRange { .. })
));
let bad_alignment = LaunchContractSpec::new(
KERNEL,
BlockRequirement::Exact((32, 1, 1)),
DynamicSharedMemoryRequirement::Exact {
bytes: 0,
min_alignment: 3,
},
);
assert!(matches!(
validate_static(bad_alignment, raw((1, 1, 1), (32, 1, 1), 0)),
Err(LaunchContractError::InvalidSharedMemoryAlignment { alignment: 3, .. })
));
}
#[test]
fn cluster_dimensions_must_be_nonzero_and_divide_grid() {
let spec = exact_spec((32, 1, 1)).with_cluster((2, 2, 1));
assert!(validate_static(spec, raw((8, 4, 1), (32, 1, 1), 0)).is_ok());
assert!(matches!(
validate_static(spec, raw((7, 4, 1), (32, 1, 1), 0)),
Err(LaunchContractError::ClusterDoesNotDivideGrid {
axis: LaunchAxis::X,
grid: 7,
cluster: 2,
..
})
));
let zero = exact_spec((32, 1, 1)).with_cluster((2, 0, 1));
assert!(matches!(
validate_static(zero, raw((8, 4, 1), (32, 1, 1), 0)),
Err(LaunchContractError::ZeroDimension {
dimension: LaunchDimension::Cluster,
axis: LaunchAxis::Y,
..
})
));
}
#[test]
fn validates_device_and_function_block_limits() {
let spec = LaunchContractSpec::new(
KERNEL,
BlockRequirement::MaxThreads(2048),
DynamicSharedMemoryRequirement::Exact {
bytes: 0,
min_alignment: 1,
},
);
let limits = generous_limits();
assert!(matches!(
validate_live_shape(spec, raw((1, 1, 1), (1025, 1, 1), 0), limits, 1024),
Err(LaunchContractError::DeviceDimensionExceeded {
dimension: LaunchDimension::Block,
axis: LaunchAxis::X,
..
})
));
assert!(matches!(
validate_live_shape(spec, raw((1, 1, 1), (33, 33, 1), 0), limits, 2048),
Err(LaunchContractError::DeviceThreadsPerBlockExceeded { actual: 1089, .. })
));
assert!(matches!(
validate_live_shape(spec, raw((1, 1, 1), (32, 16, 1), 0), limits, 256),
Err(LaunchContractError::FunctionThreadsPerBlockExceeded {
actual: 512,
max: 256,
..
})
));
assert!(matches!(
validate_live_shape(
spec,
raw((u32::MAX, 65_536, 1), (32, 1, 1), 0),
limits,
1024,
),
Err(LaunchContractError::DeviceDimensionExceeded {
dimension: LaunchDimension::Grid,
axis: LaunchAxis::Y,
..
})
));
}
#[test]
fn validates_static_plus_dynamic_shared_resources() {
let total = shared_memory_total(KERNEL, 16 * 1024, 40 * 1024).unwrap();
assert!(matches!(
validate_shared_memory_limit(KERNEL, total, 48 * 1024, false),
Err(LaunchContractError::DeviceSharedMemoryExceeded {
total: 57_344,
max: 49_152,
opt_in: false,
..
})
));
assert!(validate_shared_memory_limit(KERNEL, total, 96 * 1024, true).is_ok());
assert!(matches!(
validate_shared_memory_limit(KERNEL, 100 * 1024, 96 * 1024, true),
Err(LaunchContractError::DeviceSharedMemoryExceeded { opt_in: true, .. })
));
assert!(matches!(
shared_memory_total(KERNEL, u32::MAX, 1),
Err(LaunchContractError::SharedMemoryTotalOverflow { .. })
));
assert_eq!(
dynamic_shared_memory_max(DynamicSharedMemoryRequirement::Range {
min_bytes: 1024,
max_bytes: 8192,
min_alignment: 16,
}),
8192
);
}
#[test]
fn shared_memory_range_requires_support_for_its_advertised_maximum() {
let requirement = DynamicSharedMemoryRequirement::Range {
min_bytes: 1024,
max_bytes: 96 * 1024,
min_alignment: 16,
};
let spec =
LaunchContractSpec::new(KERNEL, BlockRequirement::Exact((32, 1, 1)), requirement);
assert!(validate_static(spec, raw((1, 1, 1), (32, 1, 1), 32 * 1024)).is_ok());
let total = shared_memory_total(KERNEL, 0, dynamic_shared_memory_max(requirement)).unwrap();
assert!(matches!(
validate_shared_memory_limit(KERNEL, total, 64 * 1024, true),
Err(LaunchContractError::DeviceSharedMemoryExceeded {
total: 98_304,
max: 65_536,
opt_in: true,
..
})
));
}
#[test]
fn validates_architecture_and_optional_capabilities() {
assert!(validate_compute_capability(KERNEL, (9, 0), (10, 0)).is_ok());
assert!(matches!(
validate_compute_capability(KERNEL, (9, 0), (8, 9)),
Err(LaunchContractError::ComputeCapabilityTooLow { .. })
));
assert!(matches!(
validate_cluster_support(KERNEL, false),
Err(LaunchContractError::ClusterLaunchUnsupported { .. })
));
assert!(validate_cluster_size(KERNEL, 8, 8).is_ok());
assert!(matches!(
validate_cluster_size(KERNEL, 16, 8),
Err(LaunchContractError::ClusterSizeExceeded {
blocks: 16,
max: 8,
..
})
));
assert!(validate_required_cluster(KERNEL, (2, 1, 1), Some((2, 1, 1))).is_ok());
assert!(matches!(
validate_required_cluster(KERNEL, (2, 1, 1), Some((4, 1, 1))),
Err(LaunchContractError::FunctionClusterShapeMismatch { .. })
));
assert!(matches!(
validate_required_cluster(KERNEL, (2, 1, 1), None),
Err(LaunchContractError::RequiredClusterDimensionsMissing { .. })
));
assert!(validate_cluster_residency(KERNEL, (2, 1, 1), 1).is_ok());
assert!(matches!(
validate_cluster_residency(KERNEL, (2, 1, 1), 0),
Err(LaunchContractError::ClusterHasNoResidency { .. })
));
assert!(matches!(
validate_cooperative_support(KERNEL, false),
Err(LaunchContractError::CooperativeLaunchUnsupported { .. })
));
}
#[test]
fn validates_cooperative_residency_capacity() {
assert!(validate_cooperative_residency(KERNEL, 80, 80).is_ok());
assert!(matches!(
validate_cooperative_residency(KERNEL, 81, 80),
Err(LaunchContractError::CooperativeGridTooLarge {
blocks: 81,
resident_capacity: 80,
..
})
));
}
#[test]
fn clustered_cooperative_residency_uses_active_cluster_capacity() {
let cluster_blocks = 2u64;
let active_clusters = 4u64;
let resident_capacity = active_clusters * cluster_blocks;
assert!(validate_cooperative_residency(KERNEL, 8, resident_capacity).is_ok());
assert!(matches!(
validate_cooperative_residency(KERNEL, 10, resident_capacity),
Err(LaunchContractError::CooperativeGridTooLarge {
blocks: 10,
resident_capacity: 8,
..
})
));
}
#[test]
fn combined_cluster_cooperative_spec_is_expressible() {
let spec = exact_spec((128, 1, 1))
.with_cluster((2, 1, 1))
.with_cooperative();
assert_eq!(spec.cluster(), Some((2, 1, 1)));
assert!(spec.cooperative());
assert!(validate_static(spec, raw((4, 1, 1), (128, 1, 1), 0)).is_ok());
assert!(matches!(
validate_static(spec, raw((3, 1, 1), (128, 1, 1), 0)),
Err(LaunchContractError::ClusterDoesNotDivideGrid { .. })
));
}
#[test]
fn spec_builders_preserve_diagnostic_metadata() {
let spec = exact_spec((32, 1, 1))
.with_cluster((2, 1, 1))
.with_cooperative()
.with_min_compute_capability(9, 0)
.with_u32_coordinates();
assert_eq!(spec.kernel_name(), KERNEL);
assert_eq!(spec.block(), BlockRequirement::Exact((32, 1, 1)));
assert_eq!(spec.cluster(), Some((2, 1, 1)));
assert!(spec.cooperative());
assert_eq!(spec.min_compute_capability(), Some((9, 0)));
assert_eq!(spec.coordinates(), CoordinateRequirement::U32);
}
}