Skip to content

[REFACTOR] Share the compile-failure classification and the guard builder - #528

Open
mark14wu wants to merge 1 commit into
rd-5a-reader-l2-factsfrom
rd-5b-shared-ir-helpers
Open

mark14wu wants to merge 1 commit into
rd-5a-reader-l2-factsfrom
rd-5b-shared-ir-helpers

Conversation

@mark14wu

@mark14wu mark14wu commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

The compiled race detector (the next PR) needs two pieces that are private to the compiled sanitizer today: the rules that decide what a failed host compile means for a launch, and the builder that works out under which tl.where arms each term of an access is read. This PR moves both into the shared IR layer so the two clients use one copy. It is a move, with no change in behaviour: the sanitizer sends the same Z3 queries and returns the same verdicts, refusals and notes as before (see Behaviour unchanged).

Stacked on #515. Triton 3.8 only. One commit.

Change

1. tilelens/ir/failures.py: the compile-failure classification

The module-level helpers in tilelens/clients/sanitizer/compiled/client.py move here unchanged. The rule they implement stays the same:

  • a kernel that cannot host-compile makes that config, or the whole IR launch, unsupported, and the program continues;
  • an error no target compiles past (a failing tl.static_assert, a construct the code generator never accepts, from a compile that never asked the driver anything) only adds a note, because the config never launches anywhere;
  • any other compile error may be the target's, so the config is unsupported with kind compile-failed, and the refusal says how to name another target;
  • a host compile that could not run at all, for example on a Triton other than 3.8, is host-compile-unavailable.

Public entry points:

new name was
compiles_nowhere(error) _compiles_nowhere
failure_refusal(failure, retarget) _failure_refusal
nowhere_note(failure, config) _nowhere_note
none_compiled(log) _none_compiled
nothing_compiled(log, eager_client) _nothing_compiled

_error_chain, _summary, _kernel_site, _for_target and _at stay private to the module. COMPILE_FAILED and HOST_COMPILE_UNAVAILABLE are the two refusal kinds as plain strings, and the sanitizer's SanitizerKind members of those names now take their values from them.

Three pieces of wording belonged to the sanitizer, so they are now parameters, and the sanitizer passes exactly the text it used before:

  • retarget: how to name another target ("Sanitizer(compile=True, target=...), or TILELENS_IR_TARGET");
  • eager_client: which eager client checks a launch that has no JITFunction ("Sanitizer()");
  • config: how a failed config is shown in a note (the sanitizer's _saveable(failure.config)).

_describe stays in the client. None of the moved helpers uses it, and the client still does for its internal errors. analyze_launch keeps the loop that turns each failure into a note or a ConfigVerdict, because that is the client's verdict policy. Like the other tilelens.ir modules, importing failures.py does not import Triton.

2. GuardDag in tilelens/ir/lowering.py: the Select-arm guard builder

_Dag in oob.py moves to lowering.py as GuardDag(roots, graph, cond=None, operands=children):

  • order / rank(term): children before parents, and a Select's condition before its arms. A term the walk does not hold comes right after its children.
  • guards: given cond (the client's lowering of a term to a Bool, e.g. Lowerer.cond), the guard of each term is the condition under which its value reaches a root through Select arms. A term that is read directly, or that is a Select's condition, has no guard.
  • premise(term, holds): Implies(guard, holds), or holds itself for a term with no guard. This turns a fact about a term, such as a width obligation, into a Select-guarded premise. It is the expression the sanitizer built inline before.

It goes into lowering.py and not a new module because it is the same kind of code: a walk over the same children relation, building Z3 conditions from the client's own lowering. It also keeps the module's contract. It is mechanism only, it never creates a Z3 context (its guards are built from what cond returns), and it raises only what children raises for a malformed graph; an exception from the client's cond or operands propagates unchanged. The module docstring gains one paragraph on it.

What stays in the sanitizer:

  • the walk's edges: its _operands stops at a LoopVar, because the loop's bounds belong to the loop's own checks, and it passes them as operands;
  • which obligations are guarded: a signed quotient that does not fit is undefined in either arm, so it is never guarded (_undefined_when_wide), and divisions are never guarded;
  • the loop's obligations, which are checked unguarded;
  • how a failure is reported.

Not in this PR

  • Any race-detector code.
  • The pid module.
  • Any behaviour change.

Behaviour unchanged

Each check below compares this PR with a pristine b3c0212 (the head of #515) in its own worktree, with the same Triton 3.8.0 setup.

  • Compiled sanitizer on the corpus. The same 977 Triton 3.8.0 TTIRs as in [FEAT] Add the race detector's TTIR reader facts #509 and [FEAT] Add the race detector's multipath and loaded-value reader facts #515. Each text is parsed, and every parsed graph goes through check_graph under the synthetic binding of [FEAT] Add the race detector's TTIR reader facts #509 (4096-element tensors, scalars 64, grid (4, 2, 1)):

    • the 527 parsed graphs are identical (the sha256 of each full repr);
    • the 450 refusals are identical in kind, message, line and loc;
    • all 6668 Z3 queries (Solver.sexpr() at every check) are byte for byte the same;
    • the findings (1137 on 268 graphs), refusals and abstentions are identical in kind, access, base, line and loc;
    • the Select-guarded path is exercised: 8 of the 977 rows produce Select-guarded premises, 120 (=> in their query sexprs, the same on both sides.

    Each tree was run five times (once unseeded, then PYTHONHASHSEED 0 to 3), and every field above is identical across all ten runs. One finding's witness changes from run to run, on the base as well as on this PR, from identical queries: the c_ptr store of tb_matmul_dequantize reports offset 4096 or 8064. In these five runs the base gave 8064 in 2 and this PR in 5; over the twelve runs of each tree made during review, the base gave it in 4 and this PR in 9. Which value comes up does not follow the hash seed (the same seed gave either value in different runs of the same tree). That is Z3's model choice, as in [FEAT] Add the race detector's TTIR reader facts #509 and [FEAT] Add the race detector's multipath and loaded-value reader facts #515. Apart from that one witness (its offset, address and lane values), the full result reprs are identical.

  • Compile-failure classification. One traced launch per failure kind through Sanitizer(compile=True). Each launch's IRVerdict repr is byte-identical between base and this PR:

    • no target compiles: every config fails a tl.static_assert;
    • mixed autotune: one config compiles, one needs num_ctas=2, one fails a static_assert;
    • target-dependent: a plain launch with num_ctas=2;
    • target-dependent: an unknown launch option;
    • Triton reporting 3.9.0, which makes the host compile unavailable.

    A function-level matrix also calls the old private helpers on the base and the moved functions on this PR with the same inputs: 18 error shapes (plain errors, CompilationError chains with a cause, a suppressed context and a cycle, static_assert and unsupported constructs with and without a driver query, HostCompileUnavailable direct and chained, a patched-language error, a call that does not bind, unknown options) for 3 targets, 3 kernels and 3 configs, plus none_compiled and nothing_compiled over several logs. The 494 output lines are identical once tensor addresses are normalized; they include 410 compile-failed and 81 host-compile-unavailable refusals.

    The existing compile-failure tests in tests/unit/sanitizer_compiled/test_client.py and tests/end_to_end/test_compiled_sanitizer.py pass unchanged. The one exception is the test that replaces failure_refusal with a broken one: it now patches the new name and signature.

Tests

New direct tests for the moved entry points:

  • tests/unit/ir/test_failures.py (4 tests): an error no target compiles past (compiles_nowhere, nowhere_note, none_compiled); a target-dependent error (failure_refusal names the client's retarget); a host compile that could not run (host-compile-unavailable); nothing_compiled names the client's eager client.
  • tests/unit/ir/test_lowering.py (1 test): GuardDag guards terms read through a Select's arms by the arm's condition, and leaves the condition and directly read terms unguarded; premise and rank; without cond there are no guards.

Commands, Triton 3.8.0, CPU only (CUDA_VISIBLE_DEVICES="", UV_NO_SYNC=1):

uv run --extra test pytest tests/unit/ir tests/unit/sanitizer_compiled tests/end_to_end/test_compiled_sanitizer.py tests/end_to_end/test_ir_lifecycle_compiled.py tests/end_to_end/test_ir_client.py tests/unit/test_ir_lifecycle.py -q -p no:cacheprovider -n 4
run base b3c0212 this PR
IR and compiled-sanitizer tests, -n 4 713 passed, 4 skipped 718 passed, 4 skipped
regression gate (the 20 files of #501–#505), -n 4 595 passed, 2 skipped 595 passed, 2 skipped

The 5 new rows are the tests above.

Line counts

From git diff --numstat against b3c0212:

file added deleted
tilelens/ir/failures.py (new) 261 0
tilelens/ir/lowering.py 90 1
tilelens/clients/sanitizer/compiled/client.py 21 238
tilelens/clients/sanitizer/compiled/oob.py 18 85
product total 390 324
tests/unit/ir/test_failures.py (new) 66 0
tests/unit/ir/test_lowering.py 21 1
tests/unit/sanitizer_compiled/test_client.py 2 2
test total 89 3

Most of the product lines are moved, not new: about 315 lines leave the sanitizer and arrive in tilelens/ir. The net +66 is the new module's docstring, the two kind constants, the three wording parameters, GuardDag.premise with the operands parameter, and the updated comment on the sanitizer's two kinds. The tests add fewer lines than the product.

…lder

The compiled race detector needs two pieces that were private to the
compiled sanitizer. Move them, unchanged in behaviour, into the shared IR
layer:

- tilelens/ir/failures.py: what a config's failed host compile means
  (compiles_nowhere, failure_refusal, nowhere_note, none_compiled,
  nothing_compiled; the error-chain, summary and kernel-site helpers stay
  private). The client's own words are parameters: how to name another
  target, which eager client checks a launch without a JITFunction, and
  how a config is shown. The sanitizer passes the texts it used before,
  so every refusal kind, message and note is the same.
- tilelens/ir/lowering.py: GuardDag, the Select-arm guard of every term
  read from a set of roots, its rank, and premise(term, holds), which
  makes a fact about a term hold only where the term's value reaches a
  root. The walk's edges are a parameter (default children); the
  sanitizer passes its own, which stops at a LoopVar, and keeps which
  obligations are guarded and how a failure is reported.
@mark14wu
mark14wu added this pull request to stack #508 November 4, 2026 15:07
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Performance Benchmark

Benchmark main (min) PR (min) Change Samples
gemm 0.057s 0.057s -0.8% 20 / 20
gemm_oob 0.064s 0.063s -1.3% 20 / 20
indirect_load 0.011s 0.011s +0.7% 20 / 20
nested_loop 0.116s 0.119s +2.1% 20 / 20
block_pointer_loop_advance 0.062s 0.064s +3.8% 20 / 20
liger_jsd 0.079s 0.080s +0.5% 20 / 20
flaggems_layernorm 0.203s 0.207s +2.1% 20 / 20
swiglu 0.091s 0.096s +5.6% ⚠️ 20 / 20
cross_entropy 0.527s 0.537s +1.8% 20 / 20
fused_linear_jsd 0.118s 0.125s +5.6% ⚠️ 20 / 20
Total 1.328s 1.358s +2.2% N/A

Threshold: >5% regression flagged with ⚠️
Iterations: 1 warmup + 20 measured
Samples are shown as main / PR; long pytest benchmarks may use fewer samples.

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.

1 participant