Skip to content

[FEAT] Add the race detector's multipath and loaded-value reader facts - #515

Open
mark14wu wants to merge 1 commit into
rd-4-reader-factsfrom
rd-5a-reader-l2-facts
Open

mark14wu wants to merge 1 commit into
rd-4-reader-factsfrom
rd-5a-reader-l2-facts

Conversation

@mark14wu

Copy link
Copy Markdown
Collaborator

Summary

This PR adds the second batch of TTIR reader facts that the compiled race detector (a later PR) needs: several loops, the cf blocks 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 new loops fields 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=True

The four facts sit behind a single switch, the same way #361's reader had them behind its multipath=True:

  • they have one consumer, the compiled race detector, and it turns them all on together;
  • they are validated together against [FEAT] [RACE DETECTOR] Z3-based race detector #361's reader with multipath=True on the same texts;
  • separate switches would add combinations (loaded values without several loops, say) that nothing runs or tests.

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.for loops (ttir_reader)

  • Each loop gets its own LoopInfo and 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_ssa and IterArgInfo.loop_ssa carry that name.
  • AccessGraph.loops lists every loop in opening order, outer before inner. AccessGraph.loop is set only when there is exactly one loop, so a single-loop graph reads as today (the sanitizer reads loop). A graph built with only one of the two gets the other (loops == (loop,), or loop from a one-entry loops), so hand-built graphs have one source of truth. AccessGraph.loop_info(name) finds a loop by name.
  • AccessEvent.loops and Fence.loops give the enclosing loops, outermost first, with in_loop == bool(loops).
  • A default parse fills these fields too, with what it can have: loops is (loop,) or (), and an access's or fence's loops is ("%loop",) exactly when in_loop. Every field that existed before is unchanged.
  • Under multipath=True a loop may be nested, follow another loop, or sit under an scf.if or a cf block. A loop under a branch carries that branch in its accesses' path. A loop inside an scf.while still refuses as a nested loop.
  • A loop-carried pointer's advance is loop-variant only for its own loop. That means its own induction variable, one of its own carried pointers, or an Observed/Loaded value 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_obligations lists 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. LoopInfo carries 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 cf blocks of an early return

Triton prints an if that holds a return as basic blocks of the function body: cf.cond_br, cf.br and tt.return. By default these are still refused as control flow, with the same message. Under multipath=True the reader walks the blocks in printed order:

  • A block's predicate is the disjunction of its incoming edges. Each edge carries the predecessor's predicate conjoined with the branch condition, negated on the false edge, or the predecessor's predicate alone for a cf.br. A block with no incoming edge gets 0 != 0, and its arguments are DataDep("argument of an unreachable block"). The predicate is the outermost conjunct of the path of every access and fence in the block, ahead of any scf.if conditions.
  • A block argument is a Select over 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 argument DataDep.
  • An edge whose condition cannot be modeled (loaded data) keeps only the predecessor's predicate. Both targets stay reachable, the target block is guarded, and its arguments are DataDep (a Select over inexact paths would pick a wrong value, not just a wider footprint).
  • A block entered before all its predecessors' branches were read has no known predicate. MLIR's block merging can print a shared tail (return-only or not) before one of its predecessors. An access, fence or branch inside such a block refuses (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.
  • Any other terminator (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.
  • The opaque-op address check (tt.ptr_to_int reaching 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 piece

Triton 3.8's MLIR bindings give an op's name, operands, results, regions, block and attributes, but no successor getter. ir.operation has none, and ir.block has 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_br in 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:

  • the branch has one successor (cf.br) or two (cf.cond_br);
  • every label names a block of that region;
  • each successor gets as many operands as its block has arguments;
  • the bindings' operand count equals the condition (for 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 no successors, 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. Loaded terms

Loaded(access_index, offset, mask, other) is the value an integer tt.load read. It is bound under multipath=True instead of DataDep("loaded value"):

  • only for an integer-typed result whose mask is modeled. A float or pointer load, and a load whose mask was dropped, keep DataDep. This is [FEAT] [RACE DETECTOR] Z3-based race detector #361's Int-only rule. It is what keeps float values out of atomic_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 through cmpf, a cast to an integer or a bitcast, which are DataDep already;
  • the rule is keyed on the load's result type, like [FEAT] Add the race detector's TTIR reader facts #509's access-level 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 (an f32 base read as i32) a Loaded is the base's bits reinterpreted, not its value converted. The docstring says so. No corpus text has this shape;
  • offset, mask and other (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 at offset where mask holds and other where it does not. An expand_dims of 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 an Observed, two lanes of one load may meet in a term;
  • [FEAT] [RACE DETECTOR] Z3-based race detector #361's Loaded also had base_param. That is the access's own base, so it is left out here;
  • a load in an scf.while condition region (with while_regions=True) is still Observed, as in [FEAT] Add the race detector's TTIR reader facts #509;
  • what the reader then accepts: an address, a mask, a branch condition, a loop bound or an atomic operand can now hold a Loaded, as in [FEAT] [RACE DETECTOR] Z3-based race detector #361. A Loaded read 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;
  • a value that an op the reader does not model computes from a Loaded (a reduction, xori, sitofp, an i1 add, a loop-carried integer that starts as a load) is memory-derived, just as its DataDep is by default. So a refusal keeps the default's kind (indirect-address, data-dependent-bound) rather than other. [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, like observed_indices, and through the lower, upper and step of 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 scan access.loops for those. The reader's own walkers (_nodes, _expand_dims) descend into a Loaded's three terms, so mentions_observed, the expand-dims lane check and width_obligations see inside it. width_obligations files 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.keep

DataDep(why, keep=None): under multipath=True, an arith.andi on i1 with an unmodelable operand records its modelable conjuncts, including those an operand kept itself, as keep. A mask that is such a value becomes keep instead of free. The access stays mask_dropped, because keep is still wider than the real mask: it only stops lanes that the modelable conjunct rules out. An ori keeps nothing. This is #361's partial-mask over-approximation. keep is not a value dependency. Splat, broadcast and expand_dims carry it, expand_dims re-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 default keep is always None.

7. Lowering (lowering.py) and the compiled sanitizer

  • TermLeaves.loaded(t): a Loaded is a leaf, Loaded -> leaves.loaded(t). Its offset, mask and other are 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 a Loaded or into DataDep.keep (keep is not the value). A test pins both.
  • A LoopVar resolves its loop by name through graph.loop_info, so with several loops each variable reads its own bounds. An unknown name raises ValueError (a malformed graph). An IterArgOffset still asks for the iteration of its IterArgInfo's loop, or of graph.loop when it names none, as before. It needs only that loop's iteration, not its LoopInfo, and [REFACTOR] Share the Term to Z3 lowering between compiled clients #482's lowering test pins an IterArgInfo that names a loop the graph does not list, so it is left unchecked.
  • The compiled sanitizer's leaves gain loaded, which refuses exactly like DataDep("loaded value") (UNMODELED_VALUE, "an unmodeled value (loaded value)"). The sanitizer parses with the default, so it never meets a Loaded.

Not in this PR

  • Any race-detector code or policy: launch snapshots or any other meaning for a loaded value, per-loop iteration bindings, pid-linear bounds, spin-shape rules, fence rules.
  • Any change to the compiled sanitizer's verdicts. It keeps the default parse.
  • Running the launch instead of skipping it, and new IR stages.
  • A path on LoopInfo for 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 _module texts; no .ttir files 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 because repr and == recurse too deep for kernel_deep_chain.
    • test_several_loops_each_with_their_own_variable (mp_loops): the default refuses a nested loop. Multipath gives three LoopInfos with their names and bounds, and graph.loop is None. A graph built with one of loop / loops gets the other. A nested access has ("%loop", "%loop1") and one under an scf.if has ("%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 an scf.while (mp_for_in_while, with while_regions=True) as nested.
    • test_integer_loads_read_as_loaded_values (mp_loaded): the default refuses indirect addressing. With multipath, a Loaded appears in an address (with its mask and other), in a mask (no longer dropped) and in a branch condition, and loaded_indices finds it. A masked loaded tile expanded to 2-D is re-placed at dim 0, mask included, with other Const(0). A load whose mask is dropped stays DataDep. (offs < n) & (w > 0) keeps offs < n as the mask, still mask_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 gives atomic_val None. loaded_indices follows 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, and base is a Select over 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 one cf.cond_br go to one block.
      • an early return under a float condition (mp_early_return): the rest is guarded.
      • an address passed to inline asm through a block argument still refuses, and one whose block argument is clean while the branch condition comes from tt.ptr_to_int parses.
    • 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, all control-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 (xori in 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: with multipath=True too, the two poll kernels pickle the same as with while_regions=True alone, so a poll's load stays Observed.
    • The new kernels' default refusals are added to the pinned outcomes.
  • test_mlir_walk.py, test_cf_blocks_align: every branch's successors in golden_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 a Loaded and of a DataDep with keep, and with two loops each LoopVar lowering through its own loop's bounds and iteration, plus an unknown loop name.
  • test_oob.py: a Loaded in an address refuses like DataDep("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 keep ignored (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 in access.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 carries keep.

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

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 f2451ea this PR
IR and compiled-sanitizer tests, -n 4 626 passed, 4 skipped 713 passed, 4 skipped
the same, serial 626 passed, 4 skipped 713 passed, 4 skipped
regression gate (the 20 files of #501–#505), -n 4 595 passed, 2 skipped 595 passed, 2 skipped

The 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_compile fails 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 auto spreads 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 with multipath=True. For #361, ttg.barrier all is rewritten to gpu.barrier, as before.

Graphs that both readers parse are compared access by access, in order. Terms are compared in canonical form:

Parse status.

this PR #361
texts the default parses (this side with while_regions=True) 566 557
texts that newly parse with multipath 343 343
texts parsed with multipath 909 900

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:

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-advance and #361 calls indirect-address, and 2 are a loop-carried integer in an address or a carried pointer's advance, which #361 calls other. 7 more are refusals #361 calls indirect-address or data-dependent-bound and this reader other: 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 any DataDep (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 an scf.while that carries values (spin-shape), which this reader reads with while_regions=True before 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=True alone and with multipath=True added. 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 the cf.cond_br, and 6 at an address made from a load, which multipath reads as a Loaded. Counting a value computed from a Loaded as memory-derived (section 5) moved 5 multipath refusals to the default's kinds: tilebench_radix_sort and tb_cache_transform_triton prefill_cache_kernel (other → indirect-address), the two fla_path_attn_parallel_varlen backward kernels (other → data-dependent-bound), and torchao_moe_fill_indices (a loop-carried integer that starts as a load: loop-variant-advance → indirect-address). The 7 multipath refusals of kind other among the 18 are modeling gaps: a pointer bitcast that changes the element width (4), a wide andi (2; the reader does not look at its operands, so it is other by default too) and a tt.reshape of values made from program ids (1). No graph fact changed. One gap is left as in #361: the arguments of a guarded block are DataDep("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):

fact agree disagree
loop count and canonical bounds (628 loops; 116 graphs with more than one) 898 graphs 0
graph.loop set exactly for one loop 898 graphs 0
per-access loops (1984 accesses in a loop, 64 in two nested ones) 6386 0
fences: count and positions 898 graphs 0
path (1369 present; 1010 accesses in a non-entry block of 81 graphs with 184 blocks) 6386 0
offset (carried pointers with their loop), mask (4714 present), atomic_val (113), atomic_cmp (22) 6386 each 0
block-argument merges: 6 merged arguments, all Selects, on both sides; accesses that read one (22) 6386 0
Loaded access indices per access (1805 accesses read one) 6386 0
Loaded placement: arange dims inside the leaves 1805 0
guarded (none set) and mask_dropped (3 set; each keeps a conjunct, and the kept masks are equal) 6386 0

There is no disagreement on any compared fact.

  • Block predicates are part of path. The paths of the 1010 accesses in a non-entry block are equal in canonical form, including the conjunction order (block predicate first).
  • Loaded placement. [FEAT] [RACE DETECTOR] Z3-based race detector #361 placed a 1-D range expanded at axis on 1 - axis. That agrees with this reader's placement for 2-D tiles and would differ on 3-D ones; none of the 1805 differ.
  • No corpus instance: an edge under an unmodeled condition (guarded is never set here; mp_early_return tests 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_ttir was run on all 977 texts at this PR and on a pristine f2451ea:

  • the 527 parsed graphs are identical in every field the base has (the full repr is compared, minus the three new loops fields);
  • the new loops fields are exactly (graph.loop,) and ("%loop",)-iff-in_loop on all 527;
  • the 450 refusals are identical in kind, message, line and loc;
  • check_graph under [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 of tb_matmul_dequantize got another witness from the same Z3 queries; the base itself gives either, so that model choice is not reproducible run to run);
  • the 6668 Z3 queries it sends (Solver.sexpr() at every check) are byte for byte the same.

Line counts

From git diff --numstat against f2451ea:

file added deleted
tilelens/ir/ttir_reader.py 445 77
tilelens/ir/_mlir_walk.py 65 3
tilelens/ir/lowering.py 29 17
tilelens/clients/sanitizer/compiled/oob.py 5 0
product total 544 97
tests/unit/ir/test_ttir_reader.py 283 0
tests/unit/ir/reader_kernels.py 72 0
tests/unit/ir/test_lowering.py 26 1
tests/unit/ir/test_mlir_walk.py 15 0
tests/unit/sanitizer_compiled/test_oob.py 6 4
test total 402 5

The tests add fewer lines than the product.

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").
@mark14wu
mark14wu added this pull request to stack #508 October 30, 2026 20:04
@github-actions

Copy link
Copy Markdown

Performance Benchmark

Benchmark main (min) PR (min) Change Samples
gemm 0.103s 0.101s -1.1% 20 / 20
gemm_oob 0.114s 0.114s +0.3% 20 / 20
indirect_load 0.021s 0.021s +1.7% 20 / 20
nested_loop 0.225s 0.226s +0.2% 20 / 20
block_pointer_loop_advance 0.120s 0.124s +2.9% 20 / 20
liger_jsd 0.134s 0.135s +0.9% 20 / 20
flaggems_layernorm 0.385s 0.385s -0.1% 20 / 20
swiglu 0.165s 0.166s +0.6% 20 / 20
cross_entropy 0.947s 0.949s +0.2% 20 / 20
fused_linear_jsd 0.202s 0.206s +2.0% 20 / 20
Total 2.416s 2.427s +0.5% N/A

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