From 89a3d688d8acef0af4691e34d5f210b81123b397 Mon Sep 17 00:00:00 2001 From: Melih Elibol <1884921+elibol@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:45:20 -0700 Subject: [PATCH 1/2] fix(cutile): retain source origins for generated code and host checks Signed-off-by: Melih Elibol <1884921+elibol@users.noreply.github.com> --- cuda-async/src/predicate.rs | 42 ++ cutile-rs/CHANGELOG.md | 11 + .../guide/debugging-and-profiling.md | 9 + .../cutile-compiler/src/compiler/_function.rs | 58 ++- .../src/compiler/checks/goals.rs | 8 +- .../src/compiler/checks/mod.rs | 24 +- .../src/compiler/compile_expression.rs | 14 +- .../src/compiler/compile_inline.rs | 2 + .../src/compiler/compile_intrinsic.rs | 3 + .../cutile-compiler/src/passes/obligation.rs | 1 + cutile-rs/cutile/src/tile_kernel.rs | 8 +- .../tests/fixtures/generated_debug_helpers.rs | 53 ++ .../tests/generated_source_locations.rs | 492 ++++++++++++++++++ cutile-rs/scripts/run_cpu_tests.sh | 5 + 14 files changed, 714 insertions(+), 16 deletions(-) create mode 100644 cutile-rs/cutile/tests/fixtures/generated_debug_helpers.rs create mode 100644 cutile-rs/cutile/tests/generated_source_locations.rs diff --git a/cuda-async/src/predicate.rs b/cuda-async/src/predicate.rs index 6b451de323..291e3d72ec 100644 --- a/cuda-async/src/predicate.rs +++ b/cuda-async/src/predicate.rs @@ -384,6 +384,23 @@ impl Predicate { } } +/// One source frame of a compiler-generated check, innermost frame first. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckFrame { + pub file: String, + /// One-based source line. + pub line: u32, + /// Zero-based source column. + pub column: u32, + pub function: Option, +} + +/// The original access and its inline callers, retained when a check moves. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckOrigin { + pub frames: Vec, +} + /// A predicate hoisted out of the device kernel to the host launcher, with a /// diagnostic `cause` rendered if it fails. Carried on the compiled kernel /// (`Validator`) and evaluated once per launch by the host's `validate_launch`. @@ -393,6 +410,31 @@ impl Predicate { pub struct LaunchCheck { pub predicate: Predicate, pub cause: String, + /// All distinct source origins sharing this predicate. Deduplication keeps + /// one host comparison without discarding the other accesses it protects. + pub origins: Vec, +} + +impl std::fmt::Display for LaunchCheck { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.cause)?; + for origin in &self.origins { + for (index, frame) in origin.frames.iter().enumerate() { + let relation = if index == 0 { "at" } else { "inlined at" }; + write!( + f, + "\n {relation} {}:{}:{}", + frame.file, + frame.line, + frame.column + 1 + )?; + if let Some(function) = &frame.function { + write!(f, " ({function})")?; + } + } + } + Ok(()) + } } #[cfg(test)] diff --git a/cutile-rs/CHANGELOG.md b/cutile-rs/CHANGELOG.md index 4b7ec6eff9..7f99fbb108 100644 --- a/cutile-rs/CHANGELOG.md +++ b/cutile-rs/CHANGELOG.md @@ -7,8 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed + +- Const-generic array bindings retain their function or method signature as + their source origin, and generated reduction bodies retain the reduction + call's location and inline chain. +- Failed host launch checks report their original accesses and inline callers, + retaining distinct origins when equivalent predicates share one comparison. + ### Changed +- `cuda_async::predicate::LaunchCheck` now carries `origins`. Code constructing + this struct directly must initialize that field (use an empty vector when + source information is unavailable). - cuTile Rust now lives in the [NVIDIA/cuda-rust](https://github.com/NVIDIA/cuda-rust) repository under `cutile-rs/`, next to cuda-oxide (SIMT) and the shared host crates (`cuda-bindings`, `cuda-core`, `cuda-core-derive`, `cuda-async`) at the diff --git a/cutile-rs/cutile-book/guide/debugging-and-profiling.md b/cutile-rs/cutile-book/guide/debugging-and-profiling.md index 684a608cd1..32e88001d6 100644 --- a/cutile-rs/cutile-book/guide/debugging-and-profiling.md +++ b/cutile-rs/cutile-book/guide/debugging-and-profiling.md @@ -230,6 +230,15 @@ when inlined, including across modules. Core operations are attributed to the user's call site. Multiple generated instructions can still map to one Rust line; this is not a one-to-one mapping, especially with optimization. +Generated const-generic array bindings use the containing function or method's +signature as their source origin. Generated reduction bodies use the reduction +call's location, including its inline caller chain. + +Checks moved to the host launcher report the original access and its inline +callers when validation fails, including in builds without device debug info. +If several accesses share one launch check, its diagnostic retains each +distinct source origin while the predicate is evaluated only once. + ## Profiling Use NVIDIA **Nsight Compute** (`ncu`, reports opened with `ncu-ui`) for diff --git a/cutile-rs/cutile-compiler/src/compiler/_function.rs b/cutile-rs/cutile-compiler/src/compiler/_function.rs index 8dc61241f8..84ab7b297d 100644 --- a/cutile-rs/cutile-compiler/src/compiler/_function.rs +++ b/cutile-rs/cutile-compiler/src/compiler/_function.rs @@ -187,6 +187,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { a_axis: usize, b_tensor: &str, b_axis: usize, + span: &proc_macro2::Span, ) -> bool { use cuda_async::predicate::{Atom, Predicate, Term}; // Resolve both tensor names to param indices; if either is not a @@ -212,6 +213,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { self.lower_obligation( predicate, format!("dim({a_tensor}, {a_axis}) == dim({b_tensor}, {b_axis})"), + span, ) } @@ -238,6 +240,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { b_tensor: &str, b_axis: usize, tile: i32, + span: &proc_macro2::Span, ) -> bool { use crate::passes::obligation::{resolve, Obligation, Resolution}; use cuda_async::predicate::{Atom, Predicate, Term}; @@ -296,6 +299,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { format!( "ceil(dim({a_tensor}, {a_axis})/{tile}) <= ceil(dim({b_tensor}, {b_axis})/{tile})" ), + span, ) } @@ -315,17 +319,26 @@ impl<'m> CUDATileFunctionCompiler<'m> { &self, predicate: cuda_async::predicate::Predicate, cause: impl Into, + span: &proc_macro2::Span, ) -> bool { use crate::passes::obligation::{resolve, Obligation, Resolution}; let obligation = Obligation::new(predicate, cause); match resolve(&obligation, &self.assumptions) { Resolution::Jit => true, - Resolution::Launch(check) => { + Resolution::Launch(mut check) => { // Canonical predicates make duplicate detection exact. Two // obligations reducing to the same host compare (e.g. a shared // `Dim`'s divisibility, once per binding) cost one check. let mut checks = self.launch_checks.borrow_mut(); - if !checks.iter().any(|c| c.predicate == check.predicate) { + let origin = self.check_origin(span); + if let Some(existing) = checks.iter_mut().find(|c| c.predicate == check.predicate) { + if !origin.frames.is_empty() && !existing.origins.contains(&origin) { + existing.origins.push(origin); + } + } else { + if !origin.frames.is_empty() { + check.origins.push(origin); + } checks.push(check); } true @@ -334,6 +347,44 @@ impl<'m> CUDATileFunctionCompiler<'m> { } } + /// Flatten the same source/inline chain emitted into Tile IR into host + /// data. Host checks must not depend on the Tile IR crate or debug mode. + fn check_origin(&self, span: &proc_macro2::Span) -> cuda_async::predicate::CheckOrigin { + use cuda_async::predicate::{CheckFrame, CheckOrigin}; + use cutile_ir::ir::{DebugScope, Location}; + fn collect(location: &Location, frames: &mut Vec) { + match location { + Location::CallSite { callee, caller } => { + collect(callee, frames); + collect(caller, frames); + } + Location::FileLineCol { + filename, + line, + column, + } => frames.push(CheckFrame { + file: filename.clone(), + line: *line, + column: *column, + function: None, + }), + Location::DebugInfo(info) => frames.push(CheckFrame { + file: info.filename.clone(), + line: info.line, + column: info.column, + function: match &info.scope { + DebugScope::Subprogram(scope) => Some(scope.name.clone()), + _ => None, + }, + }), + Location::Unknown => {} + } + } + let mut frames = Vec::new(); + collect(&self.ir_location(span), &mut frames); + CheckOrigin { frames } + } + /// The kernel-visible extent of `partition`'s axis `axis`, taken from the /// *declared* parameter shape. /// @@ -1177,6 +1228,9 @@ impl<'m> CUDATileFunctionCompiler<'m> { // Add arrays as variables. for (key, value) in generic_vars.ordered_inst_array() { + // Binding materialization has no expression in the source. Use + // the user signature, not the generated ABI wrapper's span. + let _source = self.push_opaque_call_site(&self._function.sig.span()); let arr_expr = syn::parse2::(format!("{value:?}").parse().unwrap()).unwrap(); let arr_ty = syn::parse2::(format!("[i32;{}]", value.len()).parse().unwrap()) diff --git a/cutile-rs/cutile-compiler/src/compiler/checks/goals.rs b/cutile-rs/cutile-compiler/src/compiler/checks/goals.rs index 354a8c724a..e754f0cac5 100644 --- a/cutile-rs/cutile-compiler/src/compiler/checks/goals.rs +++ b/cutile-rs/cutile-compiler/src/compiler/checks/goals.rs @@ -173,6 +173,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { &self, goals: &AxisGoals<'_>, partition: &TileRustValue, + span: &Span, ) -> bool { if let (Some(origin), Some(target)) = ( goals.index.partition_axis_origin.as_ref(), @@ -192,6 +193,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { target, goals.extent_axis, goals.tile, + span, ) { return true; @@ -238,6 +240,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { &self, goals: &AxisGoals<'_>, partition: &TileRustValue, + span: &Span, ) -> bool { use cuda_async::predicate::{Atom, Predicate, Term}; let Some(term) = goals.index.term.as_ref() else { @@ -275,7 +278,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { "num_tile_blocks({k}) <= ceil(extent({tensor}, {})/{})", goals.extent_axis, goals.tile ); - self.lower_obligation(le, cause) + self.lower_obligation(le, cause, span) } /// The entailment/launch rung for a constant zero coordinate against a @@ -300,6 +303,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { &self, goals: &AxisGoals<'_>, partition: &TileRustValue, + span: &Span, ) -> bool { use cuda_async::predicate::{Predicate, Term}; if goals.static_extent.is_some() { @@ -322,7 +326,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { "partition access on axis {} of `{tensor}` requires a non-empty extent", goals.extent_axis ); - self.lower_obligation(predicate, cause) + self.lower_obligation(predicate, cause, span) } /// The runtime scalar holding the extent for a goal whose static diff --git a/cutile-rs/cutile-compiler/src/compiler/checks/mod.rs b/cutile-rs/cutile-compiler/src/compiler/checks/mod.rs index 297d21689e..6f26a1feb5 100644 --- a/cutile-rs/cutile-compiler/src/compiler/checks/mod.rs +++ b/cutile-rs/cutile-compiler/src/compiler/checks/mod.rs @@ -170,7 +170,11 @@ impl<'m> CUDATileFunctionCompiler<'m> { // Rung 1/3: axis provenance — a same-view iterand, or a // foreign iterand whose axis a declared root-dimension // equality relates to this one. - if self.discharge_by_axis_provenance(&axis_goals, &partition_value) { + if self.discharge_by_axis_provenance( + &axis_goals, + &partition_value, + &call_expr.span(), + ) { self.count_discharged(); continue; } @@ -195,7 +199,11 @@ impl<'m> CUDATileFunctionCompiler<'m> { // grid-bounded by the execution model; the residual // grid-vs-tile-count claim moves to a launch check // (goals.rs, discharge_by_block_id_axiom). - if self.discharge_by_block_id_axiom(&axis_goals, &partition_value) { + if self.discharge_by_block_id_axiom( + &axis_goals, + &partition_value, + &call_expr.span(), + ) { self.count_discharged(); continue; } @@ -208,7 +216,11 @@ impl<'m> CUDATileFunctionCompiler<'m> { // undischarged checks are compile errors rather than device // placements. if self.check_opts.relocate_to_launch - && self.hoist_zero_coordinate_nonempty_extent(&axis_goals, &partition_value) + && self.hoist_zero_coordinate_nonempty_extent( + &axis_goals, + &partition_value, + &call_expr.span(), + ) { self.count_discharged(); continue; @@ -346,7 +358,11 @@ impl<'m> CUDATileFunctionCompiler<'m> { } // Rung 3/4: a constant `[0, 0]` coordinate against a dynamic // extent reduces to `extent > 0`, a launch-known predicate. - if self.hoist_zero_coordinate_nonempty_extent(&axis_goals, &partition) { + if self.hoist_zero_coordinate_nonempty_extent( + &axis_goals, + &partition, + &call_expr.span(), + ) { self.count_discharged(); continue; } diff --git a/cutile-rs/cutile-compiler/src/compiler/compile_expression.rs b/cutile-rs/cutile-compiler/src/compiler/compile_expression.rs index 42935d6d12..b102409c7e 100644 --- a/cutile-rs/cutile-compiler/src/compiler/compile_expression.rs +++ b/cutile-rs/cutile-compiler/src/compiler/compile_expression.rs @@ -1018,7 +1018,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { "`with_bounds` bound {axis}: the partitioned extent must be divisible by the tile \ extent {tile_dim}, or the declared bound undercounts the tiles on axis {axis}" ); - if self.lower_obligation(divisibility, cause) { + if self.lower_obligation(divisibility, cause, span) { return Ok(()); } } @@ -1056,8 +1056,8 @@ impl<'m> CUDATileFunctionCompiler<'m> { (c - 1) * tile_dim, c * tile_dim ); - if self.lower_obligation(above, cause.clone()) - && self.lower_obligation(not_above, cause) + if self.lower_obligation(above, cause.clone(), span) + && self.lower_obligation(not_above, cause, span) { return Ok(()); } @@ -1357,7 +1357,13 @@ impl<'m> CUDATileFunctionCompiler<'m> { } } if let (Some(receiver_name), Some(other_name)) = (&receiver_name, &other_name) { - if self.resolve_dim_eq(receiver_name, axis, other_name, axis) { + if self.resolve_dim_eq( + receiver_name, + axis, + other_name, + axis, + &method_call.span(), + ) { self.check_stats .discharged .set(self.check_stats.discharged.get() + 1); diff --git a/cutile-rs/cutile-compiler/src/compiler/compile_inline.rs b/cutile-rs/cutile-compiler/src/compiler/compile_inline.rs index 431932948a..61406772e0 100644 --- a/cutile-rs/cutile-compiler/src/compiler/compile_inline.rs +++ b/cutile-rs/cutile-compiler/src/compiler/compile_inline.rs @@ -196,6 +196,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { } // Add function call CGAs arrays as variables. for (key, value) in call_generic_vars.ordered_inst_array() { + let _source = self.push_opaque_call_site(&fn_item.sig.span()); let arr_expr = syn::parse2::(format!("{value:?}").parse().unwrap()).unwrap(); let arr_ty = syn::parse2::(format!("[i32;{}]", value.len()).parse().unwrap()).unwrap(); @@ -433,6 +434,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { call_variables.vars.insert(key.to_string(), tr_val); } for (key, value) in call_generic_vars.ordered_inst_array() { + let _source = self.push_opaque_call_site(&impl_method.sig.span()); let arr_expr = syn::parse2::(format!("{value:?}").parse().unwrap()).unwrap(); let arr_ty = syn::parse2::(format!("[i32;{}]", value.len()).parse().unwrap()).unwrap(); diff --git a/cutile-rs/cutile-compiler/src/compiler/compile_intrinsic.rs b/cutile-rs/cutile-compiler/src/compiler/compile_intrinsic.rs index 26f633e7cb..6de924301a 100644 --- a/cutile-rs/cutile-compiler/src/compiler/compile_intrinsic.rs +++ b/cutile-rs/cutile-compiler/src/compiler/compile_intrinsic.rs @@ -1743,6 +1743,9 @@ impl<'m> CUDATileFunctionCompiler<'m> { }; // Build the reduce body region. let region_id = { + // The synthetic closure implements this reduction call; + // its parsed tokens have no independent source location. + let _source = self.push_opaque_call_site(&call_expr.span()); let local_var_types = &[ ir_operand_type.clone(), // operand_i_current_iter ir_operand_type.clone(), // operand_i_prev_iter diff --git a/cutile-rs/cutile-compiler/src/passes/obligation.rs b/cutile-rs/cutile-compiler/src/passes/obligation.rs index 864ebd3ebe..6fbc7e999f 100644 --- a/cutile-rs/cutile-compiler/src/passes/obligation.rs +++ b/cutile-rs/cutile-compiler/src/passes/obligation.rs @@ -185,6 +185,7 @@ pub(crate) fn resolve(obligation: &Obligation, assumptions: &Assumptions) -> Res Resolution::Launch(LaunchCheck { predicate: obligation.predicate.clone(), cause: obligation.cause.clone(), + origins: Vec::new(), }) } else { Resolution::Device diff --git a/cutile-rs/cutile/src/tile_kernel.rs b/cutile-rs/cutile/src/tile_kernel.rs index 503c826e95..c2007fb201 100644 --- a/cutile-rs/cutile/src/tile_kernel.rs +++ b/cutile-rs/cutile/src/tile_kernel.rs @@ -1192,12 +1192,10 @@ fn evaluate_launch_check( match check.predicate.eval(&resolve_atom) { Some(true) => Ok(()), Some(false) => Err(Error::KernelLaunch(KernelLaunchError(format!( - "launch check failed: {}", - check.cause + "launch check failed: {check}" )))), None => Err(Error::KernelLaunch(KernelLaunchError(format!( - "launch check has unresolved operands (extent unavailable at launch): {}", - check.cause + "launch check has unresolved operands (extent unavailable at launch): {check}" )))), } } @@ -2025,6 +2023,7 @@ mod launch_check_tests { fn nonzero(param: usize, axis: usize) -> LaunchCheck { LaunchCheck { + origins: Vec::new(), predicate: Predicate::nonzero(Term::atom(Atom::Dim { param, axis })), cause: "extent > 0".to_string(), } @@ -2032,6 +2031,7 @@ mod launch_check_tests { fn view_nonzero(param: usize, axis: usize) -> LaunchCheck { LaunchCheck { + origins: Vec::new(), predicate: Predicate::nonzero(Term::atom(Atom::ViewExtent { param, axis })), cause: "view extent > 0".to_string(), } diff --git a/cutile-rs/cutile/tests/fixtures/generated_debug_helpers.rs b/cutile-rs/cutile/tests/fixtures/generated_debug_helpers.rs new file mode 100644 index 0000000000..2fff23cdf5 --- /dev/null +++ b/cutile-rs/cutile/tests/fixtures/generated_debug_helpers.rs @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#[cutile::module] +pub mod generated_debug_helpers { + use cutile::core::*; + + pub fn array_function(x: i32) -> i32 { + x + VALUES[0] + VALUES[1] + } + + #[cuda_tile::variadic_trait(N = 2)] + pub trait ArrayMethod { + fn array_method(self, shape: Shape) -> i32; + } + + #[cuda_tile::variadic_trait_impl()] + #[cuda_tile::variadic_impl(N = 2)] + impl ArrayMethod for i32 { + fn array_method(self, _shape: Shape) -> i32 { + self + VALUES[0] + VALUES[1] + } + } + + pub fn nested_arrays(x: i32) -> i32 { + let a = array_function::<{ [-17, 23] }>(x); + a.array_method(shape![31, 37]) + } + + pub fn reductions(x: Tile) -> Tile { + let sum: Tile = reduce_sum(x, 0); + let product: Tile = reduce_prod(x, 0); + let minimum: Tile = reduce_min(x, 0); + let maximum: Tile = reduce_max(x, 0); + sum + product + minimum + maximum + } + + pub fn checked_load(x: &Tensor, index: i32) -> Tile { + let p = x.partition(shape![32]); + p.load([index]) + } + + pub fn first_tile(x: &Tensor) -> Tile { + let p = x.partition(shape![32]); + p.load([0]) + } + + pub fn nested_first_tile(x: &Tensor) -> Tile { + first_tile(x) + } +} diff --git a/cutile-rs/cutile/tests/generated_source_locations.rs b/cutile-rs/cutile/tests/generated_source_locations.rs new file mode 100644 index 0000000000..38cfbad458 --- /dev/null +++ b/cutile-rs/cutile/tests/generated_source_locations.rs @@ -0,0 +1,492 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Independent follow-ups to the generated-location audit (#1475). + +mod common; +#[path = "fixtures/generated_debug_helpers.rs"] +mod helpers; + +use cutile::compile_api::{CompileArtifacts, KernelCompiler}; +use cutile::tile_kernel::{CompileOptions, DebugInfoLevel}; +use cutile_ir::bytecode::Opcode; +use cutile_ir::ir::{Attribute, DebugScope, Location, Module, OpId, Operation}; + +const HELPERS: &str = include_str!("fixtures/generated_debug_helpers.rs"); +const SOURCE: &str = include_str!("generated_source_locations.rs"); + +#[cutile::module] +mod generated_kernels { + use super::helpers::generated_debug_helpers::*; + use cutile::core::*; + + #[cutile::entry] + fn arrays(out: &mut Tensor, x: i32) { + let value = nested_arrays(x) + VALUES[0] + VALUES[1]; + out.store(broadcast_scalar(value, out.shape())); + } + + #[cutile::entry] + fn reduction_source(out: &mut Tensor, x: &Tensor) { + let value = reductions(x.partition(shape![32]).load([0])); + out.store(value.reshape(shape![1])); + } + + #[cutile::entry] + fn loop_source(out: &mut Tensor, x: &Tensor, n: i32) { + for j in 0i32..n { + let tile = checked_load(x, j); + out.store(tile); + } + } + + #[cutile::entry] + fn assume_source(out: &mut Tensor, n: i32) { + for k in 0i32..n { + // SAFETY: the loop's lower bound establishes k >= 0. + let nonnegative = unsafe { assume_bounds_lower::(k) }; + out.store(broadcast_scalar(nonnegative, out.shape())); + } + } + + #[cutile::entry] + fn launch_source(out: &mut Tensor, x: &Tensor) { + let a = nested_first_tile(x); + let b = nested_first_tile(x); + out.store(a + b); + } +} + +fn line(source: &str, text: &str) -> u32 { + (source + .lines() + .position(|line| line.trim() == text) + .expect(text) + + 1) as u32 +} + +fn compile(name: &str, level: DebugInfoLevel) -> CompileArtifacts { + let mut compiler = KernelCompiler::new( + generated_kernels::__module_ast_self, + "generated_kernels", + name, + ) + .target("sm_120") + .strides(&[("out", &[1]), ("x", &[1])]) + .options(CompileOptions::new().debug_info(level)); + if name == "arrays" { + compiler = compiler.generics(vec!["41".into(), "43".into()]); + } + compiler + .compile() + .unwrap_or_else(|error| panic!("{name}: {error}")) +} + +fn visit(module: &Module, ids: &[OpId], f: &mut impl FnMut(&Operation)) { + for &id in ids { + let op = module.op(id); + f(op); + for ®ion in &op.regions { + for &block in &module.region(region).blocks { + visit(module, &module.block(block).ops, f); + } + } + } +} + +fn frames(location: &Location) -> Vec<(&str, u32, Option<&str>)> { + match location { + Location::CallSite { callee, caller } => { + let mut chain = frames(callee); + chain.extend(frames(caller)); + chain + } + Location::DebugInfo(info) => vec![( + &info.filename, + info.line, + match &info.scope { + DebugScope::Subprogram(scope) => Some(scope.name.as_str()), + _ => None, + }, + )], + Location::FileLineCol { filename, line, .. } => vec![(filename, *line, None)], + Location::Unknown => panic!("missing source origin"), + } +} + +fn assert_helper(location: &Location, function: &str, expected_line: u32, caller_line: u32) { + let chain = frames(location); + assert!( + chain[0].0.ends_with("fixtures/generated_debug_helpers.rs"), + "{chain:?}" + ); + assert_eq!(chain[0].1, expected_line, "{chain:?}"); + assert_eq!(chain[0].2, Some(function), "{chain:?}"); + assert_eq!(chain[1].1, caller_line, "{chain:?}"); + if function == "checked_load" { + assert_eq!(chain.len(), 3, "{chain:?}"); + assert!(chain[1].0.ends_with("generated_source_locations.rs")); + assert_eq!(chain[1].2, Some("loop_source")); + } else { + assert_eq!(chain.len(), 4, "{chain:?}"); + assert!(chain[1].0.ends_with("fixtures/generated_debug_helpers.rs")); + assert_eq!(chain[1].2, Some("nested_arrays")); + assert!(chain[2].0.ends_with("generated_source_locations.rs")); + assert_eq!(chain[2].2, Some("arrays")); + assert_eq!( + chain[2].1, + line( + SOURCE, + "let value = nested_arrays(x) + VALUES[0] + VALUES[1];" + ) + ); + } + assert!(chain + .last() + .unwrap() + .0 + .ends_with("generated_source_locations.rs")); +} + +#[test] +fn array_bindings_use_entry_function_and_method_definitions() { + common::with_test_stack(|| { + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = compile("arrays", level); + let mut seen = std::collections::HashSet::new(); + visit( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + if op.opcode != Opcode::Constant { + return; + } + let Some((_, Attribute::DenseElements(value))) = + op.attributes.iter().find(|(key, _)| key == "value") + else { + return; + }; + let Ok(bytes) = <[u8; 4]>::try_from(value.data.as_slice()) else { + return; + }; + let number = i32::from_le_bytes(bytes); + match number { + -17 | 23 => assert_helper(&op.location, "array_function", line(HELPERS, "pub fn array_function(x: i32) -> i32 {"), line(HELPERS, "let a = array_function::<{ [-17, 23] }>(x);")), + 31 | 37 => assert_helper(&op.location, "array_method", line(HELPERS, "fn array_method(self, _shape: Shape) -> i32 {"), line(HELPERS, "a.array_method(shape![31, 37])")), + 41 | 43 => assert_eq!(frames(&op.location)[0].1, line(SOURCE, "fn arrays(out: &mut Tensor, x: i32) {")), + _ => return, + } + seen.insert(number); + }, + ); + assert_eq!(seen, [-17, 23, 31, 37, 41, 43].into_iter().collect()); + let dump = cutile_ir::decode_bytecode(&artifacts.bytecode().unwrap()).unwrap(); + for signature in [ + "pub fn array_function(x: i32) -> i32 {", + "fn array_method(self, _shape: Shape) -> i32 {", + ] { + assert!( + dump.contains(&format!( + "generated_debug_helpers.rs\":{}:", + line(HELPERS, signature) + )), + "{dump}" + ); + } + } + }); +} + +#[test] +fn generated_reduction_regions_keep_the_call_location() { + common::with_test_stack(|| { + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = compile("reduction_source", level); + let mut reductions = 0; + visit( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + if op.opcode != Opcode::Reduce { + return; + } + reductions += 1; + let origin = &op.location; + let chain = frames(origin); + assert_eq!(chain[0].2, Some("reductions")); + let lines = [ + "let sum: Tile = reduce_sum(x, 0);", + "let product: Tile = reduce_prod(x, 0);", + "let minimum: Tile = reduce_min(x, 0);", + "let maximum: Tile = reduce_max(x, 0);", + ]; + assert!(lines.iter().any(|text| line(HELPERS, text) == chain[0].1)); + for ®ion in &op.regions { + for &block in &artifacts.module().region(region).blocks { + visit( + artifacts.module(), + &artifacts.module().block(block).ops, + &mut |body_op| assert_eq!(&body_op.location, origin), + ); + } + } + }, + ); + assert_eq!(reductions, 4); + artifacts.bytecode().unwrap(); + } + }); +} + +#[test] +fn placed_checks_keep_the_access_origin_on_their_generated_operands() { + common::with_test_stack(|| { + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = compile("loop_source", level); + let counts = artifacts.check_counts(); + assert_eq!( + (counts.hoisted, counts.in_place), + if level == DebugInfoLevel::Full { + (0, 1) + } else { + (1, 0) + } + ); + let module = artifacts.module(); + let mut ops = Vec::new(); + visit(module, &module.functions, &mut |op| ops.push(op.clone())); + let check = ops + .iter() + .find(|op| op.opcode == Opcode::Assert) + .expect("device check"); + assert_helper( + &check.location, + "checked_load", + line(HELPERS, "p.load([index])"), + line(SOURCE, "let tile = checked_load(x, j);"), + ); + // Walk the check's operand graph. Constants and comparisons in + // the generated chain must retain the same origin after folding. + let mut pending = check.operands.clone(); + let mut seen = std::collections::HashSet::new(); + let mut constants = 0; + let mut comparisons = 0; + while let Some(value) = pending.pop() { + if !seen.insert(value) { + continue; + } + let cutile_ir::ir::ValueProducer::OpResult { op, .. } = + module.value_data(value).producer + else { + continue; + }; + let producer = module.op(op); + if producer.opcode == Opcode::Constant { + if frames(&producer.location)[0] + .0 + .ends_with("generated_source_locations.rs") + && frames(&producer.location)[0].1 == line(SOURCE, "for j in 0i32..n {") + { + continue; + } + constants += 1; + assert_eq!(producer.location, check.location); + } else if producer.opcode == Opcode::CmpI { + comparisons += 1; + assert_eq!(producer.location, check.location); + } + // Pre-existing loop bounds and view/parameter computations + // have their own legitimate source origins. + if producer.location == check.location { + pending.extend_from_slice(&producer.operands); + } + } + assert!(constants > 0 && comparisons > 0); + artifacts.bytecode().unwrap(); + } + }); +} + +#[test] +fn host_check_errors_keep_all_deduplicated_inline_origins() { + common::with_test_stack(|| { + for level in [ + DebugInfoLevel::None, + DebugInfoLevel::Line, + DebugInfoLevel::Full, + ] { + let artifacts = compile("launch_source", level); + assert_eq!( + artifacts.launch_checks().len(), + 1, + "one comparison for repeated accesses" + ); + assert_eq!(artifacts.launch_checks()[0].origins.len(), 2); + let message = cutile::tile_kernel::validate_launch_checks( + artifacts.launch_checks(), + &[vec![32], vec![0]], + &[vec![32], vec![0]], + (1, 1, 1), + ) + .unwrap_err() + .to_string(); + for (file, expected) in [ + ("generated_debug_helpers.rs", line(HELPERS, "p.load([0])")), + ("generated_debug_helpers.rs", line(HELPERS, "first_tile(x)")), + ( + "generated_source_locations.rs", + line(SOURCE, "let a = nested_first_tile(x);"), + ), + ( + "generated_source_locations.rs", + line(SOURCE, "let b = nested_first_tile(x);"), + ), + ] { + assert!( + message.contains(&format!("{file}:{expected}:")), + "{message}" + ); + } + assert!( + message.contains("first_tile") && message.contains("nested_first_tile"), + "{message}" + ); + cutile::tile_kernel::validate_launch_checks( + artifacts.launch_checks(), + &[vec![32], vec![32]], + &[vec![32], vec![32]], + (1, 1, 1), + ) + .unwrap(); + let unresolved = cutile::tile_kernel::validate_launch_checks( + artifacts.launch_checks(), + &[], + &[], + (1, 1, 1), + ) + .unwrap_err() + .to_string(); + assert!(unresolved.contains("unresolved operands")); + assert!(unresolved.contains(&format!( + "generated_debug_helpers.rs:{}:", + line(HELPERS, "p.load([0])") + ))); + } + }); +} + +#[test] +fn loop_and_explicit_assumptions_keep_their_own_origins() { + common::with_test_stack(|| { + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = compile("assume_source", level); + let loop_line = line(SOURCE, "for k in 0i32..n {"); + let explicit_line = line( + SOURCE, + "let nonnegative = unsafe { assume_bounds_lower::(k) };", + ); + let mut seen = std::collections::HashSet::new(); + visit( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + if op.opcode != Opcode::Assume { + return; + } + let chain = frames(&op.location); + if chain[0].2 == Some("assume_source") { + assert!(chain[0].0.ends_with("generated_source_locations.rs")); + assert!( + [ + loop_line, + explicit_line, + line( + SOURCE, + "out.store(broadcast_scalar(nonnegative, out.shape()));" + ) + ] + .contains(&chain[0].1), + "{chain:?}" + ); + seen.insert(chain[0].1); + } + }, + ); + assert!(seen.contains(&loop_line) && seen.contains(&explicit_line)); + let dump = cutile_ir::decode_bytecode(&artifacts.bytecode().unwrap()).unwrap(); + for source_line in seen { + assert!(dump.contains(&format!("generated_source_locations.rs\":{source_line}:"))); + } + } + }); +} + +#[test] +#[ignore = "requires tileiras and nvdisasm"] +fn generated_locations_survive_assembly() { + common::with_test_stack(|| { + use cutile_compiler::cuda_tile_runtime_utils::{run_tileiras, TileirasOptions}; + use std::process::Command; + struct Directory(std::path::PathBuf); + impl Drop for Directory { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + let directory = Directory( + std::env::temp_dir().join(format!("cutile-generated-locations-{}", std::process::id())), + ); + std::fs::create_dir(&directory.0).unwrap(); + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + for name in ["arrays", "reduction_source", "loop_source"] { + let artifacts = compile(name, level); + let flags = + TileirasOptions::from_compile_options(&CompileOptions::new().debug_info(level)); + let cubin = run_tileiras(&artifacts.bytecode().unwrap(), "sm_120", &flags).unwrap(); + let path = directory.0.join(format!("{name}-{level:?}.cubin")); + std::fs::write(&path, cubin).unwrap(); + let output = Command::new("nvdisasm") + .arg("--print-line-info-inline") + .arg(&path) + .output() + .unwrap(); + assert!(output.status.success()); + let lines = String::from_utf8(output.stdout).unwrap(); + assert!( + lines.contains(if name == "arrays" { + "generated_source_locations.rs" + } else { + "generated_debug_helpers.rs" + }), + "{name} {level:?}: {lines}" + ); + // Full-debug tileiras may coalesce adjacent reduction locations; + // exact operation origins are checked before assembly above. + if name == "reduction_source" && level == DebugInfoLevel::Line { + for text in [ + "let sum: Tile = reduce_sum(x, 0);", + "let product: Tile = reduce_prod(x, 0);", + "let minimum: Tile = reduce_min(x, 0);", + "let maximum: Tile = reduce_max(x, 0);", + ] { + let expected = line(HELPERS, text).to_string(); + assert!( + lines.lines().any(|row| { + row.contains("generated_debug_helpers.rs") + && row + .split(" inlined at") + .next() + .unwrap() + .trim_end() + .ends_with(&format!("line {expected}")) + }), + "{name} {level:?}: missing line {expected}\n{lines}" + ); + } + } + } + } + }); +} diff --git a/cutile-rs/scripts/run_cpu_tests.sh b/cutile-rs/scripts/run_cpu_tests.sh index 52f457cd3c..d529110b65 100755 --- a/cutile-rs/scripts/run_cpu_tests.sh +++ b/cutile-rs/scripts/run_cpu_tests.sh @@ -57,6 +57,7 @@ for test_target in \ error_quality \ flash_attention_compile \ global_memory \ + generated_source_locations \ integer_ops \ kernel_compiler \ load_tile_like_examples \ @@ -81,6 +82,10 @@ do cargo test -p cutile --test "$test_target" done +run_step \ + "cutile generated source locations through the assembler" \ + cargo test -p cutile --test generated_source_locations generated_locations_survive_assembly -- --ignored + run_step \ "cutile compile-only control-flow regressions" \ cargo test -p cutile --test control_flow_ops compile_ From 825de3e38f6472d3c47a3d00602af8648a2eeb16 Mon Sep 17 00:00:00 2001 From: Melih Elibol <1884921+elibol@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:27:11 -0700 Subject: [PATCH 2/2] fix(cutile): keep launch-check provenance internal to the compiler Signed-off-by: Melih Elibol <1884921+elibol@users.noreply.github.com> --- cuda-async/src/predicate.rs | 42 ---- cutile-rs/CHANGELOG.md | 3 - cutile-rs/cutile-compiler/src/compile_api.rs | 2 +- .../cutile-compiler/src/compiler/_function.rs | 67 ++----- .../src/compiler/launch_checks.rs | 186 ++++++++++++++++++ cutile-rs/cutile-compiler/src/compiler/mod.rs | 1 + .../cutile-compiler/src/passes/obligation.rs | 1 - cutile-rs/cutile/src/tile_kernel.rs | 8 +- .../tests/generated_source_locations.rs | 56 +++++- 9 files changed, 254 insertions(+), 112 deletions(-) create mode 100644 cutile-rs/cutile-compiler/src/compiler/launch_checks.rs diff --git a/cuda-async/src/predicate.rs b/cuda-async/src/predicate.rs index 291e3d72ec..6b451de323 100644 --- a/cuda-async/src/predicate.rs +++ b/cuda-async/src/predicate.rs @@ -384,23 +384,6 @@ impl Predicate { } } -/// One source frame of a compiler-generated check, innermost frame first. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CheckFrame { - pub file: String, - /// One-based source line. - pub line: u32, - /// Zero-based source column. - pub column: u32, - pub function: Option, -} - -/// The original access and its inline callers, retained when a check moves. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CheckOrigin { - pub frames: Vec, -} - /// A predicate hoisted out of the device kernel to the host launcher, with a /// diagnostic `cause` rendered if it fails. Carried on the compiled kernel /// (`Validator`) and evaluated once per launch by the host's `validate_launch`. @@ -410,31 +393,6 @@ pub struct CheckOrigin { pub struct LaunchCheck { pub predicate: Predicate, pub cause: String, - /// All distinct source origins sharing this predicate. Deduplication keeps - /// one host comparison without discarding the other accesses it protects. - pub origins: Vec, -} - -impl std::fmt::Display for LaunchCheck { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.cause)?; - for origin in &self.origins { - for (index, frame) in origin.frames.iter().enumerate() { - let relation = if index == 0 { "at" } else { "inlined at" }; - write!( - f, - "\n {relation} {}:{}:{}", - frame.file, - frame.line, - frame.column + 1 - )?; - if let Some(function) = &frame.function { - write!(f, " ({function})")?; - } - } - } - Ok(()) - } } #[cfg(test)] diff --git a/cutile-rs/CHANGELOG.md b/cutile-rs/CHANGELOG.md index 7f99fbb108..3cd8f95782 100644 --- a/cutile-rs/CHANGELOG.md +++ b/cutile-rs/CHANGELOG.md @@ -17,9 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Changed -- `cuda_async::predicate::LaunchCheck` now carries `origins`. Code constructing - this struct directly must initialize that field (use an empty vector when - source information is unavailable). - cuTile Rust now lives in the [NVIDIA/cuda-rust](https://github.com/NVIDIA/cuda-rust) repository under `cutile-rs/`, next to cuda-oxide (SIMT) and the shared host crates (`cuda-bindings`, `cuda-core`, `cuda-core-derive`, `cuda-async`) at the diff --git a/cutile-rs/cutile-compiler/src/compile_api.rs b/cutile-rs/cutile-compiler/src/compile_api.rs index 31eba9b182..8f0b97ce01 100644 --- a/cutile-rs/cutile-compiler/src/compile_api.rs +++ b/cutile-rs/cutile-compiler/src/compile_api.rs @@ -302,7 +302,7 @@ impl crate::ast::Module> KernelCompiler { hoisted: compiler.check_stats.hoisted.get(), in_place: compiler.check_stats.in_place.get(), }; - let launch_checks = compiler.launch_checks.borrow().clone(); + let launch_checks = compiler.launch_checks(); Ok(CompileArtifacts { module, bytecode_version: self.bytecode_version, diff --git a/cutile-rs/cutile-compiler/src/compiler/_function.rs b/cutile-rs/cutile-compiler/src/compiler/_function.rs index 84ab7b297d..86334ddf50 100644 --- a/cutile-rs/cutile-compiler/src/compiler/_function.rs +++ b/cutile-rs/cutile-compiler/src/compiler/_function.rs @@ -9,6 +9,7 @@ //! the old CUDATileFunctionCompiler. use super::_module::CUDATileModules; +use super::launch_checks::LaunchChecks; use crate::ast::{SourceLocation, SpanBase}; use crate::bounds::Bounds; use crate::error::{JITError, SpannedJITError}; @@ -72,7 +73,7 @@ pub struct CUDATileFunctionCompiler<'m> { /// (`Resolution::Launch`). Interior-mutable because compile passes take /// `&self`; merged into the `Validator` in [`Self::get_validator`] and run /// once per launch by the host `validate_launch`. - pub(crate) launch_checks: RefCell>, + launch_checks: RefCell, pub(crate) gpu_name: String, pub(crate) target_capabilities: Option, pub(crate) optimization_hints: OptimizationHints, @@ -325,64 +326,20 @@ impl<'m> CUDATileFunctionCompiler<'m> { let obligation = Obligation::new(predicate, cause); match resolve(&obligation, &self.assumptions) { Resolution::Jit => true, - Resolution::Launch(mut check) => { - // Canonical predicates make duplicate detection exact. Two - // obligations reducing to the same host compare (e.g. a shared - // `Dim`'s divisibility, once per binding) cost one check. - let mut checks = self.launch_checks.borrow_mut(); - let origin = self.check_origin(span); - if let Some(existing) = checks.iter_mut().find(|c| c.predicate == check.predicate) { - if !origin.frames.is_empty() && !existing.origins.contains(&origin) { - existing.origins.push(origin); - } - } else { - if !origin.frames.is_empty() { - check.origins.push(origin); - } - checks.push(check); - } + Resolution::Launch(check) => { + self.launch_checks + .borrow_mut() + .insert(check, &self.ir_location(span)); true } Resolution::Device => false, } } - /// Flatten the same source/inline chain emitted into Tile IR into host - /// data. Host checks must not depend on the Tile IR crate or debug mode. - fn check_origin(&self, span: &proc_macro2::Span) -> cuda_async::predicate::CheckOrigin { - use cuda_async::predicate::{CheckFrame, CheckOrigin}; - use cutile_ir::ir::{DebugScope, Location}; - fn collect(location: &Location, frames: &mut Vec) { - match location { - Location::CallSite { callee, caller } => { - collect(callee, frames); - collect(caller, frames); - } - Location::FileLineCol { - filename, - line, - column, - } => frames.push(CheckFrame { - file: filename.clone(), - line: *line, - column: *column, - function: None, - }), - Location::DebugInfo(info) => frames.push(CheckFrame { - file: info.filename.clone(), - line: info.line, - column: info.column, - function: match &info.scope { - DebugScope::Subprogram(scope) => Some(scope.name.clone()), - _ => None, - }, - }), - Location::Unknown => {} - } - } - let mut frames = Vec::new(); - collect(&self.ir_location(span), &mut frames); - CheckOrigin { frames } + /// Export checks with their source origins rendered in the existing host + /// diagnostic field. Structured origins remain internal to the compiler. + pub(crate) fn launch_checks(&self) -> Vec { + self.launch_checks.borrow().to_host_checks() } /// The kernel-visible extent of `partition`'s axis `axis`, taken from the @@ -711,7 +668,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { assumptions, param_index, param_is_mutable, - launch_checks: RefCell::new(Vec::new()), + launch_checks: RefCell::new(LaunchChecks::default()), gpu_name, target_capabilities: None, optimization_hints, @@ -1307,7 +1264,7 @@ impl<'m> CUDATileFunctionCompiler<'m> { let mut validator = self.validator.clone(); // Surface the launch checks hoisted during compilation so the host runs // them at launch. - validator.launch_checks = self.launch_checks.borrow().clone(); + validator.launch_checks = self.launch_checks(); validator } diff --git a/cutile-rs/cutile-compiler/src/compiler/launch_checks.rs b/cutile-rs/cutile-compiler/src/compiler/launch_checks.rs new file mode 100644 index 0000000000..e6754b8e29 --- /dev/null +++ b/cutile-rs/cutile-compiler/src/compiler/launch_checks.rs @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Retain check origins while deduplicating canonical launch predicates. +//! Export them through the existing host diagnostic string, keeping source +//! metadata and Tile IR types out of the shared host API. + +use cuda_async::predicate::LaunchCheck; +use cutile_ir::ir::{DebugScope, Location}; +use std::fmt::{self, Write}; + +#[derive(Default)] +pub(super) struct LaunchChecks { + checks: Vec, +} + +struct CheckWithOrigins { + check: LaunchCheck, + origins: Vec, +} + +#[derive(PartialEq, Eq)] +struct CheckOrigin { + // Innermost frame first. Retain only the source identity, not complete + // subprogram/compilation-unit metadata for every protected access. + frames: Vec, +} + +#[derive(PartialEq, Eq)] +struct CheckFrame { + file: String, + line: u32, + column: u32, + function: Option, +} + +impl LaunchChecks { + pub(super) fn insert(&mut self, check: LaunchCheck, location: &Location) { + // Preserve the original cause and one comparison per canonical + // predicate, while retaining every distinct access/inline chain. + let existing = match self + .checks + .iter_mut() + .find(|entry| entry.check.predicate == check.predicate) + { + Some(existing) => existing, + None => { + self.checks.push(CheckWithOrigins { + check, + origins: Vec::new(), + }); + self.checks.last_mut().unwrap() + } + }; + let mut origin = CheckOrigin { frames: Vec::new() }; + origin.collect(location); + if !origin.frames.is_empty() && !existing.origins.contains(&origin) { + existing.origins.push(origin); + } + } + + pub(super) fn to_host_checks(&self) -> Vec { + self.checks + .iter() + .map(|entry| { + let mut check = entry.check.clone(); + for origin in &entry.origins { + write!(&mut check.cause, "{origin}").expect("writing to a String cannot fail"); + } + check + }) + .collect() + } +} + +impl CheckOrigin { + fn collect(&mut self, location: &Location) { + match location { + Location::CallSite { callee, caller } => { + self.collect(callee); + self.collect(caller); + } + Location::FileLineCol { + filename, + line, + column, + } => self.frames.push(CheckFrame { + file: filename.clone(), + line: *line, + column: *column, + function: None, + }), + Location::DebugInfo(info) => self.frames.push(CheckFrame { + file: info.filename.clone(), + line: info.line, + column: info.column, + function: match &info.scope { + DebugScope::Subprogram(scope) => Some(scope.name.clone()), + _ => None, + }, + }), + Location::Unknown => {} + } + } +} + +impl fmt::Display for CheckOrigin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, frame) in self.frames.iter().enumerate() { + let relation = if index == 0 { "at" } else { "inlined at" }; + write!( + f, + "\n {relation} {}:{}:{}", + frame.file, + frame.line, + u64::from(frame.column) + 1 + )?; + if let Some(function) = &frame.function { + write!(f, " ({function})")?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cuda_async::predicate::{Atom, Predicate, Term}; + + fn check(param: usize, cause: &str) -> LaunchCheck { + // The existing two-field public struct literal must remain usable + // from a downstream crate. + LaunchCheck { + predicate: Predicate::Nonzero(Term::atom(Atom::Dim { param, axis: 0 })), + cause: cause.into(), + } + } + + fn location(file: &str, line: u32, column: u32) -> Location { + Location::FileLineCol { + filename: file.into(), + line, + column, + } + } + + #[test] + fn deduplication_retains_distinct_callers_and_export_is_repeatable() { + let mut checks = LaunchChecks::default(); + for caller_line in [20, 20, 30] { + checks.insert( + check(0, "non-empty extent"), + &Location::CallSite { + callee: Box::new(location("helper.rs", 10, 2)), + caller: Box::new(location("kernel.rs", caller_line, 4)), + }, + ); + } + let exported = checks.to_host_checks(); + assert_eq!(exported.len(), 1); + assert_eq!(exported[0].predicate, check(0, "").predicate); + assert_eq!( + exported[0].cause, + "non-empty extent\n at helper.rs:10:3\n inlined at kernel.rs:20:5\n at helper.rs:10:3\n inlined at kernel.rs:30:5" + ); + assert_eq!(checks.to_host_checks(), exported); + } + + #[test] + fn unknown_origins_preserve_causes_and_distinct_predicates() { + let mut checks = LaunchChecks::default(); + checks.insert(check(0, "first extent"), &Location::Unknown); + checks.insert(check(1, "second extent"), &Location::Unknown); + assert_eq!( + checks.to_host_checks(), + vec![check(0, "first extent"), check(1, "second extent")] + ); + checks.insert(check(0, "duplicate cause"), &location("kernel.rs", 9, 0)); + let exported = checks.to_host_checks(); + assert_eq!(exported[0].cause, "first extent\n at kernel.rs:9:1"); + assert_eq!(exported[1], check(1, "second extent")); + } +} diff --git a/cutile-rs/cutile-compiler/src/compiler/mod.rs b/cutile-rs/cutile-compiler/src/compiler/mod.rs index 933dd7460a..e75516594b 100644 --- a/cutile-rs/cutile-compiler/src/compiler/mod.rs +++ b/cutile-rs/cutile-compiler/src/compiler/mod.rs @@ -23,6 +23,7 @@ mod compile_global; mod compile_inline; mod compile_intrinsic; mod compile_type; +mod launch_checks; pub mod optimization_hints; pub mod shared_types; pub mod shared_utils; diff --git a/cutile-rs/cutile-compiler/src/passes/obligation.rs b/cutile-rs/cutile-compiler/src/passes/obligation.rs index 6fbc7e999f..864ebd3ebe 100644 --- a/cutile-rs/cutile-compiler/src/passes/obligation.rs +++ b/cutile-rs/cutile-compiler/src/passes/obligation.rs @@ -185,7 +185,6 @@ pub(crate) fn resolve(obligation: &Obligation, assumptions: &Assumptions) -> Res Resolution::Launch(LaunchCheck { predicate: obligation.predicate.clone(), cause: obligation.cause.clone(), - origins: Vec::new(), }) } else { Resolution::Device diff --git a/cutile-rs/cutile/src/tile_kernel.rs b/cutile-rs/cutile/src/tile_kernel.rs index c2007fb201..503c826e95 100644 --- a/cutile-rs/cutile/src/tile_kernel.rs +++ b/cutile-rs/cutile/src/tile_kernel.rs @@ -1192,10 +1192,12 @@ fn evaluate_launch_check( match check.predicate.eval(&resolve_atom) { Some(true) => Ok(()), Some(false) => Err(Error::KernelLaunch(KernelLaunchError(format!( - "launch check failed: {check}" + "launch check failed: {}", + check.cause )))), None => Err(Error::KernelLaunch(KernelLaunchError(format!( - "launch check has unresolved operands (extent unavailable at launch): {check}" + "launch check has unresolved operands (extent unavailable at launch): {}", + check.cause )))), } } @@ -2023,7 +2025,6 @@ mod launch_check_tests { fn nonzero(param: usize, axis: usize) -> LaunchCheck { LaunchCheck { - origins: Vec::new(), predicate: Predicate::nonzero(Term::atom(Atom::Dim { param, axis })), cause: "extent > 0".to_string(), } @@ -2031,7 +2032,6 @@ mod launch_check_tests { fn view_nonzero(param: usize, axis: usize) -> LaunchCheck { LaunchCheck { - origins: Vec::new(), predicate: Predicate::nonzero(Term::atom(Atom::ViewExtent { param, axis })), cause: "view extent > 0".to_string(), } diff --git a/cutile-rs/cutile/tests/generated_source_locations.rs b/cutile-rs/cutile/tests/generated_source_locations.rs index 38cfbad458..8002d7bc6a 100644 --- a/cutile-rs/cutile/tests/generated_source_locations.rs +++ b/cutile-rs/cutile/tests/generated_source_locations.rs @@ -173,11 +173,37 @@ fn array_bindings_use_entry_function_and_method_definitions() { }; let number = i32::from_le_bytes(bytes); match number { - -17 | 23 => assert_helper(&op.location, "array_function", line(HELPERS, "pub fn array_function(x: i32) -> i32 {"), line(HELPERS, "let a = array_function::<{ [-17, 23] }>(x);")), - 31 | 37 => assert_helper(&op.location, "array_method", line(HELPERS, "fn array_method(self, _shape: Shape) -> i32 {"), line(HELPERS, "a.array_method(shape![31, 37])")), - 41 | 43 => assert_eq!(frames(&op.location)[0].1, line(SOURCE, "fn arrays(out: &mut Tensor, x: i32) {")), - _ => return, - } + -17 | 23 => assert_helper( + &op.location, + "array_function", + line( + HELPERS, + "pub fn array_function(x: i32) -> i32 {", + ), + line(HELPERS, "let a = array_function::<{ [-17, 23] }>(x);"), + ), + 31 | 37 => assert_helper( + &op.location, + "array_method", + line( + HELPERS, + "fn array_method(self, _shape: Shape) -> i32 {", + ), + line(HELPERS, "a.array_method(shape![31, 37])"), + ), + 41 | 43 => { + let chain = frames(&op.location); + assert!(chain[0].0.ends_with("generated_source_locations.rs")); + assert_eq!( + chain[0].1, + line( + SOURCE, + "fn arrays(out: &mut Tensor, x: i32) {", + ) + ); + } + _ => return, + } seen.insert(number); }, ); @@ -215,7 +241,18 @@ fn generated_reduction_regions_keep_the_call_location() { reductions += 1; let origin = &op.location; let chain = frames(origin); + assert_eq!(chain.len(), 3, "{chain:?}"); + assert!(chain[0].0.ends_with("fixtures/generated_debug_helpers.rs")); assert_eq!(chain[0].2, Some("reductions")); + assert!(chain[1].0.ends_with("generated_source_locations.rs")); + assert_eq!(chain[1].2, Some("reduction_source")); + assert_eq!( + chain[1].1, + line( + SOURCE, + "let value = reductions(x.partition(shape![32]).load([0]));" + ) + ); let lines = [ "let sum: Tile = reduce_sum(x, 0);", "let product: Tile = reduce_prod(x, 0);", @@ -323,7 +360,14 @@ fn host_check_errors_keep_all_deduplicated_inline_origins() { 1, "one comparison for repeated accesses" ); - assert_eq!(artifacts.launch_checks()[0].origins.len(), 2); + assert_eq!( + artifacts.launch_checks()[0] + .cause + .matches("\n at ") + .count(), + 2, + "both distinct access chains belong to the same host diagnostic" + ); let message = cutile::tile_kernel::validate_launch_checks( artifacts.launch_checks(), &[vec![32], vec![0]],