use anyhow::Result;
use cuda_async::error::DeviceError;
use cuda_core::DType;
use cuda_core::{memcpy_dtoh_async, Function};
use cutile_compiler::ast::Module;
use cutile_compiler::compile_api::KernelCompiler;
use cutile_compiler::compiler::{CUDATileFunctionCompiler, CUDATileModules};
use cutile_compiler::cuda_tile_runtime_utils::{
compile_bytecode_cached, env_flag_enabled, get_compiler_version, get_gpu_name,
recompile_after_disk_rejection, serialize_tile_ir_bytecode, tileiras_fingerprint, Stage2Source,
DEFAULT_OPT_LEVEL,
};
use cutile_compiler::specialization::{DivHint, SpecializationBits};
use dashmap::DashMap;
use once_cell::sync::OnceCell;
use std::alloc::{alloc, Layout};
use std::fs;
use std::future::IntoFuture;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
fn jit_log_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| env_flag_enabled("CUTILE_JIT_LOG"))
}
macro_rules! jit_log {
($($arg:tt)*) => {
if jit_log_enabled() {
eprintln!("[cutile::jit] {}", format!($($arg)*));
}
};
}
static JIT_COMPILE_COUNT: AtomicU64 = AtomicU64::new(0);
pub fn jit_compile_count() -> u64 {
JIT_COMPILE_COUNT.load(Ordering::Relaxed)
}
#[inline]
fn record_jit_compile() {
JIT_COMPILE_COUNT.fetch_add(1, Ordering::Relaxed);
}
use crate::error::*;
use crate::tensor::{GridBound, IntoPartition, IntoPartitionArc, Partition, Tensor};
pub use cuda_async::{
device_buffer::*, device_context::*, device_future::*, device_operation::*, launch::*,
predicate::*, scheduling_policies::*,
};
pub use cutile_compiler::compiler::utils::CompileOptions;
pub type ModuleAstFn = fn() -> Module;
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct TileFunctionKey {
module_name: String,
function_name: String,
pub function_generics: Vec<String>,
pub stride_args: Vec<(String, Vec<i32>)>,
pub spec_args: Vec<(String, SpecializationBits)>,
pub scalar_hints: Vec<(String, DivHint)>,
pub grid: Option<(u32, u32, u32)>,
pub compile_options: CompileOptions,
source_hash: String,
device_id: usize,
gpu_name: String,
compiler_version: String,
tileiras_fingerprint: String,
}
pub struct TileFunctionKeyBuilder {
module_name: String,
function_name: String,
function_generics: Vec<String>,
stride_args: Vec<(String, Vec<i32>)>,
spec_args: Vec<(String, SpecializationBits)>,
scalar_hints: Vec<(String, DivHint)>,
grid: Option<(u32, u32, u32)>,
compile_options: CompileOptions,
source_hash: String,
device_id: usize,
gpu_name: String,
compiler_version: String,
tileiras_fingerprint: String,
}
impl TileFunctionKeyBuilder {
pub fn generics(mut self, generics: Vec<String>) -> Self {
self.function_generics = generics;
self
}
pub fn stride_args(mut self, stride_args: Vec<(String, Vec<i32>)>) -> Self {
self.stride_args = stride_args;
self
}
pub fn spec_args(mut self, spec_args: Vec<(String, SpecializationBits)>) -> Self {
self.spec_args = spec_args;
self
}
pub fn scalar_hints(mut self, scalar_hints: Vec<(String, DivHint)>) -> Self {
self.scalar_hints = scalar_hints;
self
}
pub fn grid(mut self, grid: (u32, u32, u32)) -> Self {
self.grid = Some(grid);
self
}
pub fn compile_options(mut self, options: CompileOptions) -> Self {
self.compile_options = options;
self
}
pub fn source_hash(mut self, hash: impl Into<String>) -> Self {
self.source_hash = hash.into();
self
}
pub fn device_id(mut self, device_id: usize) -> Self {
self.device_id = device_id;
self
}
pub fn gpu_name(mut self, name: impl Into<String>) -> Self {
self.gpu_name = name.into();
self
}
pub fn compiler_version(mut self, version: impl Into<String>) -> Self {
self.compiler_version = version.into();
self
}
pub fn tileiras_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
self.tileiras_fingerprint = fingerprint.into();
self
}
pub fn build(self) -> TileFunctionKey {
TileFunctionKey {
module_name: self.module_name,
function_name: self.function_name,
function_generics: self.function_generics,
stride_args: self.stride_args,
spec_args: self.spec_args,
scalar_hints: self.scalar_hints,
grid: self.grid,
compile_options: self.compile_options,
source_hash: self.source_hash,
device_id: self.device_id,
gpu_name: self.gpu_name,
compiler_version: self.compiler_version,
tileiras_fingerprint: self.tileiras_fingerprint,
}
}
}
impl TileFunctionKey {
pub fn builder(
module_name: impl Into<String>,
function_name: impl Into<String>,
) -> TileFunctionKeyBuilder {
TileFunctionKeyBuilder {
module_name: module_name.into(),
function_name: function_name.into(),
function_generics: vec![],
stride_args: vec![],
spec_args: vec![],
scalar_hints: vec![],
grid: None,
compile_options: CompileOptions::default(),
source_hash: String::new(),
device_id: 0,
gpu_name: String::new(),
compiler_version: String::new(),
tileiras_fingerprint: String::new(),
}
}
}
impl FunctionKey for TileFunctionKey {}
pub struct Specialization<F: Fn() -> Module> {
module_ast_fn: F,
key: TileFunctionKey,
}
impl<F: Fn() -> Module> Specialization<F> {
pub fn l1_cache_key(&self) -> &TileFunctionKey {
&self.key
}
pub fn into_l1_cache_key(self) -> TileFunctionKey {
self.key
}
pub fn l2_cache_key(&self) -> std::result::Result<String, cutile_compiler::error::JITError> {
let stride_refs: Vec<(&str, &[i32])> = self
.key
.stride_args
.iter()
.map(|(name, strides)| (name.as_str(), strides.as_slice()))
.collect();
let spec_refs: Vec<(&str, SpecializationBits)> = self
.key
.spec_args
.iter()
.map(|(name, spec)| (name.as_str(), spec.clone()))
.collect();
let scalar_hint_refs: Vec<(&str, DivHint)> = self
.key
.scalar_hints
.iter()
.map(|(name, hint)| (name.as_str(), *hint))
.collect();
let mut compiler = KernelCompiler::new(
&self.module_ast_fn,
&self.key.module_name,
&self.key.function_name,
)
.target(&self.key.gpu_name)
.generics(self.key.function_generics.clone())
.strides(&stride_refs)
.spec_args(&spec_refs)
.scalar_hints(&scalar_hint_refs)
.options(self.key.compile_options.clone());
if let Some(grid) = self.key.grid {
compiler = compiler.grid(grid);
}
compiler.l2_cache_key()
}
}
#[doc(hidden)]
#[allow(clippy::too_many_arguments)]
pub fn _specialization_from_context<F: Fn() -> Module>(
ctx: &ExecutionContext,
module_ast_fn: F,
module_name: &str,
function_name: &str,
function_generics: Vec<String>,
stride_args: Vec<(String, Vec<i32>)>,
spec_args: Vec<(String, SpecializationBits)>,
scalar_hints: Vec<(String, DivHint)>,
const_grid: Option<(u32, u32, u32)>,
compile_options: CompileOptions,
source_hash: &str,
) -> Specialization<F> {
let device_id = ctx.get_device_id();
let gpu_name = get_gpu_name(device_id);
let mut key_builder = TileFunctionKey::builder(module_name, function_name)
.generics(function_generics)
.stride_args(stride_args)
.spec_args(spec_args)
.scalar_hints(scalar_hints)
.compile_options(compile_options)
.source_hash(source_hash)
.device_id(device_id)
.gpu_name(gpu_name)
.compiler_version(get_compiler_version())
.tileiras_fingerprint(tileiras_fingerprint());
if let Some(grid) = const_grid {
key_builder = key_builder.grid(grid);
}
Specialization {
module_ast_fn,
key: key_builder.build(),
}
}
static KERNEL_CACHE: OnceLock<DashMap<TileFunctionKey, Arc<OnceCell<CompiledKernel>>>> =
OnceLock::new();
pub fn get_kernel_cache() -> &'static DashMap<TileFunctionKey, Arc<OnceCell<CompiledKernel>>> {
KERNEL_CACHE.get_or_init(DashMap::new)
}
pub fn kernel_cache_slot(key: &TileFunctionKey) -> Arc<OnceCell<CompiledKernel>> {
let cache = get_kernel_cache();
if let Some(existing) = cache.get(key) {
return Arc::clone(existing.value());
}
Arc::clone(
cache
.entry(key.clone())
.or_insert_with(|| Arc::new(OnceCell::new()))
.value(),
)
}
pub fn contains_cuda_function(key: &TileFunctionKey) -> bool {
get_kernel_cache()
.get(key)
.is_some_and(|slot| slot.value().get().is_some())
}
#[expect(unused)]
fn read_ir(path: String) -> Result<String, std::io::Error> {
let s = String::from_utf8(fs::read(path)?).expect("Unable to convert from utf8 to string.");
Ok(s)
}
fn write_ir(
module_name: &str,
function_name: &str,
cache_hash_str: &str,
extension: &str,
dir: &str,
contents: &str,
) {
let filename = format!("{module_name}_{function_name}_{cache_hash_str}.{extension}");
let path = PathBuf::from(dir).join(filename);
fs::write(path.clone(), contents).unwrap_or_else(|_| panic!("Failed to write {path:?}")); println!("IR written to {path:?}");
}
#[allow(clippy::too_many_arguments)]
fn compile_and_load_kernel(
modules: &CUDATileModules,
module_name: &str,
function_name: &str,
function_entry: &str,
generics: &[String],
stride_args: &[(String, Vec<i32>)],
spec_args: &[(String, SpecializationBits)],
scalar_hints: &[(String, DivHint)],
const_grid: Option<(u32, u32, u32)>,
gpu_name: &str,
compile_options: &CompileOptions,
device_id: usize,
key_str: &str,
) -> Result<CompiledKernel, Error> {
let t0 = std::time::Instant::now();
let stride_args_refs: Vec<(&str, &[i32])> = stride_args
.iter()
.map(|x| (x.0.as_str(), x.1.as_slice()))
.collect();
let spec_args_refs: Vec<(&str, &SpecializationBits)> =
spec_args.iter().map(|x| (x.0.as_str(), &x.1)).collect();
let scalar_hints_refs: Vec<(&str, &DivHint)> =
scalar_hints.iter().map(|x| (x.0.as_str(), &x.1)).collect();
let stage1_start = std::time::Instant::now();
let (tile_module, validator, check_stats) = {
let compiler = CUDATileFunctionCompiler::new(
modules,
module_name,
function_name,
generics,
&stride_args_refs,
&spec_args_refs,
&scalar_hints_refs,
const_grid,
gpu_name.to_string(),
compile_options,
)?;
let tile_module = compiler.compile()?;
let validator = Arc::new(compiler.get_validator());
let check_stats = (
compiler.check_stats.discharged.get(),
compiler.check_stats.hoisted.get(),
compiler.check_stats.in_place.get(),
);
(tile_module, validator, check_stats)
};
let stage1_ms = stage1_start.elapsed().as_secs_f64() * 1000.0;
let stage2_start = std::time::Instant::now();
{
let print_ir =
modules.get_entry_arg_bool_by_function_name(module_name, function_name, "print_ir")?;
let dump_mlir_dir = modules.get_entry_arg_string_by_function_name(
module_name,
function_name,
"dump_mlir_dir",
)?;
if print_ir || dump_mlir_dir.is_some() {
let ir_text = tile_module.to_mlir_text();
if print_ir {
println!("COMPILED IR: {module_name}::{function_name}\n{ir_text}");
}
if let Some(path) = dump_mlir_dir {
write_ir(
module_name,
function_name,
key_str,
"mlir",
path.as_str(),
ir_text.as_str(),
);
}
}
}
let (bytecode, bc_version) = serialize_tile_ir_bytecode(&tile_module)?;
let (cubin, mut stage2_source) =
compile_bytecode_cached(&bytecode, bc_version, gpu_name, DEFAULT_OPT_LEVEL)?;
let mut stage2_ms = stage2_start.elapsed().as_secs_f64() * 1000.0;
let mut recompile_ms = 0.0;
let stage3_start = std::time::Instant::now();
let module = match load_module_from_bytes(&cubin, device_id) {
Ok(module) => module,
Err(e) => match std::mem::replace(&mut stage2_source, Stage2Source::Tileiras) {
Stage2Source::DiskCache { store, key } => {
jit_log!(
"{module_name}::{function_name} → cached cubin rejected by the driver ({e}); \
evicting and recompiling"
);
let recompile_start = std::time::Instant::now();
let cubin = recompile_after_disk_rejection(
store.as_ref(),
&key,
&bytecode,
gpu_name,
DEFAULT_OPT_LEVEL,
)?;
recompile_ms = recompile_start.elapsed().as_secs_f64() * 1000.0;
stage2_ms += recompile_ms;
load_module_from_bytes(&cubin, device_id)?
}
Stage2Source::Tileiras => return Err(e.into()),
},
};
let function = Arc::new(module.load_function(function_entry).map_err(|e| {
Error::KernelLaunch(KernelLaunchError(format!(
"failed to load '{function_entry}' from compiled cubin: {e}"
)))
})?);
let stage3_ms = (stage3_start.elapsed().as_secs_f64() * 1000.0 - recompile_ms).max(0.0);
jit_log!(
"{module_name}::{function_name} → JIT compiled in {:.1?}",
t0.elapsed()
);
if std::env::var_os("CUTILE_JIT_TIMING").is_some() {
let stage2_source = match stage2_source {
Stage2Source::Tileiras => "tileiras",
Stage2Source::DiskCache { .. } => "disk",
};
eprintln!(
"CUTILE_JIT_TIMING module={module_name} function={function_name} key={key_str} stage1_ms={stage1_ms:.3} stage2_ms={stage2_ms:.3} stage2_source={stage2_source} stage3_ms={stage3_ms:.3} checks_discharged={} checks_hoisted={} checks_in_place={} generics={}",
check_stats.0,
check_stats.1,
check_stats.2,
generics.join(","),
);
}
Ok(CompiledKernel {
module,
function,
validator,
})
}
#[allow(clippy::too_many_arguments)]
pub fn compile_from_context<F: Fn() -> Module>(
ctx: &ExecutionContext,
kernel_ast: F,
module_name: &str,
function_name: &str,
function_entry: &str,
function_generics: Vec<String>,
stride_args: Vec<(String, Vec<i32>)>,
spec_args: Vec<(String, SpecializationBits)>,
scalar_hints: Vec<(String, DivHint)>,
const_grid: Option<(u32, u32, u32)>,
compile_options: CompileOptions,
source_hash: &str,
) -> Result<(Arc<Function>, Arc<Validator>), Error> {
let specialization = _specialization_from_context(
ctx,
kernel_ast,
module_name,
function_name,
function_generics,
stride_args,
spec_args,
scalar_hints,
const_grid,
compile_options,
source_hash,
);
let key = specialization.l1_cache_key().clone();
let device_id = key.device_id;
let gpu_name = key.gpu_name.clone();
let slot = kernel_cache_slot(&key);
let compiled = match slot.get_or_try_init(|| -> Result<CompiledKernel, Error> {
jit_log!("{module_name}::{function_name} → JIT compiling...");
let modules = CUDATileModules::from_kernel((specialization.module_ast_fn)())?;
let kernel = compile_and_load_kernel(
&modules,
module_name,
function_name,
function_entry,
&key.function_generics,
&key.stride_args,
&key.spec_args,
&key.scalar_hints,
const_grid,
&gpu_name,
&key.compile_options,
device_id,
&key.display_hash(),
)?;
record_jit_compile();
Ok(kernel)
}) {
Ok(compiled) => compiled,
Err(e) => {
drop(slot);
get_kernel_cache().remove_if(&key, |_, cell| {
cell.get().is_none() && Arc::strong_count(cell) == 1
});
return Err(e);
}
};
Ok((
Arc::clone(&compiled.function),
Arc::clone(&compiled.validator),
))
}
pub fn validate_grids(
grid: (u32, u32, u32),
partition_grids: &[(u32, u32, u32)],
) -> Result<(), Error> {
if let Some(partition_grid) = partition_grids.iter().find(|&&i| i != grid) {
Err(Error::KernelLaunch(KernelLaunchError(format!(
"{:?} != {:?}",
grid, partition_grid
))))
} else {
Ok(())
}
}
pub fn validate_grid_bounds(grid: (u32, u32, u32), bounds: &[GridBound]) -> Result<(), Error> {
for bound in bounds {
let GridBound::Exact(expected) = bound else {
continue;
};
if *expected != grid {
return Err(Error::KernelLaunch(KernelLaunchError(format!(
"launch grid {:?} does not match the inferred partition grid {:?}",
grid, expected
))));
}
}
for bound in bounds {
let GridBound::AtMost(max) = bound else {
continue;
};
let launch = [grid.0, grid.1, grid.2];
let max_axes = [max.0, max.1, max.2];
if let Some(axis) = (0..3).find(|&k| launch[k] > max_axes[k]) {
return Err(Error::KernelLaunch(KernelLaunchError(format!(
"launch grid {:?} exceeds the partial-coverage partition grid {:?} on axis {axis}",
grid, max
))));
}
}
Ok(())
}
pub fn validate_launch(
launch_checks: &[LaunchCheck],
grid: (u32, u32, u32),
partition_bounds: &[GridBound],
param_shapes: &[Vec<i32>],
view_shapes: &[Vec<i32>],
) -> Result<(), Error> {
validate_grid_bounds(grid, partition_bounds)?;
for check in launch_checks {
evaluate_launch_check(check, param_shapes, view_shapes, grid)?;
}
Ok(())
}
pub fn validate_launch_checks(
launch_checks: &[LaunchCheck],
param_shapes: &[Vec<i32>],
view_shapes: &[Vec<i32>],
launch_grid: (u32, u32, u32),
) -> Result<(), Error> {
for check in launch_checks {
evaluate_launch_check(check, param_shapes, view_shapes, launch_grid)?;
}
Ok(())
}
fn evaluate_launch_check(
check: &LaunchCheck,
param_shapes: &[Vec<i32>],
view_shapes: &[Vec<i32>],
launch_grid: (u32, u32, u32),
) -> Result<(), Error> {
let resolve_atom = |atom: &Atom| -> Option<i64> {
match atom {
Atom::Dim { param, axis } => param_shapes
.get(*param)
.and_then(|shape| shape.get(*axis))
.map(|&extent| extent as i64),
Atom::ViewExtent { param, axis } => view_shapes
.get(*param)
.and_then(|shape| shape.get(*axis))
.map(|&extent| extent as i64),
Atom::TileCount { param, axis, tile } => {
if *tile < 1 {
return None;
}
param_shapes
.get(*param)
.and_then(|shape| shape.get(*axis))
.map(|&extent| (extent as i64 + *tile as i64 - 1) / *tile as i64)
}
Atom::NumTileBlocks(k) => match k {
0 => Some(launch_grid.0 as i64),
1 => Some(launch_grid.1 as i64),
2 => Some(launch_grid.2 as i64),
_ => None,
},
Atom::Iv(_) | Atom::TileBlockId(_) => None,
}
};
match check.predicate.eval(&resolve_atom) {
Some(true) => Ok(()),
Some(false) => Err(Error::KernelLaunch(KernelLaunchError(format!(
"launch check failed: {}",
check.cause
)))),
None => Err(Error::KernelLaunch(KernelLaunchError(format!(
"launch check has unresolved operands (extent unavailable at launch): {}",
check.cause
)))),
}
}
pub fn infer_launch_grid(
grid: (u32, u32, u32),
bounds: &[GridBound],
) -> Result<(u32, u32, u32), Error> {
let exact: Vec<(u32, u32, u32)> = bounds
.iter()
.filter_map(|b| match b {
GridBound::Exact(g) => Some(*g),
GridBound::AtMost(_) => None,
})
.collect();
if grid != (0, 0, 0) {
validate_grid_bounds(grid, bounds)?;
return Ok(grid);
}
if exact.is_empty() {
if bounds.is_empty() {
return kernel_launch_error_result("Launch grid required.");
}
return kernel_launch_error_result(
"Launch grid required: a partial-coverage (partition_prefix) binding \
only bounds the grid; specify the grid explicitly or bind with \
partition().",
);
}
let grid = exact[0];
validate_grid_bounds(grid, bounds)?;
Ok(grid)
}
pub trait TileKernel<ARGS: Send, DI, STORED: Send = ARGS>: DeviceOp<Output = ARGS>
where
DI: DeviceOp<Output = STORED>,
{
#[allow(clippy::too_many_arguments)]
fn jit_compile<F: Fn() -> Module>(
&mut self,
ctx: &ExecutionContext,
kernel_ast: F,
module_name: &str,
function_name: &str,
function_entry: &str,
function_generics: Vec<String>,
stride_args: Vec<(String, Vec<i32>)>,
spec_args: Vec<(String, SpecializationBits)>,
scalar_hints: Vec<(String, DivHint)>,
grid: Option<(u32, u32, u32)>,
compile_options: CompileOptions,
source_hash: &str,
) -> Result<(Arc<Function>, Arc<Validator>), Error> {
compile_from_context(
ctx,
kernel_ast,
module_name,
function_name,
function_entry,
function_generics,
stride_args,
spec_args,
scalar_hints,
grid,
compile_options,
source_hash,
)
}
fn generics(self, generics: Vec<String>) -> Self;
fn const_grid(self, grid: (u32, u32, u32)) -> Self;
fn grid(self, grid: (u32, u32, u32)) -> Self;
fn compile_options(self, options: CompileOptions) -> Self;
fn infer_launch_grid(&self, bounds: &[GridBound]) -> Result<(u32, u32, u32), Error> {
let grid = self.get_launch_grid();
infer_launch_grid(grid, bounds)
}
fn get_launch_grid(&self) -> (u32, u32, u32);
fn get_launch_smem(&self) -> u32 {
0
}
fn get_launch_block(&self) -> (u32, u32, u32) {
(1, 1, 1)
}
}
impl<T: DType> ArcKernelArgument for Tensor<T> {
fn push_arg_arc(self: &Arc<Self>, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.cu_deviceptr());
}
for dim in self.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.strides.iter() {
launcher.push_arg(*stride);
}
}
}
impl<T: DType> KernelArgument for &Partition<Tensor<T>> {
fn push_arg(self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.object.cu_deviceptr());
}
for dim in self.object.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.object.strides.iter() {
launcher.push_arg(*stride);
}
for dim in self.partition_shape.iter() {
launcher.push_arg(*dim as i32);
}
for stride in self.partition_strides.iter() {
launcher.push_arg(*stride as i32);
}
}
}
impl<'a, T: DType> KernelArgument for &Partition<&'a mut Tensor<T>> {
fn push_arg(self, launcher: &mut AsyncKernelLaunch) {
unsafe {
launcher.push_device_ptr(self.object.cu_deviceptr());
}
for dim in self.object.shape.iter() {
launcher.push_arg(*dim);
}
for stride in self.object.strides.iter() {
launcher.push_arg(*stride);
}
for dim in self.partition_shape.iter() {
launcher.push_arg(*dim as i32);
}
for stride in self.partition_strides.iter() {
launcher.push_arg(*stride as i32);
}
}
}
pub trait PartitionOp<I, DI>
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I>,
{
fn partition<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> DeviceOperationPartition<RANK, I, DI>;
}
impl<I, DI> PartitionOp<I, DI> for DI
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I>,
{
fn partition<const RANK: usize>(
self,
partition_shape: [usize; RANK],
) -> DeviceOperationPartition<RANK, I, DI>
where
Self: Sized,
{
DeviceOperationPartition::<RANK, I, DI> {
partition_shape,
op: self,
}
}
}
pub struct DeviceOperationPartition<const RANK: usize, I, DI>
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I>,
{
partition_shape: [usize; RANK],
op: DI,
}
unsafe impl<const RANK: usize, I, DI> Send for DeviceOperationPartition<RANK, I, DI>
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I>,
{
}
impl<const RANK: usize, I, DI> DeviceOp for DeviceOperationPartition<RANK, I, DI>
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I>,
{
type Output = Partition<I>;
unsafe fn execute(
self,
context: &ExecutionContext,
) -> Result<<Self as DeviceOp>::Output, DeviceError> {
let val = self.op.execute(context)?;
Ok(val.partition(self.partition_shape))
}
}
impl<const RANK: usize, I, DI> IntoFuture for DeviceOperationPartition<RANK, I, DI>
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I>,
{
type Output = Result<Partition<I>, DeviceError>;
type IntoFuture = DeviceFuture<Partition<I>, DeviceOperationPartition<RANK, I, DI>>;
fn into_future(self) -> Self::IntoFuture {
match with_default_device_policy(|policy| {
let stream = policy.next_stream()?;
Ok(DeviceFuture::scheduled(self, ExecutionContext::new(stream)))
}) {
Ok(Ok(future)) => future,
Ok(Err(e)) => DeviceFuture::failed(e),
Err(e) => DeviceFuture::failed(e),
}
}
}
pub struct UnwrapPartition<I: Send, DI>
where
DI: DeviceOp<Output = Partition<I>>,
{
pub(crate) op: DI,
}
unsafe impl<I: Send, DI> Send for UnwrapPartition<I, DI> where DI: DeviceOp<Output = Partition<I>> {}
impl<I: Send, DI> DeviceOp for UnwrapPartition<I, DI>
where
DI: DeviceOp<Output = Partition<I>>,
{
type Output = I;
unsafe fn execute(
self,
context: &ExecutionContext,
) -> Result<<Self as DeviceOp>::Output, DeviceError> {
let val = self.op.execute(context)?;
Ok(val.unpartition())
}
}
impl<I: Send, DI> IntoFuture for UnwrapPartition<I, DI>
where
DI: DeviceOp<Output = Partition<I>>,
{
type Output = Result<I, DeviceError>;
type IntoFuture = DeviceFuture<I, UnwrapPartition<I, DI>>;
fn into_future(self) -> Self::IntoFuture {
match with_default_device_policy(|policy| {
let stream = policy.next_stream()?;
Ok(DeviceFuture::scheduled(self, ExecutionContext::new(stream)))
}) {
Ok(Ok(future)) => future,
Ok(Err(e)) => DeviceFuture::failed(e),
Err(e) => DeviceFuture::failed(e),
}
}
}
pub fn unwrap_partition<I: Send, DI>(op: DI) -> UnwrapPartition<I, DI>
where
DI: DeviceOp<Output = Partition<I>>,
{
UnwrapPartition { op }
}
pub struct TensorToHostVec<T: DType, DI>
where
DI: DeviceOp<Output = Tensor<T>>,
{
pub(crate) op: DI,
}
unsafe impl<T: DType, DI> Send for TensorToHostVec<T, DI> where DI: DeviceOp<Output = Tensor<T>> {}
impl<T: DType, DI> DeviceOp for TensorToHostVec<T, DI>
where
DI: DeviceOp<Output = Tensor<T>>,
{
type Output = Vec<T>;
unsafe fn execute(
self,
context: &ExecutionContext,
) -> Result<<Self as DeviceOp>::Output, DeviceError> {
let tensor = self.op.execute(context)?;
let cu_deviceptr = tensor.cu_deviceptr();
let size = tensor.size();
let layout = Layout::array::<T>(size).expect("overflow cannot happen");
let async_ptr = unsafe { alloc(layout).cast::<T>() };
memcpy_dtoh_async(async_ptr, cu_deviceptr, size, context.get_cuda_stream());
Ok(unsafe { Vec::from_raw_parts(async_ptr, size, size) })
}
}
impl<T: DType, DI> IntoFuture for TensorToHostVec<T, DI>
where
DI: DeviceOp<Output = Tensor<T>>,
{
type Output = Result<Vec<T>, DeviceError>;
type IntoFuture = DeviceFuture<Vec<T>, TensorToHostVec<T, DI>>;
fn into_future(self) -> Self::IntoFuture {
match with_default_device_policy(|policy| {
let stream = policy.next_stream()?;
Ok(DeviceFuture::scheduled(self, ExecutionContext::new(stream)))
}) {
Ok(Ok(future)) => future,
Ok(Err(e)) => DeviceFuture::failed(e),
Err(e) => DeviceFuture::failed(e),
}
}
}
pub trait ToHostVecOp<T: DType> {
fn to_host_vec(self) -> impl DeviceOp<Output = Vec<T>>
where
Self: DeviceOp<Output = Tensor<T>>,
{
TensorToHostVec { op: self }
}
}
impl<T: DType, DI> ToHostVecOp<T> for DI where DI: DeviceOp<Output = Tensor<T>> {}
#[cfg(test)]
mod launch_check_tests {
use super::*;
fn nonzero(param: usize, axis: usize) -> LaunchCheck {
LaunchCheck {
predicate: Predicate::nonzero(Term::atom(Atom::Dim { param, axis })),
cause: "extent > 0".to_string(),
}
}
fn view_nonzero(param: usize, axis: usize) -> LaunchCheck {
LaunchCheck {
predicate: Predicate::nonzero(Term::atom(Atom::ViewExtent { param, axis })),
cause: "view extent > 0".to_string(),
}
}
#[test]
fn empty_checks_run_only_the_grid_family() {
assert!(validate_launch(&[], (4, 1, 1), &[GridBound::Exact((4, 1, 1))], &[], &[]).is_ok());
}
#[test]
fn grid_family_still_rejects_mismatched_partition_grid() {
assert!(validate_launch(&[], (4, 1, 1), &[GridBound::Exact((2, 1, 1))], &[], &[]).is_err());
}
#[test]
fn dim_nonzero_passes_for_positive_extent() {
let shapes = vec![vec![128, 256]];
assert!(validate_launch(&[nonzero(0, 0)], (1, 1, 1), &[], &shapes, &[]).is_ok());
}
#[test]
fn dim_nonzero_rejects_zero_extent() {
let shapes = vec![vec![0, 256]];
assert!(validate_launch(&[nonzero(0, 0)], (1, 1, 1), &[], &shapes, &[]).is_err());
}
#[test]
fn dim_nonzero_fails_closed_on_missing_parameter() {
let shapes = vec![vec![128]];
assert!(validate_launch(&[nonzero(1, 0)], (1, 1, 1), &[], &shapes, &[]).is_err());
}
#[test]
fn each_atom_resolves_against_its_own_frame() {
let roots = vec![vec![256, 256]];
let views = vec![vec![0, 256]];
assert!(validate_launch(&[nonzero(0, 0)], (1, 1, 1), &[], &roots, &views).is_ok());
assert!(validate_launch(&[view_nonzero(0, 0)], (1, 1, 1), &[], &roots, &views).is_err());
}
#[test]
fn view_atoms_fail_closed_without_view_shapes() {
let roots = vec![vec![256, 256]];
assert!(validate_launch(&[view_nonzero(0, 0)], (1, 1, 1), &[], &roots, &[]).is_err());
}
#[test]
fn prefix_bound_admits_a_per_axis_prefix_and_nothing_more() {
use GridBound::{AtMost, Exact};
assert!(validate_grid_bounds((3, 2, 1), &[AtMost((3, 2, 1))]).is_ok());
assert!(validate_grid_bounds((2, 2, 1), &[AtMost((3, 2, 1))]).is_ok());
assert!(validate_grid_bounds((2, 1, 1), &[AtMost((3, 2, 1))]).is_ok());
assert!(validate_grid_bounds((4, 1, 1), &[AtMost((3, 2, 1))]).is_err());
assert!(validate_grid_bounds((1, 3, 1), &[AtMost((3, 2, 1))]).is_err());
assert!(validate_grid_bounds((6, 1, 1), &[AtMost((3, 2, 1))]).is_err());
assert!(validate_grid_bounds((2, 1, 1), &[Exact((3, 1, 1)), AtMost((3, 1, 1))]).is_err());
assert!(validate_grid_bounds((3, 1, 1), &[Exact((3, 1, 1)), AtMost((4, 1, 1))]).is_ok());
}
#[test]
fn prefix_bound_cannot_define_the_launch_grid() {
use GridBound::{AtMost, Exact};
let err = infer_launch_grid((0, 0, 0), &[AtMost((3, 1, 1))]).unwrap_err();
assert!(
err.to_string().contains("partial-coverage"),
"the error should say why inference refused: {err}"
);
assert_eq!(
infer_launch_grid((0, 0, 0), &[Exact((3, 1, 1)), AtMost((4, 1, 1))]).unwrap(),
(3, 1, 1)
);
assert!(infer_launch_grid((0, 0, 0), &[Exact((3, 1, 1)), AtMost((2, 1, 1))]).is_err());
assert_eq!(
infer_launch_grid((2, 1, 1), &[AtMost((3, 1, 1))]).unwrap(),
(2, 1, 1)
);
assert!(infer_launch_grid((4, 1, 1), &[AtMost((3, 1, 1))]).is_err());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unexpected_ast_provider() -> Module {
panic!("L1 cache-key access must not invoke the AST provider")
}
#[test]
fn l1_cache_key_does_not_invoke_ast_provider() {
let expected = TileFunctionKey::builder("module", "kernel").build();
let specialization = Specialization {
module_ast_fn: unexpected_ast_provider as ModuleAstFn,
key: expected.clone(),
};
assert_eq!(specialization.l1_cache_key(), &expected);
}
}