From 7738999685bb840cc2cb33a07723fd045962d91b Mon Sep 17 00:00:00 2001 From: Arthur031221 Date: Sun, 4 Oct 2026 04:44:08 +0800 Subject: [PATCH 1/4] fix: keep generated cuTile locations inside their containing functions Use captured module spans for source locations and the current function definition for generated types, arrays and wrapper calls. Add regressions using the assertion kernel from #1379 and a helper in another file. Co-authored-by: Melih Elibol <1884921+elibol@users.noreply.github.com> Signed-off-by: Arthur031221 --- .../cutile-compiler/src/compiler/_function.rs | 25 ++- cutile-rs/cutile/tests/debug_info.rs | 171 ++++++++++++++++++ .../cutile/tests/fixtures/debug_helpers.rs | 8 + 3 files changed, 202 insertions(+), 2 deletions(-) diff --git a/cutile-rs/cutile-compiler/src/compiler/_function.rs b/cutile-rs/cutile-compiler/src/compiler/_function.rs index 8dc61241f8..52587d9b8f 100644 --- a/cutile-rs/cutile-compiler/src/compiler/_function.rs +++ b/cutile-rs/cutile-compiler/src/compiler/_function.rs @@ -776,9 +776,25 @@ impl<'m> CUDATileFunctionCompiler<'m> { /// stack. pub(crate) fn ir_location(&self, span: &proc_macro2::Span) -> cutile_ir::ir::Location { let stack = self.call_site_stack.borrow(); + // Runtime parsing gives each captured module its own source span. + // Generated types, arrays, and wrapper calls do not join that span. + let captured_location = || { + self.modules + .name_resolver + .all_modules() + .iter() + .find_map(|(name, module)| { + module.ident.span().join(*span)?; + let base = self.modules.get_span_base(name).filter(|b| b.is_known())?; + Some(base.resolve_span(span)) + }) + }; let (caller, callee_scope, span_base) = match stack.last() { None => { - let loc = self.resolve_span(span); + let loc = captured_location().unwrap_or_else(|| { + self.span_base() + .resolve_span(&self._function.sig.ident.span()) + }); return if loc.is_known() { cutile_ir::ir::Location::FileLineCol { filename: loc.file, @@ -801,7 +817,12 @@ impl<'m> CUDATileFunctionCompiler<'m> { // callee's own module base; scope the op to the CALLEE's subprogram // (so inline debug frames name the inlined function, not the kernel // entry) and chain where it was inlined. - let loc = span_base.resolve_span(span); + let loc = captured_location().unwrap_or_else(|| match callee_scope { + Some(scope) if span_base.is_known() => { + SourceLocation::new(span_base.file.clone(), scope.line as usize, 0) + } + _ => SourceLocation::unknown(), + }); if !loc.is_known() { return cutile_ir::ir::Location::Unknown; } diff --git a/cutile-rs/cutile/tests/debug_info.rs b/cutile-rs/cutile/tests/debug_info.rs index 4b1734f85c..481c310d59 100644 --- a/cutile-rs/cutile/tests/debug_info.rs +++ b/cutile-rs/cutile/tests/debug_info.rs @@ -393,3 +393,174 @@ fn lowered_dispatch_call_keeps_its_call_site() { ); }); } + +#[cutile::module] +mod assertion_kernel { + use cutile::core::*; + + #[cutile::entry()] + fn checked_scale( + out: &mut Tensor, + x: &Tensor, + factor: f32, + ) { + validate_factor(factor); + let tile_x = x.load_like(out); + out.store(tile_x * factor.broadcast(out.shape())); + } + + fn validate_factor(factor: f32) { + assert_in_range(factor, 0.0f32, 1000.0f32); + } + + #[allow(unreachable_code, unused_variables)] + fn assert_in_range(value: f32, lo: f32, hi: f32) { + cuda_tile_assert!(value >= lo, "checked_scale: factor below the allowed range"); + cuda_tile_assert!(value <= hi, "checked_scale: factor above the allowed range"); + } +} + +fn check_generated_constant_locations(level: DebugInfoLevel) { + common::with_test_stack(move || { + let artifacts = KernelCompiler::new( + assertion_kernel::__module_ast_self, + "assertion_kernel", + "checked_scale", + ) + .target("sm_120") + .generics(vec!["128".into()]) + .strides(&[("out", &[1]), ("x", &[1])]) + .options(CompileOptions::new().debug_info(level)) + .compile() + .expect("compile assertion kernel"); + let source = include_str!("debug_info.rs"); + let kernel_line = (source + .lines() + .position(|line| line.trim_start().starts_with("fn checked_scale<")) + .unwrap() + + 1) as u32; + let mut constants = 0; + let mut invalid = Vec::new(); + visit_ops( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + if op.opcode != cutile_ir::bytecode::Opcode::Constant { + return; + } + constants += 1; + fn valid_location(loc: &Location, kernel_line: u32) -> bool { + match loc { + Location::FileLineCol { line, .. } => *line >= kernel_line, + Location::DebugInfo(info) => match &info.scope { + DebugScope::Subprogram(scope) => info.line >= scope.line, + _ => false, + }, + Location::CallSite { callee, caller } => { + valid_location(callee, kernel_line) + && valid_location(caller, kernel_line) + } + _ => false, + } + } + let valid = valid_location(&op.location, kernel_line); + if !valid { + invalid.push(format!("{:?}", op.location)); + } + }, + ); + println!( + "{level:?}: {constants} constants, {} locations before their function", + invalid.len() + ); + assert!(constants > 0, "expected generated constants"); + assert!( + invalid.is_empty(), + "constants attributed before their function: {invalid:#?}" + ); + }); +} + +#[test] +fn generated_constants_keep_function_locations_line() { + check_generated_constant_locations(DebugInfoLevel::Line); +} + +#[test] +fn generated_constants_keep_function_locations_full() { + check_generated_constant_locations(DebugInfoLevel::Full); +} + +#[cutile::module] +mod generated_helper_kernel { + use super::debug_helpers_file::debug_helpers::generated; + use cutile::core::*; + + #[cutile::entry()] + fn generated_source(out: &mut Tensor, x: i32) { + let value = generated::<{ [-17, 23] }, true>(x); + out.store(value.broadcast(out.shape())); + } +} + +#[test] +fn generated_helper_constants_keep_definition_file_and_line() { + common::with_test_stack(|| { + let artifacts = KernelCompiler::new( + generated_helper_kernel::__module_ast_self, + "generated_helper_kernel", + "generated_source", + ) + .target("sm_120") + .generics(vec!["32".into()]) + .strides(&[("out", &[1])]) + .options(CompileOptions::new().debug_info(DebugInfoLevel::Full)) + .compile() + .expect("compile generic helper"); + let helper_source = include_str!("fixtures/debug_helpers.rs"); + let definition_line = (helper_source + .lines() + .position(|line| line.trim_start().starts_with("pub fn generated<")) + .unwrap() + + 1) as u32; + let mut generated = 0; + visit_ops( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + if op.opcode != cutile_ir::bytecode::Opcode::Constant { + return; + } + let Some((_, cutile_ir::ir::Attribute::DenseElements(value))) = + op.attributes.iter().find(|(name, _)| name == "value") + else { + return; + }; + let is_generated = + value.data == (-17i32).to_le_bytes() || value.data == 23i32.to_le_bytes(); + if !is_generated { + return; + } + let Location::CallSite { callee, .. } = &op.location else { + return; + }; + let Location::DebugInfo(info) = &**callee else { + panic!("missing helper scope: {callee:?}"); + }; + let DebugScope::Subprogram(scope) = &info.scope else { + panic!("missing helper subprogram: {info:?}"); + }; + if scope.name != "generated" { + return; + } + assert!( + info.filename.ends_with("fixtures/debug_helpers.rs"), + "{info:?}" + ); + generated += 1; + assert_eq!(info.line, definition_line, "{info:?}"); + }, + ); + assert!(generated >= 2, "missing generated array constants"); + }); +} diff --git a/cutile-rs/cutile/tests/fixtures/debug_helpers.rs b/cutile-rs/cutile/tests/fixtures/debug_helpers.rs index edf0ec2a5a..60ef186624 100644 --- a/cutile-rs/cutile/tests/fixtures/debug_helpers.rs +++ b/cutile-rs/cutile/tests/fixtures/debug_helpers.rs @@ -13,6 +13,14 @@ pub mod debug_helpers { offset(x) * 2 } + pub fn generated(x: i32) -> i32 { + if FLAG { + x + VALUES[0] + VALUES[1] + } else { + x + } + } + pub trait Bump { fn bump(self) -> i32; } From a450156dac49280e0f1c037fb4b82191e7d09fd7 Mon Sep 17 00:00:00 2001 From: Arthur031221 Date: Thu, 22 Oct 2026 13:55:28 +0800 Subject: [PATCH 2/4] test: pin the locations of generated cuTile constants Check the file, definition line, subprogram and caller chain of the generated constants in the assertion kernel and in a nested cross-file helper, in Line and Full, and that the same locations are present in the serialized bytecode. A user literal in the same kernel must keep its own line. An opt-in test checks the cubin line table with nvdisasm and DWARF. Signed-off-by: Arthur031221 --- cutile-rs/cutile/tests/debug_info.rs | 467 ++++++++++++++---- .../cutile/tests/fixtures/debug_helpers.rs | 4 + 2 files changed, 374 insertions(+), 97 deletions(-) diff --git a/cutile-rs/cutile/tests/debug_info.rs b/cutile-rs/cutile/tests/debug_info.rs index 481c310d59..7ec0098fa8 100644 --- a/cutile-rs/cutile/tests/debug_info.rs +++ b/cutile-rs/cutile/tests/debug_info.rs @@ -420,6 +420,140 @@ mod assertion_kernel { } } +/// Renders a location as `callee <- caller`, each frame as +/// `subprogram[definition line]@file:line`. The outermost frame of a kernel is +/// rendered as `file:line`. +fn describe(location: &Location) -> String { + let base = |file: &str| file.rsplit('/').next().unwrap().to_string(); + match location { + Location::FileLineCol { filename, line, .. } => format!("{}:{line}", base(filename)), + Location::DebugInfo(info) => match &info.scope { + DebugScope::Subprogram(scope) => format!( + "{}[{}]@{}:{}", + scope.name, + scope.line, + base(&info.filename), + info.line + ), + other => panic!("unexpected scope {other:?}"), + }, + Location::CallSite { callee, caller } => { + format!("{} <- {}", describe(callee), describe(caller)) + } + other => panic!("unexpected location {other:?}"), + } +} + +/// Every constant in the module as (little-endian value bytes, described location). +fn constant_locations(artifacts: &CompileArtifacts) -> Vec<(Vec, String)> { + let mut constants = Vec::new(); + visit_ops( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + if op.opcode != cutile_ir::bytecode::Opcode::Constant { + return; + } + let Some((_, cutile_ir::ir::Attribute::DenseElements(value))) = + op.attributes.iter().find(|(name, _)| name == "value") + else { + panic!("constant without a dense value: {op:?}"); + }; + constants.push((value.data.clone(), describe(&op.location))); + }, + ); + constants +} + +/// The set of locations stored in the serialized debug section, rendered like `describe`. +fn decoded_locations(bytecode: &[u8]) -> std::collections::HashSet { + use std::collections::HashMap; + fn field<'a>(entry: &'a str, key: &str) -> &'a str { + let start = entry.find(&format!("{key}=")).unwrap() + key.len() + 1; + let rest = &entry[start..]; + match rest.strip_prefix('"') { + Some(quoted) => "ed[..quoted.find('"').unwrap()], + None => &rest[..rest.find([',', ')']).unwrap()], + } + } + let dump = cutile_ir::decode_bytecode(bytecode).expect("decode"); + let debug = dump.split("=== Debug").nth(1).expect("debug section"); + let mut entries: HashMap<&str, &str> = HashMap::new(); + for line in debug + .lines() + .map(str::trim_start) + .filter(|line| line.starts_with("di[")) + { + let (id, body) = line.split_once(" = ").unwrap(); + entries.insert(id, body); + } + fn render(id: &str, entries: &HashMap<&str, &str>) -> String { + let body = entries[id]; + if body.starts_with("CallSite(") { + return format!( + "{} <- {}", + render(field(body, "callee"), entries), + render(field(body, "caller"), entries) + ); + } + let scope = entries[field(body, "scope")]; + let quoted = body.split('"').nth(1).unwrap(); + let line = body.split('"').nth(2).unwrap()[1..] + .split(':') + .next() + .unwrap(); + let file = quoted.rsplit('/').next().unwrap(); + if field(scope, "linkage").ends_with("_entry") { + format!("{file}:{line}") + } else { + format!( + "{}[{}]@{file}:{line}", + field(scope, "name"), + field(scope, "line") + ) + } + } + entries + .iter() + .filter(|(_, body)| body.starts_with("DILoc(") || body.starts_with("CallSite(")) + .map(|(id, _)| render(id, &entries)) + .collect() +} + +/// Checks the exact location of the selected constants, rejects any location +/// outside the allowed set, and checks the same locations after serialization. +fn check_constant_locations( + artifacts: &CompileArtifacts, + allowed: &[String], + required: &[(Vec, String)], +) { + let constants = constant_locations(artifacts); + assert!(!constants.is_empty(), "expected generated constants"); + let unexpected: Vec<_> = constants + .iter() + .filter(|(_, location)| !allowed.contains(location)) + .collect(); + assert!( + unexpected.is_empty(), + "unexpected locations: {unexpected:#?}" + ); + for (value, location) in required { + assert!( + constants + .iter() + .any(|(bytes, found)| bytes == value && found == location), + "no constant {value:?} at {location}, found {constants:#?}" + ); + } + let decoded = decoded_locations(&artifacts.bytecode().expect("serialize")); + for (value, location) in &constants { + assert!( + decoded.contains(location), + "location of constant {value:?} lost in serialization: {location}\n{decoded:#?}" + ); + } +} + fn check_generated_constant_locations(level: DebugInfoLevel) { common::with_test_stack(move || { let artifacts = KernelCompiler::new( @@ -434,50 +568,43 @@ fn check_generated_constant_locations(level: DebugInfoLevel) { .compile() .expect("compile assertion kernel"); let source = include_str!("debug_info.rs"); - let kernel_line = (source - .lines() - .position(|line| line.trim_start().starts_with("fn checked_scale<")) - .unwrap() - + 1) as u32; - let mut constants = 0; - let mut invalid = Vec::new(); - visit_ops( - artifacts.module(), - &artifacts.module().functions, - &mut |op| { - if op.opcode != cutile_ir::bytecode::Opcode::Constant { - return; - } - constants += 1; - fn valid_location(loc: &Location, kernel_line: u32) -> bool { - match loc { - Location::FileLineCol { line, .. } => *line >= kernel_line, - Location::DebugInfo(info) => match &info.scope { - DebugScope::Subprogram(scope) => info.line >= scope.line, - _ => false, - }, - Location::CallSite { callee, caller } => { - valid_location(callee, kernel_line) - && valid_location(caller, kernel_line) - } - _ => false, - } - } - let valid = valid_location(&op.location, kernel_line); - if !valid { - invalid.push(format!("{:?}", op.location)); - } - }, - ); - println!( - "{level:?}: {constants} constants, {} locations before their function", - invalid.len() + let kernel = source_line(source, "fn checked_scale("); + let validator = source_line(source, "fn validate_factor(factor: f32) {"); + let validate_call = source_line(source, "validate_factor(factor);"); + let range_call = source_line(source, "assert_in_range(factor, 0.0f32, 1000.0f32);"); + let load = source_line(source, "let tile_x = x.load_like(out);"); + let store = source_line(source, "out.store(tile_x * factor.broadcast(out.shape()));"); + let root = format!("debug_info.rs:{kernel}"); + // Generated constants without a source span fall back to the kernel definition. + let fallback = format!("checked_scale[{kernel}]@debug_info.rs:{kernel} <- {root}"); + let statement = + |line: u32| format!("checked_scale[{kernel}]@debug_info.rs:{line} <- {root}"); + // The literals written in the user's helper keep their own line and caller chain. + let literal = format!( + "validate_factor[{validator}]@debug_info.rs:{range_call} <- \ + checked_scale[{kernel}]@debug_info.rs:{validate_call} <- {root}" ); - assert!(constants > 0, "expected generated constants"); - assert!( - invalid.is_empty(), - "constants attributed before their function: {invalid:#?}" + let allowed = [ + root.clone(), + fallback.clone(), + statement(load), + statement(store), + literal.clone(), + ]; + check_constant_locations( + &artifacts, + &allowed, + &[ + (128i32.to_le_bytes().to_vec(), fallback), + (0.0f32.to_le_bytes().to_vec(), literal.clone()), + (1000.0f32.to_le_bytes().to_vec(), literal.clone()), + ], ); + for (value, location) in constant_locations(&artifacts) { + if value == 1000.0f32.to_le_bytes() { + assert_eq!(location, literal, "user literal replaced by a fallback"); + } + } }); } @@ -503,64 +630,210 @@ mod generated_helper_kernel { } } +#[cutile::module] +mod nested_helper_kernel { + use super::debug_helpers_file::debug_helpers::generated_nested; + use cutile::core::*; + + #[cutile::entry()] + fn nested_source(out: &mut Tensor, x: i32) { + let total = generated_nested::<{ [-17, 23] }>(x); + out.store(total.broadcast(out.shape())); + } +} + +fn check_cross_file_constant_locations(level: DebugInfoLevel, nested: bool) { + common::with_test_stack(move || { + macro_rules! compile_helper { + ($module:ident, $entry:literal) => { + KernelCompiler::new($module::__module_ast_self, stringify!($module), $entry) + .target("sm_120") + .generics(vec!["32".into()]) + .strides(&[("out", &[1])]) + .options(CompileOptions::new().debug_info(level)) + .compile() + .expect("compile cross-file helper") + }; + } + let artifacts = if nested { + compile_helper!(nested_helper_kernel, "nested_source") + } else { + compile_helper!(generated_helper_kernel, "generated_source") + }; + let source = include_str!("debug_info.rs"); + let helpers = include_str!("fixtures/debug_helpers.rs"); + let (name, signature, call, store) = if nested { + ( + "nested_source", + "fn nested_source(out: &mut Tensor, x: i32) {", + "let total = generated_nested::<{ [-17, 23] }>(x);", + "out.store(total.broadcast(out.shape()));", + ) + } else { + ( + "generated_source", + "fn generated_source(out: &mut Tensor, x: i32) {", + "let value = generated::<{ [-17, 23] }, true>(x);", + "out.store(value.broadcast(out.shape()));", + ) + }; + let kernel = source_line(source, signature); + let call_line = source_line(source, call); + let store = source_line(source, store); + let definition = source_line( + helpers, + "pub fn generated(x: i32) -> i32 {", + ); + let indexing = source_line(helpers, "x + VALUES[0] + VALUES[1]"); + let root = format!("debug_info.rs:{kernel}"); + let fallback = format!("{name}[{kernel}]@debug_info.rs:{kernel} <- {root}"); + let call_site = format!("{name}[{kernel}]@debug_info.rs:{call_line} <- {root}"); + let store_site = format!("{name}[{kernel}]@debug_info.rs:{store} <- {root}"); + let mut allowed = vec![root, fallback, store_site]; + let mut required = Vec::new(); + // Caller chain of the inner `generated` call, as seen by its constants. + let inner_caller = if nested { + let nested_definition = source_line( + helpers, + "pub fn generated_nested(x: i32) -> i32 {", + ); + let nested_call = source_line(helpers, "generated::(x) + 1"); + let frame = |line: u32| { + format!( + "generated_nested[{nested_definition}]@debug_helpers.rs:{line} <- {call_site}" + ) + }; + let outer_values = frame(nested_definition); + let outer_call = frame(nested_call); + for value in [-17i32, 23] { + required.push((value.to_le_bytes().to_vec(), outer_values.clone())); + } + // The `+ 1` written in the outer helper keeps the outer helper's own line. + required.push((1i32.to_le_bytes().to_vec(), outer_call.clone())); + allowed.push(outer_values); + allowed.push(outer_call.clone()); + outer_call + } else { + call_site + }; + let values = + format!("generated[{definition}]@debug_helpers.rs:{definition} <- {inner_caller}"); + let index = + format!("generated[{definition}]@debug_helpers.rs:{indexing} <- {inner_caller}"); + for value in [-17i32, 23] { + required.push((value.to_le_bytes().to_vec(), values.clone())); + } + for value in [0i32, 1] { + required.push((value.to_le_bytes().to_vec(), index.clone())); + } + allowed.push(values); + allowed.push(index); + check_constant_locations(&artifacts, &allowed, &required); + }); +} + +#[test] +fn generated_helper_constants_keep_definition_at_line_level() { + check_cross_file_constant_locations(DebugInfoLevel::Line, false); +} + +#[test] +fn generated_helper_constants_keep_definition_at_full_level() { + check_cross_file_constant_locations(DebugInfoLevel::Full, false); +} + +#[test] +fn nested_helper_constants_keep_caller_chain_at_line_level() { + check_cross_file_constant_locations(DebugInfoLevel::Line, true); +} + +#[test] +fn nested_helper_constants_keep_caller_chain_at_full_level() { + check_cross_file_constant_locations(DebugInfoLevel::Full, true); +} + +// Opt-in, like `cubin_has_rust_lines_and_inline_frames`: the line table the +// device compiler emits for generated constants must stay inside the kernel +// and the helpers it calls. #[test] -fn generated_helper_constants_keep_definition_file_and_line() { +#[ignore = "requires tileiras, nvdisasm, and readelf"] +fn nested_helper_cubin_lines_stay_inside_their_functions() { common::with_test_stack(|| { - let artifacts = KernelCompiler::new( - generated_helper_kernel::__module_ast_self, - "generated_helper_kernel", - "generated_source", - ) - .target("sm_120") - .generics(vec!["32".into()]) - .strides(&[("out", &[1])]) - .options(CompileOptions::new().debug_info(DebugInfoLevel::Full)) - .compile() - .expect("compile generic helper"); - let helper_source = include_str!("fixtures/debug_helpers.rs"); - let definition_line = (helper_source - .lines() - .position(|line| line.trim_start().starts_with("pub fn generated<")) - .unwrap() - + 1) as u32; - let mut generated = 0; - visit_ops( - artifacts.module(), - &artifacts.module().functions, - &mut |op| { - if op.opcode != cutile_ir::bytecode::Opcode::Constant { - return; - } - let Some((_, cutile_ir::ir::Attribute::DenseElements(value))) = - op.attributes.iter().find(|(name, _)| name == "value") - else { - return; - }; - let is_generated = - value.data == (-17i32).to_le_bytes() || value.data == 23i32.to_le_bytes(); - if !is_generated { - return; + use cutile_compiler::cuda_tile_runtime_utils::{run_tileiras, TileirasOptions}; + let source = include_str!("debug_info.rs"); + let helpers = include_str!("fixtures/debug_helpers.rs"); + let kernel = source_line( + source, + "fn nested_source(out: &mut Tensor, x: i32) {", + ); + let store = source_line(source, "out.store(total.broadcast(out.shape()));"); + let first_helper = source_line( + helpers, + "pub fn generated(x: i32) -> i32 {", + ); + let last_helper = source_line(helpers, "generated::(x) + 1"); + let directory = CubinDirectory::new(); + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = KernelCompiler::new( + nested_helper_kernel::__module_ast_self, + "nested_helper_kernel", + "nested_source", + ) + .target("sm_120") + .generics(vec!["32".into()]) + .strides(&[("out", &[1])]) + .options(CompileOptions::new().debug_info(level)) + .compile() + .expect("compile nested helper kernel"); + let flags = + TileirasOptions::from_compile_options(&CompileOptions::new().debug_info(level)); + let cubin = run_tileiras(&artifacts.bytecode().unwrap(), "sm_120", &flags) + .expect("tileiras must accept the generated locations"); + let path = directory.0.join(format!("{level:?}.cubin")); + std::fs::write(&path, cubin).unwrap(); + let sass = output(Command::new("nvdisasm").arg("--print-line-info").arg(&path)); + let mut kernel_lines = Vec::new(); + let mut helper_lines = Vec::new(); + for entry in sass + .lines() + .filter_map(|line| line.trim().strip_prefix("//## File \"")) + { + let (file, line) = entry.split_once("\", line ").unwrap(); + let line: u32 = line.trim().parse().unwrap(); + if file.ends_with("fixtures/debug_helpers.rs") { + helper_lines.push(line); + } else if file.ends_with("tests/debug_info.rs") { + kernel_lines.push(line); + } else { + panic!("line entry outside the kernel and its helpers: {entry}"); } - let Location::CallSite { callee, .. } = &op.location else { - return; - }; - let Location::DebugInfo(info) = &**callee else { - panic!("missing helper scope: {callee:?}"); - }; - let DebugScope::Subprogram(scope) = &info.scope else { - panic!("missing helper subprogram: {info:?}"); - }; - if scope.name != "generated" { - return; + } + assert!( + !kernel_lines.is_empty(), + "{level:?}: no kernel lines\n{sass}" + ); + assert!( + !helper_lines.is_empty(), + "{level:?}: no helper lines\n{sass}" + ); + assert!( + kernel_lines + .iter() + .all(|line| (kernel..=store).contains(line)), + "{level:?}: kernel lines {kernel_lines:?} outside {kernel}..={store}\n{sass}" + ); + assert!( + helper_lines + .iter() + .all(|line| (first_helper..=last_helper).contains(line)), + "{level:?}: helper lines {helper_lines:?} outside {first_helper}..={last_helper}\n{sass}" + ); + if level == DebugInfoLevel::Full { + let dwarf = output(Command::new("readelf").arg("--debug-dump=info").arg(&path)); + for name in ["nested_source", "generated_nested"] { + assert!(dwarf.contains(name), "missing {name} in {dwarf}"); } - assert!( - info.filename.ends_with("fixtures/debug_helpers.rs"), - "{info:?}" - ); - generated += 1; - assert_eq!(info.line, definition_line, "{info:?}"); - }, - ); - assert!(generated >= 2, "missing generated array constants"); + } + } }); } diff --git a/cutile-rs/cutile/tests/fixtures/debug_helpers.rs b/cutile-rs/cutile/tests/fixtures/debug_helpers.rs index 60ef186624..8bffc42c7d 100644 --- a/cutile-rs/cutile/tests/fixtures/debug_helpers.rs +++ b/cutile-rs/cutile/tests/fixtures/debug_helpers.rs @@ -21,6 +21,10 @@ pub mod debug_helpers { } } + pub fn generated_nested(x: i32) -> i32 { + generated::(x) + 1 + } + pub trait Bump { fn bump(self) -> i32; } From 933e2216fa5e958076012e7088c7181f8f37deba Mon Sep 17 00:00:00 2001 From: Melih Elibol <1884921+elibol@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:18:51 -0700 Subject: [PATCH 3/4] test(cutile): isolate known tileiras 13.4.92 debug loss Signed-off-by: Melih Elibol <1884921+elibol@users.noreply.github.com> --- .../guide/debugging-and-profiling.md | 8 +++ cutile-rs/cutile/tests/debug_info.rs | 54 ++++++++++++++++--- cutile-rs/scripts/run_cpu_tests.sh | 2 +- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/cutile-rs/cutile-book/guide/debugging-and-profiling.md b/cutile-rs/cutile-book/guide/debugging-and-profiling.md index 32e88001d6..399fcb2799 100644 --- a/cutile-rs/cutile-book/guide/debugging-and-profiling.md +++ b/cutile-rs/cutile-book/guide/debugging-and-profiling.md @@ -230,6 +230,14 @@ 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. +Known toolkit limitation: tileiras **13.4.92** can replace Rust helper locations +with generated Tile IR locations and omit inline frames, even when the frontend +bytecode contains the correct source information. This affects source correlation +and helper backtraces. The same bytecode retains those locations with tileiras +13.3.36. CI reports this exact backend limitation while continuing to +check frontend origins and surviving kernel locations; it does not waive checks +for other releases, including 13.5. See [#1385](https://github.com/NVIDIA/cuda-rust/pull/1385). + 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. diff --git a/cutile-rs/cutile/tests/debug_info.rs b/cutile-rs/cutile/tests/debug_info.rs index 7ec0098fa8..6a26f9fdd2 100644 --- a/cutile-rs/cutile/tests/debug_info.rs +++ b/cutile-rs/cutile/tests/debug_info.rs @@ -752,14 +752,40 @@ fn nested_helper_constants_keep_caller_chain_at_full_level() { check_cross_file_constant_locations(DebugInfoLevel::Full, true); } +// Restrict the backend exception to the exact release reproduced in #1385. +// In particular, 13.5 must establish its own profiling/debugging behavior. +fn has_known_inline_debug_loss(fingerprint: &str) -> bool { + fingerprint + .lines() + .any(|line| line.trim() == "Cuda compilation tools, release 13.4, V13.4.92") +} + +#[test] +fn inline_debug_exception_does_not_cover_other_toolkit_versions() { + for (version, expected) in [ + ("13.3, V13.3.36", false), + ("13.4, V13.4.92", true), + ("13.4, V13.4.93", false), + ("13.5, V13.5.92", false), + ] { + let banner = format!("tileiras\nCuda compilation tools, release {version}\n"); + assert_eq!(has_known_inline_debug_loss(&banner), expected); + } + assert!(!has_known_inline_debug_loss("unknown assembler")); +} + // Opt-in, like `cubin_has_rust_lines_and_inline_frames`: the line table the // device compiler emits for generated constants must stay inside the kernel -// and the helpers it calls. +// and the helpers it calls. The exact IR/bytecode origins are checked above +// on every toolkit; only the known 13.4.92 backend loss is accounted for here. #[test] #[ignore = "requires tileiras, nvdisasm, and readelf"] fn nested_helper_cubin_lines_stay_inside_their_functions() { common::with_test_stack(|| { - use cutile_compiler::cuda_tile_runtime_utils::{run_tileiras, TileirasOptions}; + use cutile_compiler::cuda_tile_runtime_utils::{ + run_tileiras, tileiras_fingerprint, TileirasOptions, + }; + let known_debug_loss = has_known_inline_debug_loss(tileiras_fingerprint()); let source = include_str!("debug_info.rs"); let helpers = include_str!("fixtures/debug_helpers.rs"); let kernel = source_line( @@ -794,6 +820,7 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { let sass = output(Command::new("nvdisasm").arg("--print-line-info").arg(&path)); let mut kernel_lines = Vec::new(); let mut helper_lines = Vec::new(); + let mut generated_lines = Vec::new(); for entry in sass .lines() .filter_map(|line| line.trim().strip_prefix("//## File \"")) @@ -804,6 +831,10 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { helper_lines.push(line); } else if file.ends_with("tests/debug_info.rs") { kernel_lines.push(line); + } else if known_debug_loss && file.starts_with("./tile-ir-") { + // 13.4.92 substitutes internal Tile IR locations for Rust + // helper locations. Do not accept arbitrary foreign files. + generated_lines.push(line); } else { panic!("line entry outside the kernel and its helpers: {entry}"); } @@ -813,7 +844,7 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { "{level:?}: no kernel lines\n{sass}" ); assert!( - !helper_lines.is_empty(), + !helper_lines.is_empty() || known_debug_loss, "{level:?}: no helper lines\n{sass}" ); assert!( @@ -828,11 +859,22 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { .all(|line| (first_helper..=last_helper).contains(line)), "{level:?}: helper lines {helper_lines:?} outside {first_helper}..={last_helper}\n{sass}" ); + let mut missing_inline_scope = false; if level == DebugInfoLevel::Full { let dwarf = output(Command::new("readelf").arg("--debug-dump=info").arg(&path)); - for name in ["nested_source", "generated_nested"] { - assert!(dwarf.contains(name), "missing {name} in {dwarf}"); - } + assert!(dwarf.contains("nested_source"), "{dwarf}"); + missing_inline_scope = !dwarf.contains("generated_nested"); + assert!(!missing_inline_scope || known_debug_loss, "{dwarf}"); + } + if known_debug_loss + && (helper_lines.is_empty() || !generated_lines.is_empty() || missing_inline_scope) + { + eprintln!( + "::warning::tileiras 13.4.92 has known inline-debug loss ({level:?}); \ + surviving Rust locations were checked and frontend/bytecode checks remain strict, \ + but helper locations/frames are not established by this backend. \ + See NVIDIA/cuda-rust#1385. Other versions remain strict." + ); } } }); diff --git a/cutile-rs/scripts/run_cpu_tests.sh b/cutile-rs/scripts/run_cpu_tests.sh index d80021a9d7..09f2ea3516 100755 --- a/cutile-rs/scripts/run_cpu_tests.sh +++ b/cutile-rs/scripts/run_cpu_tests.sh @@ -84,7 +84,7 @@ done run_step \ "cutile debug provenance through the assembler" \ - cargo test -p cutile --test debug_info nested_helper_cubin_lines_stay_inside_their_functions -- --ignored + cargo test -p cutile --test debug_info nested_helper_cubin_lines_stay_inside_their_functions -- --ignored --nocapture run_step \ "cutile generated source locations through the assembler" \ From b25b4fe275ce943e3fb33aca5f0f53db65fa3c53 Mon Sep 17 00:00:00 2001 From: Melih Elibol <1884921+elibol@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:41:04 -0700 Subject: [PATCH 4/4] test(cutile): recognize the CI assembler debug limitation Signed-off-by: Melih Elibol <1884921+elibol@users.noreply.github.com> --- .../guide/debugging-and-profiling.md | 3 ++- cutile-rs/cutile/tests/debug_info.rs | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cutile-rs/cutile-book/guide/debugging-and-profiling.md b/cutile-rs/cutile-book/guide/debugging-and-profiling.md index 399fcb2799..fdda6386f8 100644 --- a/cutile-rs/cutile-book/guide/debugging-and-profiling.md +++ b/cutile-rs/cutile-book/guide/debugging-and-profiling.md @@ -230,7 +230,8 @@ 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. -Known toolkit limitation: tileiras **13.4.92** can replace Rust helper locations +Known toolkit limitation: tileiras **13.4.59** (in the CUDA 13.4.1 CI image) and +**13.4.92** can replace Rust helper locations with generated Tile IR locations and omit inline frames, even when the frontend bytecode contains the correct source information. This affects source correlation and helper backtraces. The same bytecode retains those locations with tileiras diff --git a/cutile-rs/cutile/tests/debug_info.rs b/cutile-rs/cutile/tests/debug_info.rs index 6a26f9fdd2..1975c35ae9 100644 --- a/cutile-rs/cutile/tests/debug_info.rs +++ b/cutile-rs/cutile/tests/debug_info.rs @@ -752,18 +752,24 @@ fn nested_helper_constants_keep_caller_chain_at_full_level() { check_cross_file_constant_locations(DebugInfoLevel::Full, true); } -// Restrict the backend exception to the exact release reproduced in #1385. +// Restrict the backend exception to the exact releases reproduced in #1385. // In particular, 13.5 must establish its own profiling/debugging behavior. fn has_known_inline_debug_loss(fingerprint: &str) -> bool { - fingerprint - .lines() - .any(|line| line.trim() == "Cuda compilation tools, release 13.4, V13.4.92") + fingerprint.lines().any(|line| { + matches!( + line.trim(), + "Cuda compilation tools, release 13.4, V13.4.59" + | "Cuda compilation tools, release 13.4, V13.4.92" + ) + }) } #[test] fn inline_debug_exception_does_not_cover_other_toolkit_versions() { for (version, expected) in [ ("13.3, V13.3.36", false), + ("13.4, V13.4.59", true), + ("13.4, V13.4.60", false), ("13.4, V13.4.92", true), ("13.4, V13.4.93", false), ("13.5, V13.5.92", false), @@ -777,7 +783,7 @@ fn inline_debug_exception_does_not_cover_other_toolkit_versions() { // Opt-in, like `cubin_has_rust_lines_and_inline_frames`: the line table the // device compiler emits for generated constants must stay inside the kernel // and the helpers it calls. The exact IR/bytecode origins are checked above -// on every toolkit; only the known 13.4.92 backend loss is accounted for here. +// on every toolkit; only the known 13.4.59/13.4.92 backend loss is accounted for here. #[test] #[ignore = "requires tileiras, nvdisasm, and readelf"] fn nested_helper_cubin_lines_stay_inside_their_functions() { @@ -785,7 +791,9 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { use cutile_compiler::cuda_tile_runtime_utils::{ run_tileiras, tileiras_fingerprint, TileirasOptions, }; - let known_debug_loss = has_known_inline_debug_loss(tileiras_fingerprint()); + let fingerprint = tileiras_fingerprint(); + eprintln!("Assembler for debug provenance checks:\n{fingerprint}"); + let known_debug_loss = has_known_inline_debug_loss(fingerprint); let source = include_str!("debug_info.rs"); let helpers = include_str!("fixtures/debug_helpers.rs"); let kernel = source_line( @@ -832,8 +840,8 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { } else if file.ends_with("tests/debug_info.rs") { kernel_lines.push(line); } else if known_debug_loss && file.starts_with("./tile-ir-") { - // 13.4.92 substitutes internal Tile IR locations for Rust - // helper locations. Do not accept arbitrary foreign files. + // These assembler builds substitute internal Tile IR locations + // for Rust helper locations. Do not accept arbitrary foreign files. generated_lines.push(line); } else { panic!("line entry outside the kernel and its helpers: {entry}"); @@ -870,7 +878,7 @@ fn nested_helper_cubin_lines_stay_inside_their_functions() { && (helper_lines.is_empty() || !generated_lines.is_empty() || missing_inline_scope) { eprintln!( - "::warning::tileiras 13.4.92 has known inline-debug loss ({level:?}); \ + "::warning::the active tileiras has known inline-debug loss ({level:?}); \ surviving Rust locations were checked and frontend/bytecode checks remain strict, \ but helper locations/frames are not established by this backend. \ See NVIDIA/cuda-rust#1385. Other versions remain strict."