Skip to content

cutile: one CodegenMode for device debug, plus CUTILE_JIT_* run-time overrides - #1600

Open
almightychang wants to merge 4 commits into
NVIDIA:mainfrom
almightychang:feat/tileiras-jit-env-overrides
Open

almightychang wants to merge 4 commits into
NVIDIA:mainfrom
almightychang:feat/tileiras-jit-env-overrides

Conversation

@almightychang

@almightychang almightychang commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1428. Continues NVlabs/cutile-rs#237.

This PR controls which compile mode a kernel gets. It does not fix trap attribution (#1379) or the inline-call breakpoint resolver (#1476); the other source-location audit items stay independent.

Problem

tileiras accepts --device-debug only at --opt-level 0. That rule spans two fields, but CompileOptions stores them independently, and each source writes them field by field:

CompileOptions { opt_level: Option<u8>, device_debug: bool, lineinfo: bool }

 build default ─┐
 code setters  ─┼─ field by field ─▶ TileirasOptions ─────▶ tileiras
 (run-time env)─┘                    can still hold          rejects it
                                     -O3 + device-debug      at JIT time

A field-by-field merge cannot keep a rule that spans two fields. Each writer would need its own copy of the "level 0 if debug" rule. Today the only copy, the default in from_compile_options, applies only when no level is set. The resolved options can still hold the rejected pair, so the only real check is tileiras itself, at JIT time, far from the setter that caused it.

NVlabs/cutile-rs#285 made the build profile another writer. Dev-profile builds now start in full debug, so CompileOptions::new().opt_level(3) alone fails at JIT time, while the same code passes with --release. The run-time overrides that NVlabs/cutile-rs#237 proposed would add a third writer and a third copy of the rule.

Change

Make the rejected pair unrepresentable, and have each writer replace the whole value:

enum CodegenMode { Debug, Optimized { level: u8, lineinfo: bool } }

 build default ─▶ code setters ─▶ CUTILE_JIT_* ─▶ CodegenMode ─┬─▶ frontend (no check hoisting in Debug)
       (each replaces the mode; the last one wins)              └─▶ tileiras flags

The rule is removed instead of copied. The run-time overrides then reduce to the same setters, applied after the code's:

Variable Values Acts like
CUTILE_JIT_DEBUG none, line, full debug_info(...)
CUTILE_JIT_OPT_LEVEL 0–3 opt_level(n)
CUTILE_JIT_SANITIZE memcheck, none sanitize_memcheck(...)

CUTILE_JIT_DEBUG=full together with CUTILE_JIT_OPT_LEVEL, or an invalid value, fails with a JIT error that names the variable. The overrides are applied once before both compile stages, so the frontend and tileiras see the same mode. NVlabs/cutile-rs#237 applied them only to the tileiras flags.

Behavior and API changes

Before After
dev build + opt_level(3) -O3 --device-debug, rejected -O3
.debug_info(Full).opt_level(3) -O3 --device-debug, rejected -O3 (last call wins)
.opt_level(3).debug_info(Full) -O3 --device-debug, rejected -O0 --device-debug
device_debug(bool), lineinfo(bool) setters removed; debug_info() covers them
opt_level, device_debug, lineinfo fields public replaced by codegen

Cache key and entry-header bytes are unchanged for every mode; a test pins them against the previous encoding. DebugInfoLevel and debug_info() from NVlabs/cutile-rs#285 stay. The v0.5.0 changelog and book include the field/setter migration and ordering rules.

Validation

Local container from nvidia/cuda:13.3.1-devel-ubuntu24.04 with an RTX 5080 (driver 580, CUDA 13.0):

  • cargo fmt -- --check; cargo clippy --workspace --exclude cuda-tile-rs --all-targets (only the existing rmsnorm.rs unused-unsafe warning)
  • scripts/run_cpu_tests.sh and scripts/run_gpu_tests.sh: all passed
  • scripts/test_debug_defaults.sh: passed
  • tests/jit_env_overrides.rs: every case runs in a child that inherits no CUTILE_JIT_*, and passes with all three exported in the parent shell. The cache-key matrix compares keys from independent processes; it fails when the mode is dropped from the key.
  • tests/gpu/jit_env_overrides.rs: the normal launcher, eight processes, one shared disk store. Each checks the kernel result, check placement, and stage2_source; it fails when the launcher skips the overrides.
  • tests/debug_info.rs --ignored: the cuda-gdb source-breakpoint and inline-frame smoke test on the full-debug cubin passes.
  • Each commit builds and passes the touched suites on its own
  • A logging shim in place of tileiras (CUTILE_TILEIRAS_PATH, empty disk cache) recorded the flags it received:
no env                                 --opt-level 0 --device-debug    (debug-profile default)
CUTILE_JIT_DEBUG=none                  --opt-level 3
CUTILE_JIT_DEBUG=full, SANITIZE        --opt-level 0 --device-debug --sanitize=memcheck
CUTILE_JIT_OPT_LEVEL=1, DEBUG=line     --opt-level 1 --lineinfo
CUTILE_JIT_DEBUG=full, OPT_LEVEL=2     JIT error before tileiras runs

…nMode

`tileiras` accepts `--device-debug` only at optimization level 0, but
`CompileOptions` kept the level and the debug flag as independent fields.
Debug builds default to full debug, so adding only `.opt_level(3)` produced
`--opt-level 3 --device-debug`, which `tileiras` rejects at JIT time.

Replace `opt_level`, `device_debug`, and `lineinfo` with
`codegen: CodegenMode`, where `Debug` owns level 0 and
`Optimized { level, lineinfo }` covers the rest. Every value now maps to
flags `tileiras` accepts. `debug_info()` and `opt_level()` select a mode,
and the last call wins. The level-0 default rule in
`TileirasOptions::from_compile_options` is gone, and the frontend reads the
same mode to decide check hoisting.

Remove the `device_debug(bool)` and `lineinfo(bool)` setters, which
`debug_info()` covers. The cache key and entry header bytes are unchanged
for every mode; a test pins them against the previous encoding.

Signed-off-by: almightychang (Sam, Joochul Chang) <almightychang@icloud.com>
Nothing could switch an already-built application to a debug or sanitizer
build without changing its code. Add three process-wide variables, read
once per process:

- CUTILE_JIT_DEBUG=none|line|full acts like `debug_info()`
- CUTILE_JIT_OPT_LEVEL=0|1|2|3 acts like `opt_level()`
- CUTILE_JIT_SANITIZE=memcheck|none acts like `sanitize_memcheck()`

Each applies after the code's options, as if its setter were called last.
`CompileOptions::with_jit_env_overrides()` applies them once before both
compile stages, so the frontend's check placement and the `tileiras` flags
always come from the same options. Invalid values, and full debug together
with an explicit level, return a JIT error naming the variable.

This continues NVlabs/cutile-rs#237, which applied the overrides only to
the `tileiras` flags and left the frontend hoisting checks under
`CUTILE_JIT_OPTIMIZATION=debug`.

Signed-off-by: almightychang (Sam, Joochul Chang) <almightychang@icloud.com>
@copy-pr-bot

copy-pr-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@elibol

elibol commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Reviewing this against #1428. The setter/parser unit tests, subprocess full-debug test, previous-cache-encoding regression, and recorded real tileiras flag matrix provide useful coverage. The important contract is that the build default, explicit options, frontend check placement, assembler flags, and caches all agree on the same effective mode.

Two integration gaps to close:

  • Make jit_env_overrides hermetic: use a dedicated child-process marker and clear all three CUTILE_JIT_* variables before setting the case. Currently an inherited DEBUG value can bypass the parent path, and inherited OPT_LEVEL/SANITIZE can change the result. Test explicit off, line+optimization, invalid/conflicting values, and the read-once behavior in isolated processes.
  • The current cache-key assertion compares two compiler configurations under the same DEBUG=full override. Both would receive the same override even if the resolved mode were omitted from the cache key. Compare independently launched processes with unset/none/line/full settings and a shared temporary disk cache: different effective modes must separate, equivalent programmatic/environment modes must reuse. Exercise the normal launcher as well as KernelCompiler, with an actual kernel result and frontend check-placement counts; retain the real-assembler flag checks already reported.

For the debug-mode claim, retain a source-breakpoint/inline-backtrace smoke test on the produced full-debug cubin. This PR controls compilation mode; it should not be treated as the fix for #1379's trap attribution or #1476's inline-call breakpoint resolver.

Once accepted, #1428 is the issue this PR should close; the other source-location audit items remain independent.

@elibol elibol added this to the cutile rust v0.5.0 milestone Oct 21, 2026
…cesses

Every override case now runs in a child process that inherits no
CUTILE_JIT_* setting and is selected by a dedicated marker variable. The
cases cover explicit none, line with an opt level, full, an invalid value,
the full/opt-level conflict, and the read-once rule.

The cache-key check compares keys reported by independent processes, each
under its own overrides, so a key that dropped the effective mode would
let two distinct modes collide. A GPU test runs the normal launcher
against one shared disk store and checks the kernel result, the check
placement, and whether stage 2 came from tileiras or the store.

Signed-off-by: almightychang (Sam, Joochul Chang) <almightychang@icloud.com>
@almightychang

Copy link
Copy Markdown
Contributor Author

Thanks, both gaps were real. Pushed 765cb1f (tests only).

Hermetic overrides. Each case runs in a child selected by CUTILE_TEST_JIT_ENV_CASE, with all three CUTILE_JIT_* removed before the case's own values are set. Cases: explicit none, line + OPT_LEVEL=1, full, an invalid value, full + OPT_LEVEL=2, and read-once (set_var after the first compile changes nothing). The suite passes with DEBUG=full OPT_LEVEL=2 SANITIZE=memcheck exported in the parent.

Cache key. The old assertion compared two configurations under the same override, as you said. Now:

  • KernelCompiler: one process per mode reports its key and placement counts. release, line, line+O1, full and memcheck keys are pairwise distinct, and each env setting matches the same mode set in code. Dropping the mode from l2_key fails this test.
  • Launcher (GPU suite): eight processes share one FileSystemJitStore, and each checks the kernel output plus the CUTILE_JIT_TIMING placement and stage2_source. Env full compiles, code Full then hits disk; none over code Full reuses the release entry; line with OPT_LEVEL=1 and SANITIZE=memcheck each compile fresh. Skipping the overrides in compile_and_load_kernel fails it.

A key that ignores the mode does not show up at the launcher. The entry header also records opt level and flags, so a colliding entry is rejected and recompiled. That case is covered by the KernelCompiler matrix.

Debug claim. The cuda-gdb smoke test in debug_info.rs stays and passes on the full-debug cubin. I updated the description to close #1428 only and to say that #1379 and #1476 are out of scope.

… main

Integrate the v0.5.0 baseline and document the removed CompileOptions fields/setters, setter precedence, and runtime override behavior.

Signed-off-by: Melih Elibol <1884921+elibol@users.noreply.github.com>
@elibol

elibol commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

/ok to test a844d56

@elibol

elibol commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Pushed a844d56ba1c1e0eb3a9cfe46d0b5173e09c254aa: merged the v0.5.0 baseline and added the changelog/API migration guide covering removed fields/setters, codegen, setter order and runtime override precedence.

CUDA 13.3 validation: all seven environment tests pass with all three overrides set in the parent; the normal-launcher shared-cache GPU matrix passes; all nine debug and six generated-source tests pass including assembler/GPU/CUDA-GDB; strict compiler Clippy and formatting pass. CUDA 13.2/13.4 Tile CI, build, semver, shared-host and reactor checks pass. Rust CodeQL is still running.

This addresses the requested migration treatment and validation for #1428. #1379/#1476 remain separate. Combined source-location/debug-mode validation in #1613 passes all 35 tests. Release tracking: #1606.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: model full debug and add TileIR JIT overrides

2 participants