#![allow(deprecated)]
use cutile_compiler::compiler::utils::CompileOptions;
mod common;
#[cutile::module]
mod token_ordering_module {
use cutile::core::*;
#[cutile::entry()]
fn straightline_same_index<const N: i32, const BLOCK_SIZE: i32>(
out: &mut Tensor<f32, { [1, N] }>,
) {
let cols = Dim::new(N / BLOCK_SIZE);
let tile_shape = const_shape![1, BLOCK_SIZE];
let mut v = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(0.0, tile_shape);
v.store(tile, coord((0i32, 0i32)));
v.store(tile, coord((0i32, 0i32)));
}
#[cutile::entry()]
fn straightline_disjoint<const N: i32, const BLOCK_SIZE: i32>(
out: &mut Tensor<f32, { [1, N] }>,
) {
let cols = Dim::new(N / BLOCK_SIZE);
let tile_shape = const_shape![1, BLOCK_SIZE];
let mut v = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(0.0, tile_shape);
v.store(tile, coord((0i32, 0i32)));
v.store(tile, coord((0i32, 1i32)));
}
#[cutile::entry()]
fn single_view_loop<const N: i32, const BLOCK_SIZE: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let cols = Dim::new(N / BLOCK_SIZE);
let tile_shape = const_shape![1, BLOCK_SIZE];
let mut v = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
for j in cols {
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(0.0, tile_shape);
v.store(tile, coord((0i32, j)));
}
}
#[cutile::entry()]
fn two_epoch_aliasing<const N: i32, const BLOCK_SIZE: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let cols = Dim::new(N / BLOCK_SIZE);
let tile_shape = const_shape![1, BLOCK_SIZE];
{
let mut a = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(1.0, tile_shape);
a.store(tile, coord((0i32, 0i32)));
}
{
let mut b = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(2.0, tile_shape);
b.store(tile, coord((0i32, 0i32)));
}
}
#[cutile::entry()]
fn two_epoch_aliasing_in_loop<const N: i32, const BLOCK_SIZE: i32>(
out: &mut Tensor<f32, { [1, N] }>,
) {
let cols = Dim::new(N / BLOCK_SIZE);
let tile_shape = const_shape![1, BLOCK_SIZE];
{
let mut a = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
for j in cols {
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(1.0, tile_shape);
a.store(tile, coord((0i32, j)));
}
}
{
let mut b = out
.partition_mut(tile_shape)
.with_bounds((Dim::new(1), cols));
for j in cols {
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(2.0, tile_shape);
b.store(tile, coord((0i32, j)));
}
}
}
#[cutile::entry()]
fn nested_loop<const M: i32, const N: i32, const BM: i32, const BN: i32>(
out: &mut Tensor<f32, { [M, N] }>,
) {
let rows = Dim::new(M / BM);
let cols = Dim::new(N / BN);
let ts = const_shape![BM, BN];
let mut v = out.partition_mut(ts).with_bounds((rows, cols));
for i in rows {
for j in cols {
let tile: Tile<f32, { [BM, BN] }> = constant(0.0, ts);
v.store(tile, coord((i, j)));
}
}
}
#[cutile::entry()]
fn two_epoch_nested_loop<const M: i32, const N: i32, const BM: i32, const BN: i32>(
out: &mut Tensor<f32, { [M, N] }>,
) {
let rows = Dim::new(M / BM);
let cols = Dim::new(N / BN);
let ts = const_shape![BM, BN];
{
let mut a = out.partition_mut(ts).with_bounds((rows, cols));
for i in rows {
for j in cols {
let tile: Tile<f32, { [BM, BN] }> = constant(1.0, ts);
a.store(tile, coord((i, j)));
}
}
}
{
let mut b = out.partition_mut(ts).with_bounds((rows, cols));
for i in rows {
for j in cols {
let tile: Tile<f32, { [BM, BN] }> = constant(2.0, ts);
b.store(tile, coord((i, j)));
}
}
}
}
#[cutile::entry()]
fn store_after_loop<const N: i32, const BLOCK_SIZE: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let cols = Dim::new(N / BLOCK_SIZE);
let ts = const_shape![1, BLOCK_SIZE];
let mut v = out.partition_mut(ts).with_bounds((Dim::new(1), cols));
for j in cols {
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(0.0, ts);
v.store(tile, coord((0i32, j)));
}
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(1.0, ts);
v.store(tile, coord((0i32, 0i32)));
}
#[cutile::entry()]
fn within_iteration_aliasing<const N: i32, const BLOCK_SIZE: i32>(
out: &mut Tensor<f32, { [1, N] }>,
) {
let cols = Dim::new(N / BLOCK_SIZE);
let ts = const_shape![1, BLOCK_SIZE];
let mut v = out.partition_mut(ts).with_bounds((Dim::new(1), cols));
for j in cols {
let t0: Tile<f32, { [1, BLOCK_SIZE] }> = constant(0.0, ts);
v.store(t0, coord((0i32, j)));
let t1: Tile<f32, { [1, BLOCK_SIZE] }> = constant(1.0, ts);
v.store(t1, coord((0i32, j))); }
}
#[cutile::entry()]
fn const_index_loop<const N: i32, const BLOCK_SIZE: i32>(out: &mut Tensor<f32, { [1, N] }>) {
let cols = Dim::new(N / BLOCK_SIZE);
let ts = const_shape![1, BLOCK_SIZE];
let mut v = out.partition_mut(ts).with_bounds((Dim::new(1), cols));
for _j in cols {
let tile: Tile<f32, { [1, BLOCK_SIZE] }> = constant(0.0, ts);
v.store(tile, coord((0i32, 0i32))); }
}
}
use token_ordering_module::__module_ast_self;
fn compile_g(function_name: &str, generics: &[&str], strides: &[(&str, &[i32])]) -> String {
let function_name = function_name.to_string();
let generics: Vec<String> = generics.iter().map(|s| s.to_string()).collect();
let strides: Vec<(String, Vec<i32>)> = strides
.iter()
.map(|(n, s)| (n.to_string(), s.to_vec()))
.collect();
common::with_test_stack(move || {
let strides: Vec<(&str, &[i32])> = strides
.iter()
.map(|(n, s)| (n.as_str(), s.as_slice()))
.collect();
common::compile_to_ir(
__module_ast_self,
"token_ordering_module",
&function_name,
&generics,
&strides,
&[],
&[],
None,
&CompileOptions::default(),
)
.unwrap_or_else(|e| panic!("failed to compile {function_name}: {e}"))
})
}
fn compile(function_name: &str) -> String {
compile_g(function_name, &["256", "64"], &[("out", &[256, 1])])
}
use std::collections::{HashMap, HashSet};
fn store_input_tokens(mlir: &str) -> Vec<String> {
mlir.lines()
.filter(|l| l.contains("store_view_tko"))
.filter_map(store_input_token)
.collect()
}
fn store_input_token(line: &str) -> Option<String> {
line.split("token = ")
.nth(1)
.and_then(|rest| rest.split_whitespace().next())
.map(str::to_string)
}
fn store_io_tokens(mlir: &str) -> Vec<(String, String)> {
mlir.lines()
.filter(|l| l.contains("store_view_tko"))
.filter_map(|l| {
let output = l.split_once('=')?.0.trim().to_string();
let input = store_input_token(l)?;
output.starts_with('%').then_some((output, input))
})
.collect()
}
fn ssa_refs(s: &str) -> Vec<String> {
let bytes = s.as_bytes();
let mut refs = vec![];
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
let start = i;
i += 1;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
refs.push(s[start..i].to_string());
} else {
i += 1;
}
}
refs
}
fn ssa_def_operands(mlir: &str) -> HashMap<String, Vec<String>> {
let mut defs: HashMap<String, Vec<String>> = HashMap::new();
let mut regions: Vec<(String, i32)> = Vec::new();
let mut depth = 0i32;
for line in mlir.lines() {
if let Some((lhs, rhs)) = line.split_once('=') {
let results: Vec<String> = lhs
.split(',')
.map(str::trim)
.filter(|s| s.starts_with('%'))
.map(str::to_string)
.collect();
if !results.is_empty() {
let operands = ssa_refs(rhs);
for r in &results {
defs.entry(r.clone()).or_default().extend(operands.clone());
}
if rhs.trim_start().starts_with("for ") && line.contains('{') {
regions.push((results[0].clone(), depth));
}
}
}
let trimmed = line.trim_start();
if trimmed.starts_with("continue ") || trimmed.starts_with("yield ") {
if let Some((for_result, _)) = regions.last() {
let ops = ssa_refs(trimmed);
defs.entry(for_result.clone()).or_default().extend(ops);
}
}
depth += line.matches('{').count() as i32 - line.matches('}').count() as i32;
while regions.last().is_some_and(|(_, open)| depth <= *open) {
regions.pop();
}
}
defs
}
fn depends_on(defs: &HashMap<String, Vec<String>>, source: &str, target: &str) -> bool {
let mut stack = vec![source.to_string()];
let mut seen = HashSet::new();
while let Some(v) = stack.pop() {
if v == target {
return true;
}
if !seen.insert(v.clone()) {
continue;
}
if let Some(ops) = defs.get(&v) {
stack.extend(ops.iter().cloned());
}
}
false
}
fn assert_ordered(mlir: &str, earlier: usize, later: usize) {
let stores = store_io_tokens(mlir);
assert!(
stores.len() > earlier.max(later),
"expected at least {} stores, found {}:\n{mlir}",
earlier.max(later) + 1,
stores.len()
);
let defs = ssa_def_operands(mlir);
let earlier_out = &stores[earlier].0;
let later_in = &stores[later].1;
assert!(
depends_on(&defs, later_in, earlier_out),
"store {later} (input {later_in}) must depend on store {earlier} (output {earlier_out}); \
the happens-before edge is missing:\n{mlir}"
);
}
fn assert_forks_off_invariant(mlir: &str) {
let toks = store_input_tokens(mlir);
assert_eq!(
toks.len(),
1,
"expected one store in the loop body:\n{mlir}"
);
assert!(
mlir.contains(&format!("{} = make_token", toks[0])),
"loop store should fork off a loop-invariant token, got {}:\n{mlir}",
toks[0]
);
}
#[test]
fn straightline_same_index_is_ordered() {
assert_ordered(&compile("straightline_same_index"), 0, 1);
}
#[test]
fn within_iteration_aliasing_is_ordered() {
assert_ordered(&compile("within_iteration_aliasing"), 0, 1);
}
#[test]
fn loop_stores_fork_off_the_entry_token() {
assert_forks_off_invariant(&compile("single_view_loop"));
}
const NESTED_G: [&str; 4] = ["256", "256", "64", "64"];
#[test]
fn nested_loop_stores_fork_at_all_levels() {
assert_forks_off_invariant(&compile_g("nested_loop", &NESTED_G, &[("out", &[256, 1])]));
}
#[ignore = "target: disjoint straight-line stores should fork; today update_token over-serializes them"]
#[test]
fn straightline_disjoint_should_fork() {
let toks = store_input_tokens(&compile("straightline_disjoint"));
assert_eq!(toks.len(), 2, "expected two stores");
assert_eq!(
toks[0], toks[1],
"disjoint straight-line stores should share an input token (forked)"
);
}
#[test]
fn cross_epoch_aliasing_should_serialize() {
assert_ordered(&compile("two_epoch_aliasing"), 0, 1);
}
#[test]
fn cross_epoch_aliasing_in_loop_should_serialize() {
assert_ordered(&compile("two_epoch_aliasing_in_loop"), 0, 1);
}
#[test]
fn cross_epoch_nested_loops_should_serialize() {
assert_ordered(
&compile_g("two_epoch_nested_loop", &NESTED_G, &[("out", &[256, 1])]),
0,
1,
);
}
#[test]
fn store_after_loop_in_epoch_should_serialize() {
assert_ordered(&compile("store_after_loop"), 0, 1);
}
#[test]
fn const_index_loop_should_serialize() {
let mlir = compile("const_index_loop");
let toks = store_input_tokens(&mlir);
assert_eq!(toks.len(), 1, "expected one store in the loop body");
assert!(
!mlir.contains(&format!("{} = make_token", toks[0])),
"const-index loop store must be serialized (carried token), not forked off \
a loop-invariant make_token, got {}:\n{mlir}",
toks[0]
);
}