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,
toolchain_env_snapshot, Stage2Source, TileirasOptions, ToolchainEnvSnapshot,
};
use cutile_compiler::specialization::{DivHint, SpecializationBits};
use dashmap::DashMap;
use once_cell::sync::OnceCell;
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, KernelInput, KernelOutput, 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 module_name(&self) -> &str {
&self.module_name
}
pub fn function_name(&self) -> &str {
&self.function_name
}
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(),
}
}
pub struct LaunchSite {
inner: std::sync::RwLock<Option<std::sync::Arc<SiteResolution>>>,
}
pub struct SiteResolution {
epoch: u64,
device_id: usize,
#[allow(dead_code)]
toolchain: ToolchainEnvSnapshot,
generics: Vec<String>,
specs: Vec<SpecializationBits>,
scalar_hints: Vec<DivHint>,
const_grid: Option<(u32, u32, u32)>,
compile_options: CompileOptions,
function: Arc<Function>,
validator: Arc<Validator>,
}
impl SiteResolution {
#[allow(clippy::too_many_arguments)]
pub fn new(
device_id: usize,
generics: Vec<String>,
specs: Vec<SpecializationBits>,
scalar_hints: Vec<DivHint>,
const_grid: Option<(u32, u32, u32)>,
compile_options: CompileOptions,
function: Arc<Function>,
validator: Arc<Validator>,
) -> Self {
Self {
epoch: kernel_cache_epoch(),
device_id,
toolchain: toolchain_env_snapshot(),
generics,
specs,
scalar_hints,
const_grid,
compile_options,
function,
validator,
}
}
}
impl LaunchSite {
pub const fn new() -> Self {
Self {
inner: std::sync::RwLock::new(None),
}
}
pub fn get(
&self,
device_id: usize,
generics: &[String],
specs: &[&SpecializationBits],
scalar_hints: &[DivHint],
const_grid: Option<(u32, u32, u32)>,
compile_options: &CompileOptions,
) -> Option<(Arc<Function>, Arc<Validator>)> {
let guard = self.inner.read().ok()?;
let r = guard.as_ref()?;
if r.epoch == kernel_cache_epoch()
&& r.device_id == device_id
&& r.const_grid == const_grid
&& &r.compile_options == compile_options
&& r.generics.as_slice() == generics
&& r.specs.len() == specs.len()
&& r.specs.iter().zip(specs).all(|(a, b)| a == *b)
&& r.scalar_hints.as_slice() == scalar_hints
{
Some((Arc::clone(&r.function), Arc::clone(&r.validator)))
} else {
None
}
}
pub fn store(&self, resolution: SiteResolution) {
if let Ok(mut guard) = self.inner.write() {
*guard = Some(std::sync::Arc::new(resolution));
}
}
}
impl Default for LaunchSite {
fn default() -> Self {
Self::new()
}
}
static KERNEL_CACHE: OnceLock<DashMap<TileFunctionKey, Arc<OnceCell<CompiledKernel>>>> =
OnceLock::new();
pub(crate) fn get_kernel_cache() -> &'static DashMap<TileFunctionKey, Arc<OnceCell<CompiledKernel>>>
{
KERNEL_CACHE.get_or_init(DashMap::new)
}
#[doc(hidden)]
pub unsafe fn clear_kernel_cache_for_tests() {
get_kernel_cache().clear();
bump_kernel_cache_epoch();
}
static KERNEL_CACHE_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn kernel_cache_epoch() -> u64 {
KERNEL_CACHE_EPOCH.load(std::sync::atomic::Ordering::Acquire)
}
fn bump_kernel_cache_epoch() {
KERNEL_CACHE_EPOCH.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
}
#[cfg(feature = "experimental-tune")]
pub unsafe fn clear_kernel_cache() -> usize {
unsafe { retain_kernels(|_| false) }
}
#[cfg(feature = "experimental-tune")]
pub unsafe fn evict_kernel(key: &TileFunctionKey) -> bool {
let removed = get_kernel_cache().remove(key).is_some();
if removed {
bump_kernel_cache_epoch();
}
removed
}
#[cfg(feature = "experimental-tune")]
pub unsafe fn retain_kernels(mut pred: impl FnMut(&TileFunctionKey) -> bool) -> usize {
let cache = get_kernel_cache();
let keys: Vec<TileFunctionKey> = cache.iter().map(|entry| entry.key().clone()).collect();
let mut removed = 0;
for key in keys {
if !pred(&key) && cache.remove(&key).is_some() {
removed += 1;
}
}
if removed > 0 {
bump_kernel_cache_epoch();
}
removed
}
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,
) -> Result<(), Error> {
let filename = format!("{module_name}_{function_name}_{cache_hash_str}.{extension}");
let path = PathBuf::from(dir).join(filename);
fs::write(&path, contents).map_err(|e| {
Error::Anyhow(anyhow::anyhow!(
"failed to write the IR dump for {module_name}::{function_name} to {path:?} \
(dump_mlir_dir = {dir:?}): {e}"
))
})?;
println!("IR written to {path:?}");
Ok(())
}
#[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 tileiras_opts = TileirasOptions::from_compile_options(compile_options);
let (cubin, mut stage2_source) =
compile_bytecode_cached(&bytecode, bc_version, gpu_name, &tileiras_opts)?;
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 unsafe { 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,
&tileiras_opts,
)?;
recompile_ms = recompile_start.elapsed().as_secs_f64() * 1000.0;
stage2_ms += recompile_ms;
unsafe { 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<T: DType> KernelArgument for &Partition<&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 }
}
impl<const RANK: usize, I, DI> GraphNode for DeviceOperationPartition<RANK, I, DI>
where
I: Send + IntoPartition + IntoPartitionArc,
DI: DeviceOp<Output = I> + GraphNode,
{
}
impl<I: Send, DI> GraphNode for UnwrapPartition<I, DI> where
DI: DeviceOp<Output = Partition<I>> + GraphNode
{
}
pub struct PrepareInput<DI, T> {
op: DI,
_elem: std::marker::PhantomData<fn() -> T>,
}
impl<DI, T> PrepareInput<DI, T> {
pub fn new(op: DI) -> Self {
Self {
op,
_elem: std::marker::PhantomData,
}
}
}
impl<T: DType, K: KernelInput<T>, DI: DeviceOp<Output = K>> DeviceOp for PrepareInput<DI, T> {
type Output = K::Stored;
unsafe fn execute(self, context: &ExecutionContext) -> Result<K::Stored, DeviceError> {
Ok(K::prepare(self.op.execute(context)?))
}
}
impl<T: DType, K: KernelInput<T>, DI: DeviceOp<Output = K> + GraphNode> GraphNode
for PrepareInput<DI, T>
{
}
impl<T: DType, K: KernelInput<T>, DI: DeviceOp<Output = K>> IntoFuture for PrepareInput<DI, T> {
type Output = Result<K::Stored, DeviceError>;
type IntoFuture = DeviceFuture<K::Stored, PrepareInput<DI, T>>;
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 PrepareOutput<DI, T> {
op: DI,
_elem: std::marker::PhantomData<fn() -> T>,
}
impl<DI, T> PrepareOutput<DI, T> {
pub fn new(op: DI) -> Self {
Self {
op,
_elem: std::marker::PhantomData,
}
}
}
impl<T: DType, K: KernelOutput<T>, DI: DeviceOp<Output = K>> DeviceOp for PrepareOutput<DI, T> {
type Output = K::Stored;
unsafe fn execute(self, context: &ExecutionContext) -> Result<K::Stored, DeviceError> {
Ok(K::prepare(self.op.execute(context)?))
}
}
impl<T: DType, K: KernelOutput<T>, DI: DeviceOp<Output = K> + GraphNode> GraphNode
for PrepareOutput<DI, T>
{
}
impl<T: DType, K: KernelOutput<T>, DI: DeviceOp<Output = K>> IntoFuture for PrepareOutput<DI, T> {
type Output = Result<K::Stored, DeviceError>;
type IntoFuture = DeviceFuture<K::Stored, PrepareOutput<DI, T>>;
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 KernelArgs<Ops>(pub Ops);
macro_rules! impl_kernel_args {
($(($op:ident, $out:ident)),*) => {
impl<$($op: DeviceOp),*> DeviceOp for KernelArgs<($($op,)*)> {
type Output = ($(<$op as DeviceOp>::Output,)*);
#[allow(unused_variables, clippy::unused_unit)]
unsafe fn execute(
self,
context: &ExecutionContext,
) -> Result<<Self as DeviceOp>::Output, DeviceError> {
let ($($out,)*) = self.0;
Ok(($($out.execute(context)?,)*))
}
}
impl<$($op: GraphNode),*> GraphNode for KernelArgs<($($op,)*)> {}
impl<$($op: DeviceOp),*> IntoFuture for KernelArgs<($($op,)*)> {
type Output = Result<<Self as DeviceOp>::Output, DeviceError>;
type IntoFuture = DeviceFuture<<Self as DeviceOp>::Output, Self>;
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),
}
}
}
};
}
macro_rules! impl_kernel_args_cascade {
(@acc [$($acc:tt),*]) => {
impl_kernel_args!($($acc),*);
};
(@acc [$($acc:tt),*] $next:tt $(, $rest:tt)*) => {
impl_kernel_args!($($acc),*);
impl_kernel_args_cascade!(@acc [$($acc,)* $next] $($rest),*);
};
($($pairs:tt),* $(,)?) => {
impl_kernel_args_cascade!(@acc [] $($pairs),*);
};
}
impl_kernel_args_cascade!(
(A0, a0),
(A1, a1),
(A2, a2),
(A3, a3),
(A4, a4),
(A5, a5),
(A6, a6),
(A7, a7),
(A8, a8),
(A9, a9),
(A10, a10),
(A11, a11),
(A12, a12),
(A13, a13),
(A14, a14),
(A15, a15),
(A16, a16),
(A17, a17),
(A18, a18),
(A19, a19),
(A20, a20),
(A21, a21),
(A22, a22),
(A23, a23),
(A24, a24),
(A25, a25),
(A26, a26),
(A27, a27),
(A28, a28),
(A29, a29),
(A30, a30),
(A31, a31),
(A32, a32),
(A33, a33),
(A34, a34),
(A35, a35),
(A36, a36),
(A37, a37),
(A38, a38),
(A39, a39),
(A40, a40),
(A41, a41),
(A42, a42),
(A43, a43),
(A44, a44),
(A45, a45),
(A46, a46),
(A47, a47),
(A48, a48),
(A49, a49),
(A50, a50),
(A51, a51),
(A52, a52),
(A53, a53),
(A54, a54),
(A55, a55),
(A56, a56),
(A57, a57),
(A58, a58),
(A59, a59),
(A60, a60),
(A61, a61),
(A62, a62),
(A63, a63)
);
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 mut host = Vec::<T>::with_capacity(size);
if size > 0 {
unsafe {
memcpy_dtoh_async(
host.as_mut_ptr(),
cu_deviceptr,
size,
context.get_cuda_stream(),
)
}?;
}
unsafe { host.set_len(size) };
Ok(host)
}
}
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);
}
}