Repository navigation
Conversation
Read the facts a compiled race detector needs to analyse kernels with
several loops, early returns and integer loads in addresses, masks and
branches, under one opt-in option: parse_ttir(multipath=True). With it
off (the default, and what the compiled sanitizer uses) every field that
existed before reads as before; the new loops fields hold the one loop.
- several scf.for loops (sequential, nested, under a branch), each with
its own LoopInfo ("%loop", "%loop1", ...) and induction variable:
AccessGraph.loops in opening order, AccessEvent.loops and Fence.loops
(the enclosing loops, outermost first); AccessGraph.loop stays the
single loop and is None for several (a graph built with one of loop
and loops gets the other); a carried pointer's advance is variant
only in its own loop; width obligations per enclosing loop;
AccessGraph.loop_info(name);
- the cf blocks Triton prints for an if that holds a return: each
block's predicate (the disjunction of its incoming edges) is the
outermost conjunct of its accesses' paths, a block argument is a
Select over what the incoming edges pass, an edge under an unmodeled
condition leaves the block guarded, and an access, fence, branch or
address in a block entered before its predecessors refuses as control
flow (this also refuses cycles);
- the walk reads cf.br / cf.cond_br successors from the printed labels
(the bindings have no successor getter) and resolves them to blocks,
checking the operand counts;
- Loaded(access_index, offset, mask, other): the value of an integer
load with a modeled mask, read in the load's own type, its lanes
placed where it is read (an expand_dims re-places them); float,
pointer and dropped-mask loads stay DataDep; a value an unmodeled op
computes from a Loaded is memory-derived, so refusals keep the
default's kind; loaded_indices(term, graph), which follows loop
variables into their loops' bounds;
- DataDep.keep: the modelable conjuncts of an i1 and with an unmodelable
operand, used as a dropped mask's mask (still mask_dropped);
- lowering: TermLeaves.loaded for a Loaded leaf, LoopVar resolved by its
loop's name, children() does not descend a Loaded or DataDep.keep; the
compiled sanitizer refuses a Loaded like DataDep("loaded value").
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 second batch of TTIR reader facts that the compiled race detector (a later PR) needs: several loops, the
cfblocks of an early return, integer loads as values (Loaded), and the modelable part of a partly loaded mask (DataDep.keep). The lowering learns the matching leaf and resolves each loop variable by its loop's name. The facts are neutral: the reader reports what the IR says and puts no race-detector policy in.All four facts are opt-in, under one new option,
parse_ttir(text, multipath=True). With the option off (the default, and what the compiled sanitizer uses), every field that existed before reads as before. The newloopsfields hold the one loop a default parse can have. The sanitizer sends the same Z3 queries and returns the same results on all 977 corpus texts (see Fact validation).Stacked on #509 (top of #507 → #494 → #479 → #495 → #480 → #482 → #501 → #503 → #504 → #505 → #509). Triton 3.8 only. One commit.
Change
1. One option,
multipath=TrueThe four facts sit behind a single switch, the same way #361's reader had them behind its
multipath=True:multipath=Trueon the same texts;Each fact starts at a refusal of the default model (a second loop, a function with several blocks) or at a
DataDep. So a text without these constructs and with no integer load in a term reads the same with the option on or off (a test checks this byte for byte, with pickle, on the 36 such corpus texts).while_regions=True(from #509) stays a separate option, since the sanitizer has no use for it. The two combine.2. Several
scf.forloops (ttir_reader)LoopInfoand induction variable. The first loop to open keeps the name"%loop"and later ones are"%loop1","%loop2", and so on, in opening order.LoopVar.loop_ssaandIterArgInfo.loop_ssacarry that name.AccessGraph.loopslists every loop in opening order, outer before inner.AccessGraph.loopis set only when there is exactly one loop, so a single-loop graph reads as today (the sanitizer readsloop). A graph built with only one of the two gets the other (loops == (loop,), orloopfrom a one-entryloops), so hand-built graphs have one source of truth.AccessGraph.loop_info(name)finds a loop by name.AccessEvent.loopsandFence.loopsgive the enclosing loops, outermost first, within_loop == bool(loops).loopsis(loop,)or(), and an access's or fence'sloopsis("%loop",)exactly whenin_loop. Every field that existed before is unchanged.multipath=Truea loop may be nested, follow another loop, or sit under anscf.ifor acfblock. A loop under a branch carries that branch in its accesses'path. A loop inside anscf.whilestill refuses as a nested loop.Observed/Loadedvalue read inside that loop. Inside an inner loop, an advance by the outer loop's variable is invariant, so it is accepted. With one loop this is the old rule exactly. The tiles of a carried pointer expanded inside the loop take their delta when their own loop closes, and they carry its name.width_obligationslists the bounds and increment obligations of every enclosing loop, outermost first, all under role"loop". They hold wherever the loop is reached. That is everywhere unless a multipath graph nests the loop under a branch.LoopInfocarries no path of its own; the docstring now says that the access's own path implies its loops are reached, so it is a sound guard for them.3. The
cfblocks of an early returnTriton prints an
ifthat holds areturnas basic blocks of the function body:cf.cond_br,cf.brandtt.return. By default these are still refused as control flow, with the same message. Undermultipath=Truethe reader walks the blocks in printed order:cf.br. A block with no incoming edge gets0 != 0, and its arguments areDataDep("argument of an unreachable block"). The predicate is the outermost conjunct of thepathof every access and fence in the block, ahead of anyscf.ifconditions.Selectover its incoming edges' operands: the last edge's operand is the fallback, and each earlier one is selected under its own edge path. Pointers that share a base select their offsets. Pointers of two bases, and any incoming value that cannot be modeled, make the argumentDataDep.guarded, and its arguments areDataDep(aSelectover inexact paths would pick a wrong value, not just a wider footprint).control-flow), and so does an address made from one of its arguments. This also refuses every cycle: the first printed block of a cycle is entered before one of its predecessors, and its branch refuses. Triton never prints a cycle. None of the 977 corpus texts has a shared tail printed early; reading the blocks in reverse post-order would let such kernels parse, and is left for when one shows up.cf.switch, a branch in generic form) refuses. That check runs before any block is read, so it is reported ahead of an earlier refusal in reading order.tt.ptr_to_intreaching inline asm or an extern function) now follows a block argument back to the operand each branch into its block passes at that argument's position.These are #361's rules for its block walk, with one reorganization: this reader reads the block structure from the op tree instead of a text pre-scan.
4. Branch successors in the walk (
_mlir_walk), the one text-scan pieceTriton 3.8's MLIR bindings give an op's name, operands, results, regions, block and attributes, but no successor getter.
ir.operationhas none, andir.blockhas no successor or predecessor accessor either. So the targets of a branch can only come from the printed text. The text layer already reads block labels.For a
cf.br/cf.cond_brin custom form, the text scan reads each printed successor label and the number of operands it passes (^bb5(%a, %b : i32, i32)passes two). The aligner then resolves each label to a block index of the same region, and checks all of the following:cf.br) or two (cf.cond_br);cf.cond_br) plus all the successor operands.The result is
Op.attrs["successors"], the target block indices. A mismatch, or a branch outside a block, makes the module misaligned, like every other disagreement between the two layers. A branch in generic form gets nosuccessors, and the multipath reader refuses it. Operands are split per successor by the target blocks' argument counts, in ODS order: condition, true-target operands, false-target operands.5.
LoadedtermsLoaded(access_index, offset, mask, other)is the value an integertt.loadread. It is bound undermultipath=Trueinstead ofDataDep("loaded value"):DataDep. This is [FEAT] [RACE DETECTOR] Z3-based race detector #361's Int-only rule. It is what keeps float values out ofatomic_val/atomic_cmp, which take the operand's value as is (a test pins a float atomic operand). Elsewhere a float value reaches an integer term only throughcmpf, a cast to an integer or a bitcast, which areDataDepalready;elem_float, where [FEAT] [RACE DETECTOR] Z3-based race detector #361 keyed it on the base parameter's pointee. So the element is read in the load's own integer type: through a same-width pointer bitcast (anf32base read asi32) aLoadedis the base's bits reinterpreted, not its value converted. The docstring says so. No corpus text has this shape;offset,maskandother(None when absent or not modelable) are the load's own terms. They give the value's lane placement: the value at a lane is the element atoffsetwheremaskholds andotherwhere it does not. Anexpand_dimsof the loaded tile re-places their aranges like any other lane term, as [FEAT] [RACE DETECTOR] Z3-based race detector #361's reader did. So, unlike anObserved, two lanes of one load may meet in a term;Loadedalso hadbase_param. That is the access's own base, so it is left out here;scf.whilecondition region (withwhile_regions=True) is stillObserved, as in [FEAT] Add the race detector's TTIR reader facts #509;Loaded, as in [FEAT] [RACE DETECTOR] Z3-based race detector #361. ALoadedread inside a loop, in that loop's pointer advance, refuses as loop-variant. [FEAT] [RACE DETECTOR] Z3-based race detector #361 had no such check;Loaded(a reduction,xori,sitofp, an i1 add, a loop-carried integer that starts as a load) is memory-derived, just as itsDataDepis by default. So a refusal keeps the default's kind (indirect-address,data-dependent-bound) rather thanother. [FEAT] [RACE DETECTOR] Z3-based race detector #361 has the gap; this reader closes it, and a test checks that the same text refuses with the same kind in both modes;loaded_indices(term, graph)lists the loads a term reads: through loop-carried pointers, likeobserved_indices, and through thelower,upperandstepof the loop of every loop variable and carried pointer it reaches, following the lowering's value relation (a loop with a loaded lower bound, the CSR shape, now parses). Whether an access runs at all also depends on its enclosing loops' bounds; the docstring says to scanaccess.loopsfor those. The reader's own walkers (_nodes,_expand_dims) descend into aLoaded's three terms, somentions_observed, the expand-dims lane check andwidth_obligationssee inside it.width_obligationsfiles a load's own address terms under the role of the term that reads the value, without the load's mask; the docstring says so (precision only).What a loaded value means (snapshot contents, a free value, a refusal) is the consumer's call.
6.
DataDep.keepDataDep(why, keep=None): undermultipath=True, anarith.andion i1 with an unmodelable operand records its modelable conjuncts, including those an operand kept itself, askeep. A mask that is such a value becomeskeepinstead of free. The access staysmask_dropped, becausekeepis still wider than the real mask: it only stops lanes that the modelable conjunct rules out. Anorikeeps nothing. This is #361's partial-mask over-approximation.keepis not a value dependency. Splat, broadcast andexpand_dimscarry it,expand_dimsre-placing its lanes. A cast drops it (a cast of an i1 widens it, so the result is not a mask), and so do every other op and a loop's iter arg. By defaultkeepis always None.7. Lowering (
lowering.py) and the compiled sanitizerTermLeaves.loaded(t): aLoadedis a leaf,Loaded -> leaves.loaded(t). Itsoffset,maskandotherare for the client to lower, because their meaning (a read of a snapshot under a mask, say) is the client's.children()does not descend into aLoadedor intoDataDep.keep(keepis not the value). A test pins both.LoopVarresolves its loop by name throughgraph.loop_info, so with several loops each variable reads its own bounds. An unknown name raisesValueError(a malformed graph). AnIterArgOffsetstill asks for the iteration of itsIterArgInfo's loop, or ofgraph.loopwhen it names none, as before. It needs only that loop's iteration, not itsLoopInfo, and [REFACTOR] Share the Term to Z3 lowering between compiled clients #482's lowering test pins anIterArgInfothat names a loop the graph does not list, so it is left unchecked.loaded, which refuses exactly likeDataDep("loaded value")(UNMODELED_VALUE, "an unmodeled value (loaded value)"). The sanitizer parses with the default, so it never meets aLoaded.Not in this PR
LoopInfofor a loop under a branch: the access's path already guards its loops' obligations, so this costs precision only.Tests
The unit tests read TTIR compiled at test time. Five kernels are added to
tests/unit/ir/reader_kernels.py, and the rest are short_moduletexts; no.ttirfiles are checked in.test_ttir_reader.py:test_multipath_reads_a_default_text_alike_but_for_loaded_values, one row per corpus text (66). A default parse's loop facts are its one loop. For each of the 36 texts the default parses whose multipath graph has no loaded value in a term and no kept mask, the two graphs pickle byte for byte the same. Pickle is used becausereprand==recurse too deep forkernel_deep_chain.test_several_loops_each_with_their_own_variable(mp_loops): the default refuses a nested loop. Multipath gives threeLoopInfos with their names and bounds, andgraph.loopis None. A graph built with one ofloop/loopsgets the other. A nested access has("%loop", "%loop1")and one under anscf.ifhas("%loop2",)and the branch in its path. The inner pointer advances by the outer variable, which is accepted. Each access gets one increment obligation per enclosing loop, outermost first. An advance by the loop's own variable (p1_variant_delta) or by a value loaded in the loop (mp_loaded_advance) still refuses as loop-variant, and a loop inside anscf.while(mp_for_in_while, withwhile_regions=True) as nested.test_integer_loads_read_as_loaded_values(mp_loaded): the default refuses indirect addressing. With multipath, aLoadedappears in an address (with its mask andother), in a mask (no longer dropped) and in a branch condition, andloaded_indicesfinds it. A masked loaded tile expanded to 2-D is re-placed at dim 0, mask included, withotherConst(0). A load whose mask is dropped staysDataDep.(offs < n) & (w > 0)keepsoffs < nas the mask, stillmask_dropped, and keeps it re-placed when the mask is expanded;(offs < n) | (w > 0)keeps nothing. A float load used as an atomic operand givesatomic_valNone.loaded_indicesfollows a loop variable into a loaded lower bound.test_cf_blocks_read_with_block_predicates_and_merged_arguments:golden_nested_guard_merge: two nested early returns and a merge block. The path is the merge block's predicate, andbaseis aSelectover the two incoming edges. The shared return-only block, which is printed before one of its predecessors, holds no access, so the text parses.crafted_same_dest: both arms of onecf.cond_brgo to one block.mp_early_return): the rest is guarded.tt.ptr_to_intparses.test_cf_blocks_out_of_order_or_cyclic_refuse: an access in a block printed before its predecessor, a self-loop, and an address made from such a block's argument, allcontrol-flow.test_cf_block_arguments_merge_only_modeled_values: a merge under an unmodeled edge, of an unmodelable value, and of two bases each refuse on "block argument"; a branch in generic form refuses as control flow.test_a_value_made_from_a_load_refuses_alike_in_both_modes: five texts (xoriin an address, casts in a loop bound, a float load cast to an index, i1 arithmetic, a loop-carried integer that starts as a load), each refusing with the same kind with the option on and off.test_while_regions_are_read_only_on_request: withmultipath=Truetoo, the two poll kernels pickle the same as withwhile_regions=Truealone, so a poll's load staysObserved.test_mlir_walk.py,test_cf_blocks_align: every branch'ssuccessorsingolden_nested_guard_merge, two successors naming one block, and a misalignment when the printed operands do not match the target's arguments.test_lowering.py: the loaded leaf (its terms are not lowered),children()of aLoadedand of aDataDepwithkeep, and with two loops eachLoopVarlowering through its own loop's bounds and iteration, plus an unknown loop name.test_oob.py: aLoadedin an address refuses likeDataDep("loaded value").Mutation check (not in the suite): of 65 single-line mutations of the new reader, walk, lowering and sanitizer code, 54 fail at least one test. The 11 that survive change no fact on any test kernel: the order of a block predicate inside a conjunction, a nested
keepignored (a wider mask), four variance rules for carried pointers and expanded tiles across several loops (the 898 corpus graphs, compared with each carried pointer's loop, cover these), an unknown loop name inaccess.loops, two of the four successor checks (only a disagreement between the text and the bindings can trip them), labels resolved across regions, and a cast that carrieskeep.Commands, Triton 3.8.0, CPU only (
CUDA_VISIBLE_DEVICES=""):-n 4-n 4The 87 new IR rows are the 66 rows of the multipath-equals-default test, the 5 pinned outcomes of the new kernels, and 16 rows of the other new tests.
One test that predates this PR is order-dependent:
tests/end_to_end/test_ir_lifecycle_compiled.py::test_a_relaunch_reuses_the_traces_host_compilefails when run alone, on the base too (assert 3 == 2), and passes only after the first test of its file. The new rows change how-n autospreads tests across workers, so CI can hit it at random. It will be fixed separately.Fact validation
Method. The corpus is the same 977 Triton 3.8.0 TTIRs as in #509 (16 corpora). Each text is read by this PR's reader, with the default and with
while_regions=True, multipath=True, and by #361's frozen text reader (62c6d7c, used as is), with the default and withmultipath=True. For #361,ttg.barrier allis rewritten togpu.barrier, as before.Graphs that both readers parse are compared access by access, in order. Terms are compared in canonical form:
IntCastwrappers and arange ssa names and dims are stripped (dims are compared separately for the aranges insideLoadedleaves);<result>@<line>);IterArgOffsetis resolved to its loop's index, itsoffset0and itsdelta;Loaded.base_paramis dropped.Parse status.
while_regions=True)Both readers parse 898 texts with multipath and both refuse 66. The 13 that differ are the 11 texts that differ by default (#509: this reader parses 10 that #361 refuses, 9 of them on #361's "non-pointer value" refusals and 1 on an indirect address, and refuses 1 impure inline asm that #361 parses), plus 2 that both readers refused by default:
fla_ttt_chunk_varlen…chunk_ttt_linear_fwd_kernel_h: a loop bound that is now aLoadedon both sides. [FEAT] [RACE DETECTOR] Z3-based race detector #361 then stops at its "load of a non-pointer value" refusal, the cause shared by the 8 otherfla_ttttexts;tritonbench jagged_sum…variable_length_loop_buffer_then_sum: the same, on [FEAT] [RACE DETECTOR] Z3-based race detector #361's side it now parses. This reader refuses its impure inline asm in both modes.Of the 66 texts both readers refuse, 20 have the same kind string. 35 more are the same refusal under another name: 33 are a loop-carried integer in an address, which this reader calls
loop-variant-advanceand #361 callsindirect-address, and 2 are a loop-carried integer in an address or a carried pointer's advance, which #361 callsother. 7 more are refusals #361 callsindirect-addressordata-dependent-boundand this readerother: an unmodeled op (arith.xori,tt.reshape,arith.sitofp,ub.poison) over values made from program ids and constants. #361 calls any integer arithmetic over anyDataDep(a float constant included) "arith over loaded data"; this reader keeps a modeling gap a gap, so these are #361's misclassifications. The last 4 stop at different ops: in 3, #361 refuses anscf.whilethat carries values (spin-shape), which this reader reads withwhile_regions=Truebefore it refuses a loop-carried address; in 1 (tb_triton_argmax), #361 stops at an SSA value it cannot resolve and this reader at a reduction over loaded data (indirect-address).Refusal kinds, default against multipath on this reader. 68 texts refuse with both
while_regions=Truealone and withmultipath=Trueadded. 50 refuse with the same kind; the other 18 refuse at a later op under multipath, so their kinds are not comparable: by default 12 stop at the second loop or thecf.cond_br, and 6 at an address made from a load, which multipath reads as aLoaded. Counting a value computed from aLoadedas memory-derived (section 5) moved 5 multipath refusals to the default's kinds:tilebench_radix_sortandtb_cache_transform_triton prefill_cache_kernel(other→indirect-address), the twofla_path_attn_parallel_varlenbackward kernels (other→data-dependent-bound), andtorchao_moe_fill_indices(a loop-carried integer that starts as a load:loop-variant-advance→indirect-address). The 7 multipath refusals of kindotheramong the 18 are modeling gaps: a pointer bitcast that changes the element width (4), a wideandi(2; the reader does not look at its operands, so it isotherby default too) and att.reshapeof values made from program ids (1). No graph fact changed. One gap is left as in #361: the arguments of aguardedblock areDataDep("block argument"), which does not count as memory-derived although the edge condition is loaded data; no corpus text has a guarded block.Agreement on the 898 graphs (6386 accesses; the access kinds and bases line up in all 898):
graph.loopset exactly for one looppath(1369 present; 1010 accesses in a non-entry block of 81 graphs with 184 blocks)offset(carried pointers with their loop),mask(4714 present),atomic_val(113),atomic_cmp(22)Selects, on both sides; accesses that read one (22)Loadedaccess indices per access (1805 accesses read one)Loadedplacement: arange dims inside the leavesguarded(none set) andmask_dropped(3 set; each keeps a conjunct, and the kept masks are equal)There is no disagreement on any compared fact.
path. The paths of the 1010 accesses in a non-entry block are equal in canonical form, including the conjunction order (block predicate first).axison1 - axis. That agrees with this reader's placement for 2-D tiles and would differ on 3-D ones; none of the 1805 differ.guardedis never set here;mp_early_returntests it), a shared tail printed before a predecessor that holds an access, and an integer load through a same-width bitcast of a float pointer.The sanitizer is unchanged. The default
parse_ttirwas run on all 977 texts at this PR and on a pristine f2451ea:loopsfields);loopsfields are exactly(graph.loop,)and("%loop",)-iff-in_loopon all 527;check_graphunder [FEAT] Add the race detector's TTIR reader facts #509's synthetic binding (4096-element tensors, scalars 64, grid (4, 2, 1)) returns identical findings, refusals and abstentions on all 527 graphs, witnesses included (in one run of the base, run alongside the test suite, one finding oftb_matmul_dequantizegot another witness from the same Z3 queries; the base itself gives either, so that model choice is not reproducible run to run);Solver.sexpr()at everycheck) are byte for byte the same.Line counts
From
git diff --numstatagainst f2451ea:tilelens/ir/ttir_reader.pytilelens/ir/_mlir_walk.pytilelens/ir/lowering.pytilelens/clients/sanitizer/compiled/oob.pytests/unit/ir/test_ttir_reader.pytests/unit/ir/reader_kernels.pytests/unit/ir/test_lowering.pytests/unit/ir/test_mlir_walk.pytests/unit/sanitizer_compiled/test_oob.pyThe tests add fewer lines than the product.