use proc_macro2::Span;
use super::super::_function::CUDATileFunctionCompiler;
use super::super::_value::{CompilerContext, LoopFrame};
use super::super::shared_utils::TileBinaryOp;
use super::goals::AxisGoals;
use crate::compiler::_value::TileRustValue;
use crate::compiler::tile_rust_type::TileRustType;
use crate::error::JITError;
use crate::generics::GenericVars;
use cutile_ir::builder::{append_op, OpBuilder};
use cutile_ir::bytecode::Opcode;
use cutile_ir::ir::{BlockId, Module};
pub(super) enum HoistIndex {
Const { min: i32, max: i32 },
Invariant,
InductionAffine {
scale: i64,
offset: i64,
lower: cutile_ir::ir::Value,
upper: cutile_ir::ir::Value,
},
}
enum Extreme {
Strongest,
Weakest,
}
fn fits_i32(bounds: &crate::bounds::Bounds<i64>) -> bool {
bounds.start >= i32::MIN as i64 && bounds.end <= i32::MAX as i64
}
impl<'m> CUDATileFunctionCompiler<'m> {
fn classify_hoist(
&self,
goals: &AxisGoals<'_>,
dynamic_extent: Option<&TileRustValue>,
block_id: BlockId,
ctx: &CompilerContext,
) -> (Option<(LoopFrame, HoistIndex)>, Option<&'static str>) {
let mut no_hoist_why: Option<&'static str> = None;
let hoist = 'classify: {
if crate::cuda_tile_runtime_utils::check_hoisting_disabled() {
no_hoist_why = Some("hoisting disabled via CUTILE_DISABLE_CHECK_HOISTING");
break 'classify None;
}
let frames = &ctx.loop_frames;
let Some(innermost) = frames.last() else {
break 'classify None;
};
if block_id != innermost.body_block {
no_hoist_why = Some("check sits inside a conditional block");
break 'classify None;
}
let mut operand_deps: Vec<cutile_ir::ir::Value> = vec![];
if let Some(shape_value) = dynamic_extent {
let Some(value) = shape_value.value else {
no_hoist_why = Some("dynamic shape operand has no direct value");
break 'classify None;
};
operand_deps.push(value);
}
let kind = if let Some(bounds) = goals.index.bounds.filter(fits_i32) {
HoistIndex::Const {
min: bounds.start as i32,
max: bounds.end as i32,
}
} else {
let Some(value) = goals.index.value else {
no_hoist_why = Some("index has no direct value");
break 'classify None;
};
let affine = if innermost.induction_values.contains(&value) {
Some((value.index(), 1i64, 0i64))
} else {
goals
.index
.term
.as_ref()
.and_then(|term| term.as_single_affine())
.and_then(|(atom, scale, offset)| match atom {
cuda_async::predicate::Atom::Iv(id)
if innermost.induction_values.iter().any(|v| v.index() == id) =>
{
Some((id, scale, offset))
}
_ => None,
})
};
if let Some((iv_id, scale, offset)) = affine {
if !innermost.unit_step {
no_hoist_why = Some("index depends on a non-unit-step induction variable");
break 'classify None;
}
if scale == 0 {
no_hoist_why = Some("degenerate affine index");
break 'classify None;
}
let static_max = innermost.induction_range.and_then(|iv_range| {
crate::value_facts::term_range(
&cuda_async::predicate::Term::affine(
cuda_async::predicate::Atom::Iv(iv_id),
scale,
offset,
),
&|atom| match atom {
cuda_async::predicate::Atom::Iv(id) if *id == iv_id => {
Some(iv_range)
}
_ => None,
},
)
});
if let Some(range) = static_max.filter(fits_i32) {
HoistIndex::Const {
min: range.start as i32,
max: range.end as i32,
}
} else if !(scale == 1 && offset == 0) {
no_hoist_why = Some("non-identity affine instance could overflow i32");
break 'classify None;
} else {
operand_deps.push(innermost.upper);
operand_deps.push(innermost.lower);
HoistIndex::InductionAffine {
scale,
offset,
lower: innermost.lower,
upper: innermost.upper,
}
}
} else if value.index() < innermost.value_watermark {
operand_deps.push(value);
HoistIndex::Invariant
} else {
no_hoist_why = Some("index is computed inside the loop body");
break 'classify None;
}
};
let mut target = frames.len() - 1;
while target > 0 {
let inner = &frames[target];
let outer = &frames[target - 1];
let contiguous = inner.preheader_block == outer.body_block;
let deps_dominate = operand_deps
.iter()
.all(|value| value.index() < outer.value_watermark);
if contiguous && inner.known_non_empty && deps_dominate {
target -= 1;
} else {
break;
}
}
Some((frames[target].clone(), kind))
};
(hoist, no_hoist_why)
}
#[allow(clippy::too_many_arguments)]
fn affine_extreme_instance(
&self,
module: &mut Module,
check_block: BlockId,
lower: cutile_ir::ir::Value,
upper: cutile_ir::ir::Value,
scale: i64,
offset: i64,
extreme: Extreme,
index_ty: &TileRustType,
generic_vars: &GenericVars,
ctx: &mut CompilerContext,
span: &Span,
) -> Result<TileRustValue, JITError> {
let at_last_iteration = (scale > 0) == matches!(extreme, Extreme::Strongest);
let mut instance = if at_last_iteration {
let upper_value = TileRustValue::new_primitive(upper, index_ty.clone(), None);
let one = self.compile_constant(module, check_block, generic_vars, 1)?;
self.compile_binary_op_from_values(
module,
check_block,
upper_value,
one,
&TileBinaryOp::Sub,
generic_vars,
ctx,
None,
span,
)?
} else {
TileRustValue::new_primitive(lower, index_ty.clone(), None)
};
if scale != 1 {
let scale_value =
self.compile_constant(module, check_block, generic_vars, scale as i32)?;
instance = self.compile_binary_op_from_values(
module,
check_block,
instance,
scale_value,
&TileBinaryOp::Mul,
generic_vars,
ctx,
None,
span,
)?;
}
if offset != 0 {
let offset_value =
self.compile_constant(module, check_block, generic_vars, offset as i32)?;
instance = self.compile_binary_op_from_values(
module,
check_block,
instance,
offset_value,
&TileBinaryOp::Add,
generic_vars,
ctx,
None,
span,
)?;
}
Ok(instance)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn place_residual_check(
&self,
module: &mut Module,
block_id: BlockId,
goals: &AxisGoals<'_>,
dynamic_extent: Option<TileRustValue>,
generic_vars: &GenericVars,
ctx: &mut CompilerContext,
span: &Span,
) -> Result<(), JITError> {
let axis = goals.axis;
let (hoist, no_hoist_why) =
self.classify_hoist(goals, dynamic_extent.as_ref(), block_id, ctx);
let (check_block, guard_bounds, hoist_kind) = match &hoist {
Some((frame, kind)) => (
frame.preheader_block,
(!frame.known_non_empty).then_some((frame.lower, frame.upper)),
Some(kind),
),
None => (block_id, None, None),
};
let lower_static: Option<i64> = if crate::cuda_tile_runtime_utils::force_device_checks() {
None
} else {
goals
.index
.bounds
.filter(fits_i32)
.map(|b| b.start)
.or(match &hoist_kind {
Some(HoistIndex::Const { min, .. }) => Some(*min as i64),
_ => None,
})
};
if let Some(min) = lower_static {
if min < 0 {
return self.jit_error_result(
span,
&format!(
"partition access out of bounds: dim {axis}, block index can be {min} \
(0 <= index is required)"
),
);
}
}
let needs_lower_guard = lower_static.is_none();
let placement_detail = no_hoist_why
.map(|why| format!(" ({why})"))
.unwrap_or_default();
self.deny_residual_check(
&format!("the bounds check for partition axis {axis}{placement_detail}"),
&format!(
"For a loop counter, iterate `0..num_tiles(&partition, {axis})`, whose \
result carries the axis it counts (a hand-computed `n / TILE` is the \
same number but proves nothing); for an index from a different tensor, \
relate the extents with `preconditions = (dim(a, i) == dim(b, j),)`; \
a tile-block id cannot be proven against a partition's tile count \
today, since the launch grid counts CTA slabs rather than tiles"
),
span,
)?;
if hoist.is_some() {
self.check_stats
.hoisted
.set(self.check_stats.hoisted.get() + 1);
} else {
self.check_stats
.in_place
.set(self.check_stats.in_place.get() + 1);
if let Some(why) = no_hoist_why {
if crate::cuda_tile_runtime_utils::jit_hoist_log_enabled() {
eprintln!(
"[cutile::jit] bounds check for dim {axis} stays in the loop body: {why}"
);
}
}
}
let tile_dim_value =
self.compile_constant(module, check_block, generic_vars, goals.tile)?;
let index_instance = match hoist_kind {
Some(HoistIndex::InductionAffine {
scale,
offset,
lower,
upper,
}) => {
self.affine_extreme_instance(
module,
check_block,
*lower,
*upper,
*scale,
*offset,
Extreme::Strongest,
&goals.index.ty,
generic_vars,
ctx,
span,
)?
}
Some(HoistIndex::Const { max, .. }) => {
self.compile_constant(module, check_block, generic_vars, *max)?
}
Some(HoistIndex::Invariant) | None => {
if crate::cuda_tile_runtime_utils::force_device_checks() {
goals.index.clone()
} else if let Some(bounds) = goals.index.bounds.filter(fits_i32) {
self.compile_constant(module, check_block, generic_vars, bounds.end as i32)?
} else {
goals.index.clone()
}
}
};
let shape_dim_value = match dynamic_extent {
Some(shape_value) => shape_value,
None => {
let extent = goals
.static_extent
.expect("residual check without a dynamic extent has a static one");
self.compile_constant(module, check_block, generic_vars, extent)?
}
};
let tile_minus_one =
self.compile_constant(module, check_block, generic_vars, goals.tile - 1)?;
let shape_plus_tile_minus_one = self.compile_binary_op_from_values(
module,
check_block,
shape_dim_value.clone(),
tile_minus_one,
&TileBinaryOp::Add,
generic_vars,
ctx,
None,
span,
)?;
let div_result_value = self.compile_binary_op_from_values(
module,
check_block,
shape_plus_tile_minus_one,
tile_dim_value,
&TileBinaryOp::Div,
generic_vars,
ctx,
None,
span,
)?;
let ineq_result_value = self.compile_binary_op_from_values(
module,
check_block,
index_instance,
div_result_value,
&TileBinaryOp::Lt,
generic_vars,
ctx,
None,
span,
)?;
let ineq_result_value = if needs_lower_guard {
let guard_operand = match hoist_kind {
Some(HoistIndex::InductionAffine {
scale,
offset,
lower,
upper,
}) => self.affine_extreme_instance(
module,
check_block,
*lower,
*upper,
*scale,
*offset,
Extreme::Weakest,
&goals.index.ty,
generic_vars,
ctx,
span,
)?,
_ => goals.index.clone(),
};
let zero = self.compile_constant(module, check_block, generic_vars, 0)?;
let lower_ok = self.compile_binary_op_from_values(
module,
check_block,
guard_operand,
zero,
&TileBinaryOp::Ge,
generic_vars,
ctx,
None,
span,
)?;
self.compile_binary_op_from_values(
module,
check_block,
lower_ok,
ineq_result_value,
&TileBinaryOp::BitAnd,
generic_vars,
ctx,
None,
span,
)?
} else {
ineq_result_value
};
let checked_value = if let Some((lower, upper)) = guard_bounds {
let upper_value =
TileRustValue::new_primitive(upper, self.scalar_i32_type(span)?, None);
let lower_value =
TileRustValue::new_primitive(lower, self.scalar_i32_type(span)?, None);
let vacuous = self.compile_binary_op_from_values(
module,
check_block,
upper_value,
lower_value,
&TileBinaryOp::Le,
generic_vars,
ctx,
None,
span,
)?;
self.compile_binary_op_from_values(
module,
check_block,
vacuous,
ineq_result_value,
&TileBinaryOp::BitOr,
generic_vars,
ctx,
None,
span,
)?
} else {
ineq_result_value
};
let result_value = checked_value
.value
.ok_or_else(|| self.jit_error(span, "failed to compile a binary expression operand"))?;
let shape_desc = match goals.static_extent {
Some(extent) => format!("{extent}"),
None => "?".to_string(),
};
let suffix = if needs_lower_guard {
" or index < 0"
} else {
""
};
let message = format!(
"partition access out of bounds: dim {axis}, block index >= ceil({shape_desc}/{})\
{suffix}",
goals.tile
);
let (assert_op_id, _) = OpBuilder::new(Opcode::Assert, self.ir_location(span))
.attr("message", cutile_ir::ir::Attribute::String(message))
.operand(result_value)
.build(module);
append_op(module, check_block, assert_op_id);
Ok(())
}
}