Repository navigation
Conversation
RaceDetector(compile=True) and tile-race --compile analyze each launch's host-compiled TTIR for global-memory races without running the kernel. The analysis reads the reader's multipath and while-region facts, lowers them through RaceLeaves (pids from the new neutral tilelens/clients/pids.py), reads loaded integer values from launch-value snapshots, and picks the least concretization that decides each config (T0, T1, T1-launch). Width obligations, an await's exit predicate's included, become Select-guarded premises, reported as a qualifier on any-grid proofs and discharged at the launch whatever the rung. Tile records carry their shape (the reader's new AccessEvent.shape) and, when lane variables name every dimension, a lane position; dependency order composes only through accesses that run wherever the dependent does. The fence rule, the await (spin-shape) pass and the CAS operand gate are the race detector's own. A launch-grid requery Z3 does not decide leaves the config unsupported, its any-grid races kept as evidence only. Solves are serialized in Z3's main context (hb_common.MAIN_CONTEXT_LOCK). Each launch records a RaceVerdict with one RaceConfigVerdict per config and a CompiledRaceRecord per race, which tilelens.save()/load() keep. The shared Lowerer takes a divrem hook (by default the truncating encoding it had, so the compiled sanitizer's queries are unchanged); RaceLeaves' BoundedDivRem uses Z3's Euclidean // and % where bounds over the domains every query assumes make them the IR's truncating ones. The compiled sanitizer's config helpers move to tilelens.ir.capture (configs_of, saveable_config), unchanged; ArtifactLog gains last_binding.
Performance Benchmark
Iterations: 1 warmup + 20 measured |
A launch the compiled race detector could not analyze (an unsupported part, a timeout, a config that compiled to nothing) printed nothing unless TILELENS_VERBOSE was set or tile-race was running, and neither did a proof, so a quiet launch could be read as race-free. The not-analyzed lines and notes now always print, once per site per detector, as the compiled sanitizer and the eager race detector already report them. The race-free line still prints only under verbose or tile-race.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds the compiled race detector:
RaceDetector(compile=True)ortile-race --compile script.py. Each launch is analyzed for global-memory races on the TTIR Triton compiles for it, on the host, without running the kernel. Like the compiled sanitizer (#480), it needs no GPU, the kernel's outputs are not written, every autotune config is analyzed, and a config that does not compile for the target isunsupportedwhile the program goes on.The analysis is #361's compiled (TTIR) race detector, taken from the frozen 62c6d7c and rebuilt on the shared IR layer of this stack: the TTIR reader's facts (#509, #515), the shared lowering (
TermLeaves,Lowerer,fold,GuardDag; #482, #528), the compile-failure classification (#528) and the two-copy solver (#503, #505). It always reads the reader's multipath and while-region facts, and always reads loaded integer values from launch-value snapshots; there is no level switch.Stacked on #528 (stack #508). Triton 3.8 only. One commit.
User-visible changes:
RaceDetector(compile=True)returns aCompiledRaceDetector(until now it raisedNotImplementedError). WithENABLE_RACE_DETECTOR=0it is theNullRaceDetector, as for the eager mode.SymbolicRaceDetector(compile=True)raises aTypeErrornamingRaceDetector(compile=True), likeSymbolicSanitizer(compile=True).tile-race --compile script.pyruns it;tile-race script.pyis unchanged.CompiledRaceRecordper race, then aRaceVerdictwith oneRaceConfigVerdictper config, inLaunch.records.tilelens.save()/load()keep them.CompiledRaceDetector,CompiledRaceRecord,RaceVerdictandRaceConfigVerdictare exported fromtilelens.clients.[CompiledRaceDetector] not analyzed ...), with the launch's notes, so a quiet launch never reads as race-free; this matches the compiled sanitizer's default and the eager race detector's warning. A proved launch prints one line only underTILELENS_VERBOSE=1or thetile-raceCLI. Nothing callssys.exit.The eager race detector, the compiled sanitizer, the tracer and the profiler behave as before. The compiled sanitizer sends the same Z3 queries and returns the same results on the 977 corpus texts (see Tests).
Change
1. The client (
tilelens/clients/race_detector/compiled/client.py)CompiledRaceDetectoris anIRClientwith its ownNAME(compiled_race_detector),IR_STAGES = {"ttir"}andLAUNCH = "skip". It does not subclass the eager detector or the sanitizer; it is registered as a virtual subclass ofRaceDetector, so the factory's disable flag reads it alike. The constructor isCompiledRaceDetector(abort_on_error=False, *, target=None):abort_on_erroris the eager factory's parameter, kept and unused (a separate PR, #502, retires it everywhere);targetnames the IR target as inSanitizer(compile=True, target=...).Snapshots.
before_launchcallsIRClient.before_launch(which records the event) and reads the binding it recorded from the new neutralArtifactLog.last_binding; then, for each tensor argument, it takes its pre-launch contents when they can be read: an integer or bool, contiguous tensor of 1 to 16384 elements (SNAPSHOT_MAX_ELEMENTS), with no lazy conj/neg, while the launch's snapshots stay under 256 MiB (SNAPSHOT_CAP_BYTES). Otherwise the tensor'sLaunchTensorcarries the reason ("float dtype ...", "too large (... elements, bound 16384)", "non-contiguous", ...), which a refusal names. No kernel has run at that point, in an IR-only trace and in a mixed one (the host compile comes before the interpreted run).Configs. Every config the launch compiled is analyzed against its own bindings, grouped by config kwargs as the compiled sanitizer groups them. Configs whose TTIR (minus its source locations), scalar arguments, tensor facts and snapshots, grid and binding errors are the same share one solve; autotune configs that differ only in
num_warpsornum_stagescompile to different kernels with the same TTIR. There is no launch-level time budget: each solver has its own timeout (T0 10 s, T1 120 s, theT0_TIMEOUT_MS/T1_TIMEOUT_MSclass attributes), set on the solver object, never throughz3.set_param.Compile failures go through
tilelens/ir/failures.pyexactly as in the compiled sanitizer: an error no target compiles past is a note; any other is acompile-failedconfig whose refusal namesRaceDetector(compile=True, target=...), or TILELENS_IR_TARGET; a host compile that could not run ishost-compile-unavailable; a launch with no JITFunction isno-compiled-kernel, naming the eagerRaceDetector().Verdicts. A config is:
ok, with itsrungandqualifiers;races, with itsCompiledRaceRecords; possible races under over-approximation found beside them are withheld, and a launch note counts them;unsupported, with aRefusal: the reader's kind (prefixed with the site), the race detector's own kind,timeout/unknownfor an undecided Z3 query (the launch-grid requery included: its any-grid races are then kept asgrid_fragileevidence, never as races of the launch), orpotential-racewhen every race found is between over-approximated records (a dropped mask, a loaded value with no snapshot);vacuous: no execution satisfies the premises, so no proof is claimed.The launch's status is
racesif a config races, elseunsupported, elsevacuous, elseok. Forok,RaceVerdict.scopesays what the weakest config's proof holds for:any-params-any-grid(T0),this-params-any-grid(T1) orthis-params-this-grid(T1-launch). The qualifiers areassumes no i32 overflow(the widths named are all those whose obligations can fail in the proof's domain: one query per width until none is left, since one model need not show them all),assumes termination(the kernel awaits a flag) andreads tensor contents(the verdict read a snapshot, so it holds for those contents).Records (
tilelens/clients/race_detector/data.py), plain data with no Z3 terms and no tensors:CompiledRaceRecord(RaceRecord): the eager record's fields (race type, both accesses with op, write, tensor name, source location and element; the witness program ids; the reason) plus the config. An element is counted from the launch tensor's data pointer;RaceConfigVerdict(ConfigVerdict):rung,qualifiers, andgrid_fragile, the any-grid races behind a T1-launch proof, or behind anunsupportedconfig whose launch-grid requery Z3 did not decide. These are evidence only, never races of this launch;RaceVerdict(IRVerdict):qualifiers, the configs' union.The shared
tilelens/ir/verdict.pyis unchanged.trace_ioalready registersdata.py, so nothing is added there.Threads. Everything the analysis builds lives in Z3's main context, which is not thread-safe.
CompiledRaceDetector._solveholdshb_common.MAIN_CONTEXT_LOCK(one process-wideRLock) aroundanalyze_graphand the conversion of its outcome to plain data, and drops the outcome before releasing it; a refusal kept in an outcome has its traceback cleared, and so does a contained internal error, so no frame full of Z3 terms outlives the lock and is collected on another thread. The eager detector's solve takes the same lock (its record building, during the interpreted run, does not). Traced launches on several host threads therefore take turns.Ctrl+C. When Z3's own SIGINT handler cancels a query (unknown, "interrupted from keyboard"), the analysis raises
KeyboardInterrupt, as the compiled sanitizer does, instead of reading the query as undecided and asking the next rung.Qualifiers are attached to
okandracesconfigs only; an undecided config carries none.configs_ofandsaveable_configmove from the compiled sanitizer's client totilelens/ir/capture.py, unchanged, so both clients use one copy.ArtifactLoggainslast_binding, the binding the latestrecordreturned.2. Program-id names (
tilelens/clients/pids.py)A neutral move: one place for the program-id and grid variable names:
PID_AXES,pid_name,pid_var(axis, ctx),grid_name,grid_var(axis, ctx), andPID0–PID2in Z3's main context. It touches three other clients, with no change in behaviour: the eager engine'sSymbolicExpr.PID0–PID2and node labels, the solver's per-copy pid rename (its helper is now_main_context_pids) and the compiled sanitizer's environment pids and witness keys now use it, with the same Z3 names as before.PID0–PID2are created on first use: created at import, they made main-context Z3 terms when the compiled sanitizer imported the module, and its own per-check contexts then picked other models (25 of the 977 corpus rows got other witnesses from the same queries).3. A reader fact: the tile shape (
tilelens/ir/ttir_reader.py)AccessEvent.shapeis the access's tile shape, read from the pointer operand's tensor type (tensor<16x16x!tt.ptr<i32>>is(16, 16), a scalar pointer()). Nothing else in the reader changes, and the compiled sanitizer does not read it (its queries and results are the same, see Tests).4. Leaves (
compiled/leaves.py)RaceLeavesis the race detector'sTermLeaves, in Z3's main context (ctxNone), so the solver's per-copyz3.substituterename applies to what it lowers:pids.pid_var(axis), the grid sizepids.grid_var(axis);start + lane, one lane per(dim, extent)(the reader's contract), registered in the solver's arange dict so each copy has its own lane in[0, extent);If(mask, snapshot[offset], other), the snapshot an array whose elements are asserted equal to the launch's contents, read at the load's own width with the signed reading (i1/bool included). Constant and affine offsets fold throughsnapshot_lookup. A width that differs from the tensor's element width, a non-contiguous tensor, a view smaller than its allocation, a tensor the kernel writes, an unsigned tensor holding a value the signed reading makes negative (auint8of 128 or more: anextuiof it would fail its width obligation although the program computes it exactly), or one with no snapshot leaves the value free, and the record that reads it is over-approximated. Each read addsmask -> 0 <= offset < numelas a domain premise;DataDep) refuses withunmodeled-value;//and%go throughBoundedDivRem, the race detector'sdivremfor the sharedLowerer(a new hook; its default is the truncating encoding the lowering had, which the compiled sanitizer keeps, so its queries are unchanged). It takes inclusive bounds over the domains every query assumes and nothing else: constants, the launch's parameters (none at T0), program ids>= 0, the grid>= 1, lanes in[0, extent), and+ - * // % min max, selects and casts over them; never a mask, a path, a launch pin or a loop's range (a loop's iteration range is a premise of its own accesses' records only). With a dividend>= 0, Z3's Euclideana / banda % bequal the IR's truncating ones for either sign ofb(%is guarded atb == 0, where the truncating remainder isa, unlessb's sign is known); with a dividend<= 0and a divisor of known sign, the same on-awith the sign re-applied; otherwise the truncating encoding with its sign branches. A grouped program-id swizzle (pid // width,pid % group_size) no longer branches on signs (see Validation).Refusals are
RaceRefusals with the race detector'sRaceKind(spin-shape,cas-value,control-flow,indirect-address,snapshot-bound,unmodeled-value,missing-binding,loop-bound,integer-overflow,timeout,unknown,potential-race,vacuous,other, and the client's kinds), not the reader's closedTTIRKind. Walkers over terms (terms_below,loaded_leaves,observed_leaves, the T0 linearity gate, the CAS gate) are built onlowering.children/foldand step into aLoaded's offset, mask and other. #361's own walker missed products under anIntCast.5. Records (
compiled/global_records.py)The
Encoderbuilds the solver'sAccessEventRecords from the access graph, ported from #361'sencode_graph/encode_graph_t0/_record_for:k = 0; a non-constant or non-positive step refuses (loop-bound);indirect-address,snapshot-boundfor a table over the bound);reach(poll) ∧ held -> exit, and where the poll is reached the obligations' failure joins the encoder's violations, so the qualifier and the launch discharge cover them ((pid * big) // bigas the expected flag value wraps for one program, which then reads the payload unsynchronized: a race);depsfrom the reader feed the solver's dependency order, closed transitively through an intermediate access only when it runs wherever the dependent does (no dropped mask, no unmodeled branch, its mask and path conjuncts among the dependent's, a constant-true mask counting as none): causal order composes lane for lane, but where an atomic is masked off, the value the store writes is not the load's. Without the closure a load → unmasked exchange → store chain at one lane was a false WAR ([FEAT] [RACE DETECTOR] Z3-based race detector #361'stest_atomic_rmw_mutual_atomicity); with it ungated, a masked atomic in the chain hid a real WAR;AccessEvent.shapewith an extent above 1) carries its shape for the solver (symbolic_expr, a stub with.shape) and, when every dimension of extent above 1 is named by the lane variable of that(dim, extent)occurring in its lowered address or activity, alane_position(one lane variable per dimension, 0 along an extent-1 one). A tile with a dimension no lane variable names (a broadcast pointer,p + tl.zeros([B]),tl.broadcast_to(row_ptr, (B, B))) has none, so the solver reads its lanes as not named (distinct lanes at any assignment) and its B lanes on one element are compared; such a tile is no dependency anchor either. Before, compiled records carried neither, so the solver read a tile whose address used no lane variable as one scalar and proved stores of B different values to one element race-free.Width obligations. Each access's
width_obligationsbecome one premise, guarded withlowering.GuardDagso an obligation holds only where atl.wherearm reads its term (an unguarded premise could exclude real executions). A signed quotient that does not fit is undefined in either arm, so it is never guarded. For each obligation the encoder also records where it fails while it matters: a loop's obligations where the loop is reached, guarded by the access's path and the outer loops' iteration premises (the increment also under the loop's own); path obligations at every iteration; mask obligations where the path holds; offset obligations where path and mask hold. A loop under a branch is guarded by that branch this way, which is what #515 left to this PR (LoopInfohas no path).CAS gate. A CAS operand may be a constant, an observation, a comparison of those,
not/and/orof comparisons, theextuiof an i1 condition to the operand's width ((pid == 0).to(tl.int32)), or aselectof these. Any otherIntCastis refused (cas-value, "changes an integer's width"), and so are arithmetic, loaded operands, unmodeled observations and float CASes; a CAS in a loop iscontrol-flow. #361 refused a CAS when any value-changing integer cast appeared anywhere in the graph.6. Fences (
compiled/fences.py)The race detector's fence rule, passed to the solver through #503's
fence_betweenhook and #505'sfence_epochhook:ttg.barriercounts only when its address space has both the global-read (2) and global-write (4) bits (Triton 3.8'stl.debug_barrierprintsall);a < f < bonly when its block path is a prefix of both accesses' block paths, so a barrier under a branch orders nothing outside it;scf.fororders the two accesses of one iteration around it; it orders every pair of executions only when another barrier of that loop body lies outside[a, b](one on the back edge: for iterations i < j, a_i < f_i < b_j; for i > j, b_j < g < a_i). That back-edge case goes past "a loop barrier orders same-iteration pairs only"; it is sound, and called out here for review;epoch(record)counts the barriers run before an execution, as an int or an affine term in the loop iteration indices. It is exact for unconditional barriers with constant trip counts; anything else (a conditional barrier, one inside awhile, a symbolic trip count) gets a fresh unconstrained epoch.#361 placed each barrier at a float position between sequence numbers, which ordered a conditional barrier's accesses as if it always ran, and a loop barrier's accesses across iterations.
7. Awaits (
compiled/awaits.py)A pass over the reader's
WhileRegionfacts decides whichscf.whileloops are spins on a flag, with #361's rules (its two structural ones, anscf.conditionoutside a condition region and a spin without one, are now the reader's refusal and rule 8): no carried values; not nested in anotherscf.while(inside anscf.forit may be); the body is onlyscf.yield; no store in the condition region; exactly one access, not float; an RMW poll is an add, or or xor of 0; the condition is a comparison; the poll is compared against a value that does not depend on it. Anything else refuses withspin-shape, the race detector's kind. It returns{access: exit predicate}as the race detector's own table (the reader marks nothing as awaited). The verdict then assumes the spin terminates.8. Rungs (
compiled/analysis.py)analyze_graph(graph, params, tensors, grid)picks the least concretization that decides a kernel, ported from #361's_solve_one_graph,_try_t0,_t1_content_freeand_launch_scoped_requery:unsupported(timeout/unknown) and the any-grid races are evidence only: they may lie outside the launch's grid.unsupported(integer-overflow, ortimeout), since the unbounded reading it proved is not what this launch computes. Sotl.store(out + ((pid * 2**30) // 2**30) % 3, 1)is proved with the qualifier at grid 2 and refused at grid 3, where program 2 wraps to element 0. A content-free T1 attempt refused this way leaves the snapshot attempt to decide.Not in this PR
LAUNCH = "run"; several IR targets in one trace._snapshot_constant, which specializes a loop bound that is constant over a correlated table (rowptr[pid + 1] - rowptr[pid]); constant offsets still fold throughsnapshot_lookup.Known limitations
assumes no i32 overflowholds where the obligations hold; they hold at this launch, but may fail at another grid (or, at T0, other parameters) in the scope it names.atomic_xchg(flag, pid * BIG)) can wrap without a premise; a wrap-capable RMW chain falls to the solver's unknown read-from. The CAS gate refuses arithmetic, so CAS operands cannot.store; if p >= n: return; barrier; load) orders nothing: the block-path prefix rule does not reach across cf blocks, so the pair races ([FEAT] [RACE DETECTOR] Z3-based race detector #361 proved it). A grid-stride loop (step = tl.num_programs(0)) is refused asloop-boundeven with a concrete launch grid.min(grid_m - group_id * GROUP_M, GROUP_M)group size of a grouped swizzle) has no bound, so a remainder of it downstream is the truncating encoding.indirect-address).Config.pre_hookdoes not run underLAUNCH = "skip", so a verdict that reads tensor contents read them as the launch began, not as a config's pre_hook would leave them.assumes termination).unsupported(timeout).Tests
TTIR is compiled at test time; crafted texts are inlined; no
.ttirfiles are checked in.tests/unit/race_detector/compiled_kernels.py: 47 small kernels and their signatures,ttir()/graph()throughtests/unit/ir/ttir_corpus.py(cuda:89). Every kernel is built withreal_jit, which pinsknobs.runtime.interpretoff aroundtriton.jit(asttir_corpus._loaddoes), so the kernels are JITFunctions even whentests/unit/test_multithreading.py(which setsTRITON_INTERPRET=1on import) is collected first.tests/unit/race_detector/test_compiled_analysis.py(33 rows,analyze_graphdirectly): one race per class between two programs (WAW, RAW, WAR, atomic vs load) with their source lines; a proof per rung (T0 with and without the qualifier, T1, T1-launch with grid-fragile evidence and its racing grid-3 twin); aliased views of one buffer race (no T0); a loaded mask proved by the content-free attempt without the content qualifier, and the same mask with no snapshot as a possible race only; a snapshot proof with the content qualifier, a duplicate index that races, the refusal without a snapshot, a view of a larger allocation refused, a table the kernel writes refused, and a masked load'sotherracing; fences (unfenced race, global barrier proved, conditional barrier races, loop barrier orders one iteration only, a back-edge barrier orders all); dependency order (in place proved, shifted races, a load → exchange → store chain proved, the same chain through a masked-off atomic racing); tiles whose lanes no variable names (B lanes on one element, a broadcast row pointer) racing beside their named or masked twins; awaits (an acquire spin proved, assuming termination; a relaxed poll, a poll for the initial value and a skipped poll race; a wrapping exit value racing at grid 5 and proved at the launch at grid 2); a countingwhilerefused withspin-shape; a CAS lock; four fenced CAS handoffs; the CAS gate (extuiof a comparison accepted, a narrowing cast and arithmetic refused, a CAS in a loopcontrol-flow); an early return, two sequential loops and their overlapping twin, nested loops, an in-place update after a read-only loop and inside a second loop (both proved at T0); width obligations (a loop under a branch whose bounds wrap only where it is not reached, the Select-guarded premise oftl.where(pid >= 2, 0, pid * 2**30 // 2**30)racing at grid 3, a qualified T0 proof at grid 2 and its refusal at grid 3, a qualifier naming both i32 and i64 where no one execution wraps both, a launch that wraps refused); a timeout; an undecided launch-grid requeryunsupported(timeout) with the any-grid races as evidence only; a cancelled query raisingKeyboardInterrupt.tests/unit/race_detector/test_compiled_rules.py(35 rows): the pid-rename contract;//and%without sign branches exactly where they equal the truncating encoding (9 cases: non-negative, non-positive and unknown dividends, positive, negative, zero and unknown divisors, a lane and a select dividend), proved equal to it under the domains; lanes keyed by(dim, extent), the reader's tile shape and the lane position; signed parameters and symbolic T0 parameters; the unmodeled-value refusal; snapshot width rules, the signed reading and the unsigned refusal; 4 accepted spins and 10spin-shaperefusals; the failed polls of an await: a cta-scope reset of the flag (WAW) and a plain read of it (WAR) race the pre-exit representative, the no-reset and gpu-scope reset twins are proved; the barrier address-space rule end to end; fence epoch forms.tests/end_to_end/test_compiled_race_detector.py(49 rows, throughtilelens.trace(RaceDetector(compile=True))on CPU tensors with the driver unreachable):Launch.records, the per-config counts and the printout;spin-shape, the CAS gate, an early return, several loops, a loop under a branch, the width qualifier, the guarded premise, the any-grid proof refused at a launch that wraps,potential-race;snapshot-bound) and the launch's byte cap;inject_ttir, races;unsupported(timeout); an undecided launch-grid requery records the verdict alone, noCompiledRaceRecord, with the evidence ingrid_fragile;num_ctas=2configcompile-failedbeside a proved one; a racing config beside acompile-failedone makes the launchraces; configs proved at T0 and T1-launch give the launch scopethis-params-this-grid; three configs, two sharing a TTIR, solved twice; bindings of one specialization that differ only in grid or only in snapshot contents solved apart;tilelens.save()/load();tile-race --compileon a script (subprocess).tests/unit/ir/inject.py:inject_ttir(traced, text_or_fn).tests/unit/ir/test_lowering.py: a client'sdivremencodes every quotient and remainder.tests/unit/test_race_detector.py:RaceDetector(compile=True)is aCompiledRaceDetector;SymbolicRaceDetector(compile=True)raises; the flag off gives theNullRaceDetector.Commands, Triton 3.8.0, CPU only (
CUDA_VISIBLE_DEVICES="",UV_NO_SYNC=1):The base is a pristine worktree of #528's head (fca42ef) with the same setup.
-n 4-n 4-n 4tests/, serialThe 118 new rows are the 68 race detector unit rows, the 49 end-to-end rows and the lowering row above. The new tests also pass with
tests/unit/test_multithreading.pycollected first (127 passed).A pre-existing order leak, not from this PR:
tests/unit/test_patch_scope.py::test_scope_restores_tensor_descriptor_base_builtinsfollowed by::test_patch_lang_restores_core_reduce_scan_from_tl_kernelleavestriton.languagepatched, so every later host compile in that process fails (host-compile-unavailable, "'block_argument' object has no attribute 'dtype'"). Under-n 8the scheduler sometimes puts the two on one worker without the rest of their file; then that worker's compiled tests fail. On the base, the same two tests beforetests/end_to_end/test_compiled_sanitizer.pyfail 57 of its 66 tests. It is left for a separate fix.Compiled sanitizer unchanged. The 977 Triton 3.8.0 TTIRs of #509–#528, each parsed with the default reader and checked with
check_graphunder #509's synthetic binding, against a pristine worktree of #528's head: the 527 graphs are identical apart from the newAccessEvent.shapefield (their reprs compared with it removed), the 450 refusals are identical, and so are all 6668 Z3 queries (Solver.sexpr()at everycheck) and the findings, refusals and abstentions in kind, access, base, line and loc. The full result reprs, witnesses included, are identical in some runs; in others one witness differs, that of thec_ptrstore oftb_matmul_dequantize(element offset 4096 or 8064). It differs the same way between two runs of the pristine base: it is the Z3 model choice #528 reports for its base and its head.#361's unit suites, as a baseline (not committed; measured on this PR's first revision, before the review fixes):
test_t1_global_races,test_multipath_races,test_route2_snapshot_select,test_await_abstractionandtest_compiled_casfrom 62c6d7c, run against this code through a shim of #361's API (crafted texts given argument names, five invalid texts fixed,tl.debug_barrieradded where a test relied on #361's program-order default): 90 passed, 47 failed, no verdict different. The 47: 20 call #361's client API and 11 its evaluation harness; 1 is a replay watchdog test (next PR); 2 use texts that are invalid MLIR; 4 read theAccessEvent.awaitedfield the reader no longer has; 3 test the single-path mode, which is gone; 3 count local constraints, which the width premise adds one to; 3 match #361's refusal wording; 1 expects a refusal the reader now raises while parsing.Validation
Row by row against #361's compiled (static) track at its shipped level, on the 207-row Triton 3.8.0 sample used for the earlier PRs of this stack. The oracle is #361 (62c6d7c) run through its own harness (
run_oneat its shipped level, the host-compiled TTIR rewrittenttg.barrier all->gpu.barrier, which #361 does not read on 3.8). Its witness replay (confirmation) is ignored; it comes with the next PR. This PR runs each row's launch undertilelens.trace(RaceDetector(compile=True, target="cuda:80")): host compile only, CPU tensors, no GPU.cuda:80is the target #361's harness compiles for on a host with no GPU. The spec loader and argument binding are the oracle's; no TTIR is rewritten for this PR. Rows ran 16 at a time.Compared per row: the status; for a proof, the rung and the content / termination qualifiers (
proved@T1-launch+content, ...); for races, the set of witnesses (race type, first line, second line).This PR's first revision had three port bugs, found by this validation and fixed in this revision (below). On this revision:
Two runs of this revision give this table; between them only the witness program ids of two racing rows differ (Z3's model choice), with the same race pairs.
fp8e4nvkernels and liger'stl.float32(...)fail to compile forcuda:80. [FEAT] [RACE DETECTOR] Z3-based race detector #361's harness records a compile error; this PR recordsunsupported(compile-failed). It is the same failure under this PR's name.fla_ttt_fused_chunkloads through a pointer that anscf.ifyields. [FEAT] [RACE DETECTOR] Z3-based race detector #361's reader refused it ("load of a non-pointer value"); this stack's reader reads it. The kernel is proved at T1 (assuming no i32 overflow). The load is ofeta, which the kernel never writes.trb021_guarded_acq_rel_no,trb021_role_specific_order_no,trb022_acquire_on_failure_no,trb026_fenced_tile_handoff_no. At grid 3 the any-grid query finds a RAW whose read-from source is a third program's CAS ([FIX] Close two false proofs in the race detector's two-copy model #505's open-world read-from). At the launch's grid (2) that query is UNSAT, so these rows are proved at T1-launch, with the grid-3 RAW kept as grid-fragile evidence. The grid-3 report over-approximates: the third program's CAS succeeds only after it reads the producer's release, so it extends the producer's release sequence. This difference was registered in advance.The port bugs, and what changed with each fix:
fla_delta_rule_fused_recurrent(bwd),fla_gated_delta_rule_chunkandfla_gated_delta_rule_chunk_varlen(prepare_wy_repr_bwd),fla_simple_gla_parallel(bwd): [FEAT] [RACE DETECTOR] Z3-based race detector #361 proves each of them, and the first revision reported a WAR inside one program between an in-placetl.load(p)and the dependenttl.store(p, ...). In the witness both copies were at the same lane and the same iteration of the accesses' own loop, and differed only in the iteration index of another loop.Encoder._recordput the iteration variable of every loop of the kernel in each record'scopy_local_vars, which the solver reads as the record's loop iterators, so a dependency-ordered pair could "differ in iteration" in a loop neither access is in. Minimal repro: a loop that only reads, thenv = tl.load(p); tl.store(p, v + acc)after it or inside a second loop (the one-loop twin was proved at T0). A record now lists only its own loops, plus the loops of an awaited poll, whose premise every record carries. The four rows match [FEAT] [RACE DETECTOR] Z3-based race detector #361; the repro is a regression test (read_loop_then_inplace, proved at T0 both ways).tritonbench_meta_gemm_splitk_kernel: [FEAT] [RACE DETECTOR] Z3-based race detector #361 proves it at T1-launch, in a 46 s row. Here the any-grid query finds a WAW, and in the first revision the requery at the launch grid took 104 to 142 s alone (170 s with 16 rows running), against the 120 s T1 timeout. When it did not finish, the config was reported asraceswith the any-grid witness, programs(1792, 0, 0)and(1792, 1, 0), neither a program of the launch's grid(2048, 1, 1). [FEAT] [RACE DETECTOR] Z3-based race detector #361 did the same. An undecided requery now makes the configunsupported(timeout/unknown), with those races kept asgrid_fragileevidence only (unit and end-to-end regression tests).//and%as truncating division with sign branches, even for a constant positive divisor and a program-id dividend; on the kernel's grouped program-id swizzle this is what made the requery slow (with a measurement-only Euclidean//and%everywhere, the row was proved in 37 s). The divrem hook andBoundedDivRem(Change, section 4) bring back [FEAT] [RACE DETECTOR] Z3-based race detector #361's bounds-guided division._splitk_kernelis now proved at T1-launch in 16 s in the 16-row run (20 to 39 s in four more runs beside other jobs; in the 39 s one the requery took 34 s), as [FEAT] [RACE DETECTOR] Z3-based race detector #361 proves it.tut03_matmul_leaky_relutakes 3.1 s, against 15.0 s on the first revision and 2.0 s for [FEAT] [RACE DETECTOR] Z3-based race detector #361. The rows' analysis times add up to 505 s (525 s in a second run), against 702 s for the first revision with only the false-race fix. Apart from this row, no status, rung or witness changes.One more defect showed up in the same comparison: the width qualifier named the widths that fail in one model of the obligation query, so which widths it named depended on Z3's model choice (between two runs with different division encodings, one proof went from
i32/i64toi32and another fromi32toi32/i64). It now asks again until no further width can fail; 15 proofs name one more width (i32/i64where one was named), with no change in status, and a regression row (two_widths, whose i32 and i64 products wrap on disjoint paths) checks it. The width qualifier appears on 79 of the 123 proofs (assumes no i32 overflowon 55,assumes no i64 overflowon 6, both on 18).Race-labelled rows: the sample has 52 rows labelled race; the port reports
raceson all 52. None is proved race-free.Advance-registered differences that did not show up in this sample: the conditional-fence and loop-fence rules, launch-time width refusals (the
rv_*rows are not in the sample), lane keying, and the CASextuigate.After the fixes the race detector suites, the IR suites, the regression gate and the compiled sanitizer equivalence were run again (the Tests numbers above are this revision's).
Driver:
run_row_compiled.py(one row) andcompare.py, in the validation scratch directory.Line counts
From
git diff --numstatagainst #528's head (fca42ef):tilelens/clients/race_detector/compiled/global_records.py(new)tilelens/clients/race_detector/compiled/client.py(new)tilelens/clients/race_detector/compiled/leaves.py(new)tilelens/clients/race_detector/compiled/analysis.py(new)tilelens/clients/race_detector/compiled/fences.py(new)tilelens/clients/race_detector/compiled/awaits.py(new)tilelens/clients/race_detector/compiled/__init__.py(new)tilelens/clients/pids.py(new)tilelens/clients/race_detector/data.pytilelens/clients/race_detector/race_detector.pytilelens/clients/race_detector/hb_common.pytilelens/clients/race_detector/two_copy_symbolic_hb_solver.pytilelens/clients/symbolic_engine.pytilelens/clients/sanitizer/compiled/client.pytilelens/clients/sanitizer/compiled/oob.pytilelens/ir/capture.pytilelens/ir/lowering.pytilelens/ir/ttir_reader.pytilelens/wrapper.pytilelens/clients/__init__.pytests/unit/race_detector/compiled_kernels.py(new)tests/end_to_end/test_compiled_race_detector.py(new)tests/unit/race_detector/test_compiled_analysis.py(new)tests/unit/race_detector/test_compiled_rules.py(new)tests/unit/ir/inject.py(new)tests/unit/ir/test_lowering.pytests/unit/test_race_detector.pyREADME.mdThe tests add fewer lines than the product. About 80 of the product lines are moved, not new: the two config helpers leave the compiled sanitizer's client for
tilelens/ir/capture.py.