use cutile;
use cutile_compiler::compiler::utils::CompileOptions;
mod common;
#[cutile::module]
mod checked_store_module {
use cutile::core::*;
#[cutile::entry]
fn constant_out_of_range<const N: i32, const BLOCK: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let tile_shape = const_shape![1, BLOCK];
let mut p = out.partition_mut(tile_shape);
let t: Tile<f32, { [1, BLOCK] }> = constant(0.0, tile_shape);
p.store(t, [1i32, 0i32]);
}
#[cutile::entry]
fn constant_negative<const N: i32, const BLOCK: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let tile_shape = const_shape![1, BLOCK];
let mut p = out.partition_mut(tile_shape);
let t: Tile<f32, { [1, BLOCK] }> = constant(0.0, tile_shape);
p.store(t, [0i32, -1i32]);
}
#[cutile::entry]
fn runtime_scalar_index<const N: i32, const BLOCK: i32>(
out: &mut Tensor<f32, { [1, N] }>,
idx: i32,
) {
let tile_shape = const_shape![1, BLOCK];
let mut p = out.partition_mut(tile_shape);
let t: Tile<f32, { [1, BLOCK] }> = constant(0.0, tile_shape);
p.store(t, [0i32, idx]);
}
#[cutile::entry]
fn block_id_index<const N: i32, const BLOCK: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let pid: (i32, i32, i32) = get_tile_block_id();
let tile_shape = const_shape![1, BLOCK];
let mut p = out.partition_mut(tile_shape);
let t: Tile<f32, { [1, BLOCK] }> = constant(0.0, tile_shape);
p.store(t, [0i32, pid.0]);
}
#[cutile::entry]
fn mixed_constant_and_inferred<const N: i32, const BLOCK: i32>(
out: &mut Tensor<f32, { [1, N] }>,
) {
let tile_shape = const_shape![1, BLOCK];
let mut p = out.partition_mut(tile_shape);
for j in 0i32..num_tiles(&p, 1) {
let t: Tile<f32, { [1, BLOCK] }> = constant(0.0, tile_shape);
p.store(t, [0i32, j]);
}
}
#[cutile::entry]
fn dynamic_row_extent<const N: i32, const BLOCK: i32>(out: &mut Tensor<f32, { [-1, N] }>) {
let tile_shape = const_shape![1, BLOCK];
let mut p = out.partition_mut(tile_shape);
for j in 0i32..num_tiles(&p, 1) {
let t: Tile<f32, { [1, BLOCK] }> = constant(0.0, tile_shape);
p.store(t, [0i32, j]);
}
}
}
use checked_store_module::__module_ast_self;
use cutile_compiler::compile_api::CheckPlacementCounts;
fn compile_mlir(
name: &str,
generics: &[&str],
strides: &[(&str, &[i32])],
) -> Result<String, String> {
let generics: Vec<String> = generics.iter().map(|s| s.to_string()).collect();
common::compile_to_ir(
__module_ast_self,
"checked_store_module",
name,
&generics,
strides,
&[],
&[],
None,
&CompileOptions::default(),
)
.map_err(|err| err.to_string())
}
fn artifacts(
name: &str,
generics: &[&str],
strides: &[(&str, &[i32])],
) -> cutile_compiler::compile_api::CompileArtifacts {
let generics: Vec<String> = generics.iter().map(|s| s.to_string()).collect();
cutile_compiler::compile_api::KernelCompiler::new(
__module_ast_self,
"checked_store_module",
name,
)
.target("sm_120")
.generics(generics)
.strides(strides)
.options(CompileOptions::default())
.compile()
.unwrap_or_else(|e| panic!("compile {name}: {e}"))
}
fn counts(name: &str, generics: &[&str], strides: &[(&str, &[i32])]) -> (u32, u32, u32) {
let CheckPlacementCounts {
discharged,
hoisted,
in_place,
} = artifacts(name, generics, strides).check_counts();
(discharged, hoisted, in_place)
}
const STRIDES: &[(&str, &[i32])] = &[("out", &[256, 1])];
#[test]
fn constant_past_the_end_is_rejected() {
common::with_test_stack(|| {
let err = compile_mlir("constant_out_of_range", &["256", "64"], STRIDES)
.expect_err("a constant past the end of the axis must not compile");
assert!(
err.contains("Bounds check failed") || err.contains("out of bounds"),
"expected an out-of-range diagnostic, got: {err}"
);
});
}
#[test]
fn constant_negative_index_is_rejected() {
common::with_test_stack(|| {
let err = compile_mlir("constant_negative", &["256", "64"], STRIDES)
.expect_err("a negative constant index must not compile");
assert!(
err.contains("Bounds check failed")
|| err.contains("0 <=")
|| err.to_lowercase().contains("negative"),
"expected a lower-bound diagnostic, got: {err}"
);
});
}
#[test]
fn runtime_scalar_index_keeps_a_two_sided_check() {
common::with_test_stack(|| {
let mlir = compile_mlir("runtime_scalar_index", &["256", "64"], STRIDES)
.expect("compile runtime_scalar_index");
assert!(
mlir.contains("partition access out of bounds"),
"an unprovable store index must keep its check:\n{mlir}"
);
assert!(
mlir.contains("greater_than_or_equal"),
"a runtime store index needs a lower-bound guard, found none:\n{mlir}"
);
assert_eq!(
counts("runtime_scalar_index", &["256", "64"], STRIDES),
(1, 0, 1),
"expected the constant row to discharge and the runtime column to \
stay in the kernel"
);
});
}
#[test]
fn block_id_index_discharges_by_staking_a_grid_check() {
common::with_test_stack(|| {
use cutile::tile_kernel::validate_launch_checks;
let mlir = compile_mlir("block_id_index", &["256", "64"], STRIDES)
.expect("compile block_id_index");
assert!(
!mlir.contains("partition access out of bounds"),
"the block-id store check should leave the kernel:\n{mlir}"
);
let compiled = artifacts("block_id_index", &["256", "64"], STRIDES);
let checks = compiled.launch_checks();
assert!(
format!("{checks:?}").contains("num_tile_blocks(0)"),
"the discharge must stake a claim on the launch grid: {checks:?}"
);
let roots = [vec![256i32, 256]];
let views = [vec![1i32, 256]];
assert!(
validate_launch_checks(checks, &roots, &views, (4, 1, 1)).is_ok(),
"a grid inside the view's tile count must launch: {checks:?}"
);
assert!(
validate_launch_checks(checks, &roots, &views, (5, 1, 1)).is_err(),
"a grid wider than the view's tile count must be rejected: {checks:?}"
);
});
}
#[test]
fn mixed_constant_and_inferred_coordinates_fully_discharge() {
common::with_test_stack(|| {
assert_eq!(
counts("mixed_constant_and_inferred", &["256", "64"], STRIDES),
(2, 0, 0),
"expected the constant row and the inferred column to discharge"
);
let mlir = compile_mlir("mixed_constant_and_inferred", &["256", "64"], STRIDES)
.expect("compile mixed_constant_and_inferred");
assert!(
!mlir.contains("partition access out of bounds"),
"a fully provable checked store must emit no check:\n{mlir}"
);
});
}
#[test]
fn dynamic_row_extent_moves_the_check_to_launch() {
common::with_test_stack(|| {
use cutile::tile_kernel::validate_launch_checks;
let mlir = compile_mlir("dynamic_row_extent", &["256", "64"], STRIDES)
.expect("compile dynamic_row_extent");
assert!(
!mlir.contains("partition access out of bounds"),
"the non-empty-extent obligation should leave the kernel:\n{mlir}"
);
let compiled = artifacts("dynamic_row_extent", &["256", "64"], STRIDES);
let checks = compiled.launch_checks();
assert!(
!checks.is_empty(),
"expected a launch check for the dynamic row extent, got none"
);
assert!(
format!("{checks:?}").contains("ViewExtent"),
"a &mut param's extent check must be in the view (slab) frame: {checks:?}"
);
let roots = [vec![256i32, 256]];
assert!(
validate_launch_checks(checks, &roots, &[vec![0i32, 256]], (1, 1, 1)).is_err(),
"an empty slab must be rejected at launch"
);
assert!(
validate_launch_checks(checks, &roots, &[vec![1i32, 256]], (1, 1, 1)).is_ok(),
"a non-empty slab must be accepted at launch"
);
});
}