Repository navigation
Conversation
Read the facts a compiled race detector needs from the TTIR, without any race-detector policy in the reader and without changing what the compiled sanitizer sees: - the walk reads tt.atomic_rmw's atomic_rmw_op / sem / scope, tt.atomic_cas's sem / scope and ttg.barrier's addrSpace bits through get_int_attr, each against a closed vocabulary; - AccessEvent.atomic (AtomicInfo), atomic_val / atomic_cmp as Terms, elem_float from the accessed pointer's pointee, block_path, and a declared deps: the earlier loads and atomics whose result reaches a store's value and mask, an atomic_rmw's val and mask or an atomic_cas's cmp and val lane for lane, the operands united (element-wise ops keep it, tt.dot only through its accumulator, arith.select through its condition or both arms, anything else and region results drop it); - AccessGraph.pid_axes (every program-id read in the module, combine regions included), and a Fence for every ttg.barrier with its position among the accesses, addrSpace bits, path, guarded, in_loop, block path and loc; - observed_indices(term, graph), which reaches through loop-carried pointers like mentions_observed; - parse_ttir(while_regions=True) reads each scf.while as a WhileRegion (before / after accesses and blocks, the scf.condition term, the body's ops, carried values, nesting) and binds an integer load of its condition region as Observed; by default scf.while is still refused; - ArtifactLog.record returns the binding it records.
mark14wu
added this pull request to stack #508
October 14, 2026 20:45
Performance Benchmark
Iterations: 1 warmup + 20 measured |
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 TTIR reader facts that the compiled race detector (a later PR) needs. The facts are neutral: the reader reports what the IR says and puts no race-detector policy in. The compiled sanitizer, the reader's only client today, gets the same graphs and the same results as before (see Fact validation).
Stacked on #505 (top of #507 → #494 → #479 → #495 → #480 → #482 → #501 → #503 → #504 → #505). Triton 3.8 only. One commit.
Each fact is read from Triton's MLIR bindings: the walk's attribute table gets new rows, and nothing new is read from the text.
Change
1. Atomic attributes (
_mlir_walk._ATTRS)Six attribute rows under three new op keys, each read with
get_int_attrand checked against a closed vocabulary, the same way the walk already readsarith.cmpi's predicate:tt.atomic_rmw:atomic_rmw_op1..10 =and,or,xor,add,fadd,max,min,umax,umin,exch;sem1..4 =relaxed,acquire,release,acq_rel;scope1..3 =gpu,cta,sys;tt.atomic_cas:sem,scope;ttg.barrier:addrSpace, which is a bit enum and is kept as its bits (local1,global_read2,global_write4,tensor_read8,tensor_write16;all, whichtl.debug_barrier()prints, is 31).All of these are inherent attributes that the op verifiers type as signless i32, so
get_int_attrcannot hit its assertion. An integer outside the vocabulary makes the module misaligned, as for the existing rows. The reader reports the printer's keywords (exch, notxchg).2. Per-access facts (
AccessEvent)atomic: AtomicInfo(rmw_op, sem, scope), set exactly on atomics (rmw_opis None for CAS).atomic_valandatomic_cmp: the operands in ODS order,tt.atomic_rmw (ptr, val, mask?)andtt.atomic_cas (ptr, cmp, val), each as a Term, or None when the operand is loaded data, a float or a pointer.elem_float: whether the accessed pointer's pointee is neither an integer nor a pointer, read at the access, so a float type the walk does not list still reads as float.FuncArggets no such field. This differs from [FEAT] [RACE DETECTOR] Z3-based race detector #361 by design: [FEAT] [RACE DETECTOR] Z3-based race detector #361 read the base parameter's pointee. The two disagree on width-preserving pointer bitcasts: the fmax atomics ofgolden_atomic_fmaxgo through i32 views of an f32 pointer and report False here (an integer observation, which is also how the reader binds their results). [FEAT] [RACE DETECTOR] Z3-based race detector #361 refuses that text, so the corpus shows no atomic disagreement.deps: now a declared field. It lists the earlier loads and atomics whose result reaches the access's value operands lane for lane: a store's value and mask, anatomic_rmw's val and mask, anatomic_cas's cmp and val. Loads have(). The walk runs backwards over the bindings' def-use edges and is memoized per value:arith.*,math.*,tt.precise_*,tt.addptr,tt.bitcast,tt.fp_to_fp,tt.int_to_ptr,tt.ptr_to_int,tt.clampf,tt.mulhiui,tt.extern_elementwiseandtt.elementwise_inline_asm;tt.dotkeeps only its accumulator's;arith.selectkeeps its condition's, plus those that both arms have;depsadds happens-before edges, so it must be exact or narrower, never wider. The per-op rules match [FEAT] [RACE DETECTOR] Z3-based race detector #361's. Two results differ from [FEAT] [RACE DETECTOR] Z3-based race detector #361, both on shapes the 977-TTIR corpus does not contain:tl.store(out + offs, x, mask=tl.sum(x, 0) > 0)this reader gives(0,)(the value readsxlane for lane) and [FEAT] [RACE DETECTOR] Z3-based race detector #361 gives(). This is a wider edge than [FEAT] [RACE DETECTOR] Z3-based race detector #361's but an exact one: the store reads both operands at the lane, they are not alternatives. A test row pins it. Because it widens a happens-before edge relative to the frozen reference, it is called out here for review.%2:2 = tt.elementwise_inline_asm ... packed_element = 1stored through%2#0, this reader gives the asm's operand as a dep and [FEAT] [RACE DETECTOR] Z3-based race detector #361 gives none (its text reader keyed provenance by%2while the uses read%2#0). The dep is correct for one element per pack.A packed inline asm (
packed_element > 1) counts as element-wise in both readers, although the asm sees its whole pack and could permute inside it. The lanes of one pack sit in one thread, so a same-position pair stays in that thread. All 5 inline asms in the corpus havepacked_element = 1.block_path: the access op'sOp.path, the block indices from the module body down to the access's own block.3. Per-graph facts (
AccessGraph)pid_axes: every axis that att.get_program_idortt.get_num_programsreads anywhere in the module, including one whose value reaches no modeled term and one inside att.reduce/tt.scancombine region (which the builder does not walk). It is read from the walk's ops, not from the ops the builder visits, because a consumer that pins an unread axis to 1 must not miss one.fences: aFencefor everyttg.barrier. Until now the reader listed it as inert and dropped it. Each fence records:position, which islen(accesses) - 0.5at the barrier;addr_spacebits;path,guardedandin_loop, read the same way as an access's;block_path,line_noandloc.The block path is what the race detector's fence rule needs: a fence orders
a < f < bonly when its block path is a prefix of both accesses' block paths. That rule, and whichaddrSpacebits order global memory, belong to the race detector. The sanitizer does not read fences. A barrier inside att.reduce/tt.scancombine region stays inert and gives no Fence, as before (a test pins it), and anscf.whilethere gives noWhileRegion. That is the safe direction for a happens-before consumer: one fewer fence orders fewer pairs, and combine regions hold no accesses.observed_indices(term, graph): the access indices of theObservedleaves thattermreaches. It follows loop-carried pointers (anIterArgOffset'soffset0anddelta), the same walk asmentions_observed. [FEAT] [RACE DETECTOR] Z3-based race detector #361's version stopped atIterArgOffset.4.
scf.while, opt-in (parse_ttir(text, while_regions=True))With the option off (the default, and what the sanitizer uses),
scf.whileis still refused as control flow, with the same message. With it on, eachscf.whilebecomes aWhileRegioningraph.whileswith:line_noandloc;scf.conditionTerm (None when it is loaded data);scf.yieldincluded, and that region's access indices;carries);WhileRegion(parent), itsblock_path, and its before and after block indices (blocks), so an op or fence is inside the loop when its block path extendsblock_pathwith one of them.The accesses themselves are ordinary entries of
graph.accesses.in_loopstill means "inside thescf.for". Inside the before region, an integertt.loadresult (a volatile poll included) binds asObserved(index), the way atomic results already do; theObserveddocs now say an observation can index a load. Region arguments and results are loop-carriedDataDeps. Anscf.forinside a while region refuses as a nested loop. Which while loops count as an await (the spin-shape rules) is for the race detector to decide. A text withoutscf.whilegives an equal graph with the option on or off.5.
ArtifactLog.recordreturns the binding it recordsReturning the binding lets the race detector tie the values it captures in
before_launchto theLaunchBindingthat the log stores.Not in this PR
cfblocks, several loops andDataDep.keep. These belong to a later PR.addrSpacepolicy, theexch→xchgmapping.LoopInfo.induction_var. Only [FEAT] [RACE DETECTOR] Z3-based race detector #361's TTGIR reader used it, andLoopVaralready names the induction variable.Tests
The unit tests read TTIR that is compiled at test time. Seven kernels are added to
tests/unit/ir/reader_kernels.py; no.ttirfiles are checked in.test_ttir_reader.py:test_atomics_carry_their_attributes_and_operands: every rmw op, every sem and scope, and a CAS;atomic_val/atomic_cmpas Terms, and None for loaded or float operands;elem_floatat the access (the fmax atomics ofgolden_atomic_fmaxgo through i32 views of an f32 pointer).test_observed_indices_reach_through_loop_carried_pointers: an observation in an iter-arg'soffset0and in itsdelta.test_pid_axes_are_every_axis_read: axes that reach only a stored value, and a program id and a barrier inside att.reducecombine function (pid_axes == {0, 1}, no Fence).test_barriers_are_fences_among_the_accesses: positions,addr_space(includinglocalandglobal_read|global_write), path, guarded, in_loop, line/loc, and block-path prefixes: a fence is a prefix of exactly the accesses in its block.test_deps_are_the_loads_a_value_reads_lane_for_lane: each rule. Element-wise ops keep their deps (arith.*,math.exp,tt.bitcast); select by condition, by both arms, and by one arm each; att.reduce, a splat of a scalar sum, anscf.ifresult and anscf.forloop-carried value drop their deps; a load masked by a loaded value has(); the operands of one access unite;tt.dotkeeps only the accumulator's; a store's value and mask; rmw val and mask; cas cmp and val; an atomic's result; a splat. It also checkselem_floaton f32 stores.test_while_regions_are_read_only_on_request. Off: a control-flow refusal. On:parent), each condition overObserved;scf.while, and the option passed throughParseCache.test_mlir_walk.py: the generic-formtt.atomic_rmwreads its three attributes.test_oob.py,test_the_race_detector_facts_change_no_check:check_graphreturns the same result with fences, deps, block paths and pid_axes stripped, once on a clean launch and once on a launch with findings.test_ir_capture.py:recordreturns the binding thatspecializationslists.Commands, Triton 3.8.0, CPU only (
CUDA_VISIBLE_DEVICES=""):-n 4-n 4The 20 new IR rows are the six reader tests, thirteen parametrized rows for the new kernels and the
test_oobrow. The gate gains thetest_oobrow.Fact validation
Method. The corpus is the 977 Triton 3.8.0 TTIRs that were host-compiled for the race detector's validation roster (16 corpora). Each text is read three ways:
while_regions=Trueand with the default;ttg.barrier allrewritten togpu.barrierbecause [FEAT] [RACE DETECTOR] Z3-based race detector #361 only knewgpu.barrier.Graphs are compared access by access, in order. Terms are compared in canonical form, which strips widths, sites,
IntCastwrappers (#361 has none) and arange dims (the 3-D make_range fix). #361 has no block paths, so those are not compared.Parse status. Both readers parse 556 texts (39 of them with
scf.while) and both refuse 410. Eleven texts differ, and they differ the same way before this PR:Agreement on the 556 graphs (2708 accesses; the access kinds and bases line up in all 556):
atomic_valpresence; equal Term where present (112)atomic_cmppresence; equal Term where present (22)deps(378 non-empty)elem_floatobserved_indicesof offset, mask, path, val and cmp (41 non-empty)pid_axeselem_float: all 766 differences are stores to a float pointee. [FEAT] [RACE DETECTOR] Z3-based race detector #361 never setelem_floaton a store (its store path does not pass the pointee), while this reader reports the pointee for every access. Every load and atomic agrees, and those are the only accesses the race detector reads the field on.addr_space31, 56 have a path, and none is guarded or in a loop. [FEAT] [RACE DETECTOR] Z3-based race detector #361 records positions only.scf.yield-only body and no carried values. Its access is exactly [FEAT] [RACE DETECTOR] Z3-based race detector #361's awaited access, and [FEAT] [RACE DETECTOR] Z3-based race detector #361'sexit_predequalsNot(cond). The other 3 texts withscf.whileare refused by both readers. The option changes nothing on the 935 texts withoutscf.while.observed_indices: no observation in this corpus is carried by a loop pointer, so the graph-aware walk finds no more than [FEAT] [RACE DETECTOR] Z3-based race detector #361 here.test_observed_indices_reach_through_loop_carried_pointerscovers that case.The sanitizer is unchanged. The default
parse_ttirwas run on all 977 texts at this PR and on a pristine a3847f7 (with the new fields stripped):check_graphunder one synthetic binding (4096-element tensors, scalars 64, grid (4, 2, 1)) returns identical results on all 527 graphs, 268 of them with findings.A second binding (37-element tensors, scalars 300, grid (3, 1, 2)) gives the same finding kinds, access indices, refusals and abstentions on all 977 rows. Two tritonbench matmul kernels return different witness lanes and violation offsets there, but two runs of the pristine base differ in the same way: it is Z3's run-to-run choice of witness, not this PR.
Line counts
From
git diff --numstatagainst a3847f7:tilelens/ir/ttir_reader.pytilelens/ir/_mlir_walk.pytilelens/ir/capture.pytilelens/ir/lowering.pytests/unit/ir/test_ttir_reader.pytests/unit/ir/reader_kernels.pytests/unit/sanitizer_compiled/test_oob.pytests/unit/ir/test_ir_capture.pytests/unit/ir/test_mlir_walk.pyThe tests are 17 lines over the product. The overrun is the
depstest (18 of the lines added in review):depsis the one fact where an error points in the unsound direction, because a wrong dep adds a happens-before edge and can hide a race. So each rule has a row that fails when the rule is widened or narrowed:math.*andtt.bitcastas element-wise ops, a realtt.reduce, a load whose mask is a loaded value, a loop-carried value, and the operand union.