Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions cutile-rs/cutile-book/guide/debugging-and-profiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ debug = false
Cargo provides only `DEBUG=true|false` to build
scripts, not the exact debug level. Any enabled level currently selects
device-debug mode, including `debug = "line-tables-only"` and `"limited"`.
It also selects device optimization level 0 unless explicitly overridden,
even in a release profile with `debug = true`. Automatic line-only mapping
Device-debug mode compiles at optimization level 0, even in a release
profile with `debug = true`. Automatic line-only mapping
is not implemented. Use the explicit `line` override below for optimized
profiling. See Cargo's [profile settings][cargo-profiles] and
[build-script environment][cargo-build-env].
Expand All @@ -175,8 +175,8 @@ only for a kernel's crate does not. Setting the environment variable only
when running an already-built binary does not change device compilation.

`CompileOptions::new()` and `CompileOptions::default()` inherit this build
default. Use `debug_info()` to replace it for one launch. It sets both debug
flags, so it can also turn off a build-time `full` default:
default. Use `debug_info()` to replace it for one launch. It replaces the
inherited mode, so it can also turn off a build-time `full` default:

```rust
use cutile::tile_kernel::{CompileOptions, DebugInfoLevel};
Expand All @@ -187,8 +187,7 @@ my_kernel(args)
```

Each option is part of the JIT cache key, so the modes do not share a cached
compiled kernel. The individual flags remain available, as does sanitizer
instrumentation:
compiled kernel. Sanitizer instrumentation is a separate option:

```rust
use cutile::tile_kernel::{CompileOptions, DebugInfoLevel};
Expand All @@ -207,18 +206,37 @@ my_kernel(args)
my_kernel(args).compile_options(CompileOptions::new().sanitize_memcheck(true)).sync()?;
```

- `debug_info(level)` replaces both debug flags. The individual
`device_debug(bool)` and `lineinfo(bool)` setters change only their own
flag, leaving other inherited settings in place.
- `device_debug(true)` passes `--device-debug` to the device compiler and
implies optimization level 0. An explicit `opt_level` takes precedence,
but `tileiras` currently rejects full debug information at optimized
levels. The frontend also stops hoisting bounds checks out of loops.
Checks it proved unnecessary or moved to launch time are unaffected;
they never reach device code in any mode.
- `lineinfo(true)` passes `--lineinfo`: source-line correlation for Nsight Compute and Nsight Systems without changing code generation. This is the option for profiling optimized kernels.
- `debug_info(DebugInfoLevel::Full)` selects device-debug mode, which passes
`--opt-level 0 --device-debug`. The frontend also stops hoisting bounds
checks out of loops. Checks it proved unnecessary or moved to launch time
are unaffected; they never reach device code in any mode.
- `debug_info(DebugInfoLevel::Line)` passes `--lineinfo`: source-line correlation for Nsight Compute and Nsight Systems without changing code generation. This is the option for profiling optimized kernels.
- `debug_info(DebugInfoLevel::None)` and `Line` keep the current
optimization level, or use level 3 when leaving device-debug mode.
- `opt_level(n)` passes `--opt-level n` and keeps line information.
`tileiras` supports device debugging only at level 0, so this also leaves
device-debug mode. Whichever of `debug_info` and `opt_level` is called
last decides.
- `sanitize_memcheck(true)` passes `--sanitize=memcheck` for `compute-sanitizer --tool memcheck`.
- `opt_level(n)` selects `--opt-level` directly; the default is 3.

To switch an already-built application without changing its code, set these
variables when running it. Each applies after the code's options, as if the
matching setter were called last. They are read once per process:

| Variable | Values | Acts like |
| --- | --- | --- |
| `CUTILE_JIT_DEBUG` | `none`, `line`, `full` | `debug_info(...)` |
| `CUTILE_JIT_OPT_LEVEL` | `0`, `1`, `2`, `3` | `opt_level(n)` |
| `CUTILE_JIT_SANITIZE` | `memcheck`, `none` | `sanitize_memcheck(...)` |

```bash
CUTILE_JIT_DEBUG=full cuda-gdb --args ./target/release/my_program
CUTILE_JIT_SANITIZE=memcheck compute-sanitizer --tool memcheck ./target/release/my_program
```

An unset variable leaves the code's choice in place. An invalid value, or
`CUTILE_JIT_DEBUG=full` together with `CUTILE_JIT_OPT_LEVEL`, fails JIT
compilation with an error that names the variable.

Full debug mode describes source locations and inline frames, not Rust
variables or values. Tile IR currently supports control-flow debugging of
Expand Down Expand Up @@ -275,6 +293,7 @@ without a GPU:

```bash
cargo test -p cutile --test debug_info
cargo test -p cutile --test jit_env_overrides
bash scripts/test_debug_defaults.sh
```

Expand Down
48 changes: 25 additions & 23 deletions cutile-rs/cutile-compiler/src/check_optimizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,41 +103,43 @@ impl CheckOptimizations {
mod tests {
use super::*;
use crate::cuda_tile_runtime_utils::{TileirasOptions, DEFAULT_OPT_LEVEL};
use crate::hints::CompileOptions;
use crate::hints::{CodegenMode, CompileOptions, DebugInfoLevel};

#[test]
fn device_debug_implies_opt_zero_unless_explicit() {
let debug =
TileirasOptions::from_compile_options(&CompileOptions::new().device_debug(true));
assert_eq!(debug.opt_level, 0);
assert!(debug.device_debug);

let explicit = TileirasOptions::from_compile_options(
&CompileOptions::new().device_debug(true).opt_level(2),
fn full_debug_compiles_at_level_zero() {
let debug = TileirasOptions::from_compile_options(
&CompileOptions::new().debug_info(DebugInfoLevel::Full),
);
assert_eq!(explicit.opt_level, 2, "an explicit level wins");
assert_eq!(debug.codegen, CodegenMode::Debug);
assert_eq!(debug.codegen.opt_level(), 0);

let release = TileirasOptions::from_compile_options(
&CompileOptions::new().debug_info(crate::hints::DebugInfoLevel::None),
&CompileOptions::new().debug_info(DebugInfoLevel::None),
);
assert_eq!(release.opt_level, DEFAULT_OPT_LEVEL);
assert_eq!(release.codegen.opt_level(), DEFAULT_OPT_LEVEL);
assert_eq!(release, TileirasOptions::default());
}

#[test]
fn flags_byte_is_injective_over_the_flag_combinations() {
let mut seen = std::collections::BTreeSet::new();
for dd in [false, true] {
for li in [false, true] {
for sm in [false, true] {
let o = TileirasOptions {
opt_level: 3,
device_debug: dd,
lineinfo: li,
sanitize_memcheck: sm,
};
assert!(seen.insert(o.flags_byte()), "flags_byte collision");
}
for codegen in [
CodegenMode::Debug,
CodegenMode::Optimized {
level: 3,
lineinfo: false,
},
CodegenMode::Optimized {
level: 3,
lineinfo: true,
},
] {
for sanitize_memcheck in [false, true] {
let o = TileirasOptions {
codegen,
sanitize_memcheck,
};
assert!(seen.insert(o.flags_byte()), "flags_byte collision");
}
}
assert_eq!(
Expand Down
16 changes: 13 additions & 3 deletions cutile-rs/cutile-compiler/src/compile_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,12 @@ impl<F: Fn() -> crate::ast::Module> KernelCompiler<F> {
/// The returned string is the same 64-character lowercase SHA-256 key that
/// the runtime's L2 cache lookup would use for this specialization.
pub fn l2_cache_key(self) -> Result<String, JITError> {
let gpu_name = self.gpu_name.clone();
let resolved = self.with_jit_env_overrides()?;
let gpu_name = resolved.gpu_name.clone();
let tileiras_opts = crate::cuda_tile_runtime_utils::TileirasOptions::from_compile_options(
&self.compile_options,
&resolved.compile_options,
);
let artifacts = self.compile()?;
let artifacts = resolved.compile_resolved()?;
if let Some(version) = artifacts.bytecode_version {
let bytes = artifacts.bytecode()?;
Ok(crate::cuda_tile_runtime_utils::current_l2_key_for_bytecode(
Expand All @@ -255,6 +256,15 @@ impl<F: Fn() -> crate::ast::Module> KernelCompiler<F> {
///
/// This is a pure compilation step — no GPU or CUDA driver is needed.
pub fn compile(self) -> Result<CompileArtifacts, JITError> {
self.with_jit_env_overrides()?.compile_resolved()
}

fn with_jit_env_overrides(mut self) -> Result<Self, JITError> {
self.compile_options = self.compile_options.with_jit_env_overrides()?;
Ok(self)
}

fn compile_resolved(self) -> Result<CompileArtifacts, JITError> {
let module_ast = (self.module_ast_fn)();
let modules = CUDATileModules::from_kernel(module_ast)?;

Expand Down
2 changes: 1 addition & 1 deletion cutile-rs/cutile-compiler/src/compiler/_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ impl<'m> CUDATileFunctionCompiler<'m> {
// in-kernel code motion (see CheckOptimizations::device_debug for
// why discharge and launch relocation are untouched).
let mut check_opts = crate::check_optimizations::CheckOptimizations::from_env();
if compile_options.device_debug {
if compile_options.codegen == crate::hints::CodegenMode::Debug {
check_opts.hoist_to_preheaders = false;
}
Ok(CUDATileFunctionCompiler {
Expand Down
51 changes: 20 additions & 31 deletions cutile-rs/cutile-compiler/src/cuda_tile_runtime_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! Provides GPU detection and bytecode compilation helpers.

use crate::error::JITError;
use crate::hints::CodegenMode;
use cuda_core::{get_device_sm_name, Device};
use cutile_ir::bytecode::{write_bytecode_version, BytecodeVersion};
use std::collections::HashMap;
Expand Down Expand Up @@ -519,7 +520,7 @@ fn negotiate_bytecode_version(
.map_err(|error| JITError::Generic(error.to_string()))
}

/// `--opt-level` passed to `tileiras`. Not configurable yet.
/// Default `--opt-level` for optimized code.
/// Numeric, not a string: the disk-cache key and entry header store it as one
/// byte.
pub const DEFAULT_OPT_LEVEL: u8 = 3;
Expand Down Expand Up @@ -646,51 +647,38 @@ pub(crate) fn current_l2_key_for_module(
/// field can never share a cubin.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TileirasOptions {
/// `--opt-level <N>`.
pub opt_level: u8,
/// `--device-debug`: generate debug information.
pub device_debug: bool,
/// `--lineinfo`: generate line-number information only.
pub lineinfo: bool,
/// `--opt-level <N>`, plus `--device-debug` or `--lineinfo`.
pub codegen: CodegenMode,
/// `--sanitize=memcheck`: instrument memory accesses for the sanitizer.
pub sanitize_memcheck: bool,
}

impl Default for TileirasOptions {
fn default() -> Self {
Self {
opt_level: DEFAULT_OPT_LEVEL,
device_debug: false,
lineinfo: false,
codegen: CodegenMode::RELEASE,
sanitize_memcheck: false,
}
}
}

impl TileirasOptions {
/// Resolves the launch-facing [`crate::hints::CompileOptions`] into the
/// stage-2 flags. `device_debug` implies `--opt-level 0` unless an
/// explicit level was requested.
/// The stage-2 subset of the launch-facing [`crate::hints::CompileOptions`].
pub fn from_compile_options(options: &crate::hints::CompileOptions) -> Self {
let opt_level = options.opt_level.unwrap_or(if options.device_debug {
0
} else {
DEFAULT_OPT_LEVEL
});
Self {
opt_level,
device_debug: options.device_debug,
lineinfo: options.lineinfo,
codegen: options.codegen,
sanitize_memcheck: options.sanitize_memcheck,
}
}

/// The boolean flags packed into one byte, for the cache-entry header
/// and the L2 key material.
pub fn flags_byte(&self) -> u8 {
(self.device_debug as u8)
| ((self.lineinfo as u8) << 1)
| ((self.sanitize_memcheck as u8) << 2)
let codegen = match self.codegen {
CodegenMode::Debug => 0b001,
CodegenMode::Optimized { lineinfo, .. } => (lineinfo as u8) << 1,
};
codegen | ((self.sanitize_memcheck as u8) << 2)
}
}

Expand Down Expand Up @@ -726,13 +714,14 @@ fn run_tileiras_at(
JITError::Generic(format!("Failed to write bytecode for {bc_filename}: {e}"))
})?;

let opt_level_arg = opts.opt_level.to_string();
let opt_level_arg = opts.codegen.opt_level().to_string();
let mut args = vec!["--gpu-name", gpu_name, "--opt-level", &opt_level_arg];
if opts.device_debug {
args.push("--device-debug");
}
if opts.lineinfo {
args.push("--lineinfo");
match opts.codegen {
CodegenMode::Debug => args.push("--device-debug"),
CodegenMode::Optimized { lineinfo: true, .. } => args.push("--lineinfo"),
CodegenMode::Optimized {
lineinfo: false, ..
} => {}
}
if opts.sanitize_memcheck {
args.push("--sanitize=memcheck");
Expand Down Expand Up @@ -858,7 +847,7 @@ fn compile_bytecode_cached_at(
let params = EntryParams {
bc_sha256: Sha256::digest(bytecode).into(),
gpu_name,
opt_level: opts.opt_level,
opt_level: opts.codegen.opt_level(),
flags: opts.flags_byte(),
tileiras_fp,
};
Expand Down
Loading