Repository navigation
Conversation
The race detector's pure-Z3 solver library from PR #361, for the eager and compiled race detectors to call; no client uses it yet. - two_copy_symbolic_hb_solver.py: TwoCopySymbolicHBSolver lowers the records under two alpha-renamed program instances and asks Z3 for unordered, conflicting, aliasing event pairs across instances and between lanes or operations of one instance. It models CAS and value-modeled RMW returns (closed-world read-from, per-location atomic coherence, value causality, reads-through release sequences, the counting axiom), await records and pre-exit representatives, the feasibility check, the enumeration fallback, and the compiled restrictions (only_pairs, launch_ceiling, extra_assumptions). - hb_common.py (shared primitives, transitive HB closure), conflict_simplification.py and snapshot_lemmas.py (the UNSAT-only conflict precheck), guarded_division.py (certified divisor folding), report_diagnostics.py (the missing-fence explanation). - data.py gains RaceType, RaceReport and the solver's record fields; the hb_solver.py placeholder is deleted. Changes from #361: - Fence order is always on, and which fence orders which pair is the caller's rule: the solver takes fence_between(first, second), asked for two records of one instance in program order (None: no fence orders any pair). It replaces fence_seqs, so the eager and the compiled detector can each keep their own fence rule. - Every Solver gets its own timeout (timeout_ms, default TIMEOUT_MS = 180 s, must be positive); no global Z3 parameter is set. The enumeration's 5 s per-case and 10 s retry limits are capped by timeout_ms. An unknown answer stays unsupported. - RaceReport.race_type drops the access-mode fallback only HBSolver reports used. - Removed: the fence_order=False reading, cuTile token order, the ablations switch, CAS-only minimal_atomic_read_from and the unused iter_lane.
mark14wu
added this pull request to stack #496
October 13, 2026 17:33
Performance Benchmark
Iterations: 1 warmup + 20 measured |
This was referenced Oct 13, 2026
mark14wu
removed this pull request from stack #496
October 14, 2026 20:10
mark14wu
added this pull request to stack #508
October 14, 2026 20:10
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
The second layer of the race detector port: the race detector's pure-Z3 solver library, taken from the frozen PR #361 (62c6d7c). Nothing calls it yet. The eager race detector client that uses it comes in the next PR, and the compiled race detector later.
No user-visible change: no client, config field or environment variable is added, and the eager sanitizer, tracer, profiler and compiled sanitizer are untouched.
Stacked on #501 (top of #494 → #479 → #495 → #480 → #482 → #501). Triton 3.8 only.
Change
Modules under
tilelens/clients/race_detector/, ported from #361 withtriton_vizrenamed totilelens:two_copy_symbolic_hb_solver.py:TwoCopySymbolicHBSolver. It lowers the access records under two program instances, alpha-renaming pids, arange lanes and per-record copy-local vars (CAS and RMW returns, loop iterators). It then asks Z3 for pairs of events that conflict, alias and are not ordered by happens-before: across two instances, and between lanes or operations of one instance.find_races()returnsRaceReports with a RAW/WAR/WAW type, a witness address and the two witness pids.check_feasibility()checks that the launch's premises admit some execution, so a race-free result over no executions is not reported as a proof. The model covers:add/max/min/xchg, andor/xorwith 0) as values: closed-world read-from with anrf_unknownescape when an unmodeled writer can overlap, per-location atomic coherence, value causality (no out-of-thin-air values), release sequences through RMW chains, and the counting axiom (last block done, work-queue slots, single winner) with its guards;await_dependencies,await_observations,causal_constraints) and pre-exit representatives;enum_fallback_grid;only_pairs,launch_ceiling,extra_assumptions.Z3
unknownis never read as UNSAT. An undecided race or feasibility query raisesUnsupportedSymbolicRaceQuery, and an undecided overlap check opens therf_unknownescape. The constructor raises the same exception (or its subclassCountingScopeUnsupported, which the compiled race detector retries at a pinned grid) when a record cannot be lowered. The caller must report such a launch as unsupported, never as race-free. All terms live in Z3's main context, which is not thread-safe, so the caller serializes solves (the module docstring says so).hb_common.py: shared primitives (substitution, lane helpers, the conflict predicate for atomic pairs, the read-from predicate) and the sparse transitive HB closure.conflict_simplification.py,snapshot_lemmas.py: the conflict precheck. It relaxes nonlinear and array terms in a necessary conflict condition and may only discard a pair when that weaker condition is UNSAT. Snapshot lemmas add the range and injectivity facts that complete integer tables already asserted in the query entail.guarded_division.py: folds a variable divisor to a constant only when an original top-level mask in the query proves its value.report_diagnostics.py: explains an intra-instance witness between two operations that no captured fence orders.data.py: [FEAT] [RACE DETECTOR] Z3-based race detector #361's version replaces the skeleton's. It addsRaceType,RaceReportand the solver's record fields toAccessEventRecord. The skeleton's fields are unchanged, sorace_detector.pyand its tests are unchanged.RaceReport.race_typedrops [FEAT] [RACE DETECTOR] Z3-based race detector #361's access-mode fallback, which only HBSolver's reports used; the two-copy solver always sets the type.The 7-line
hb_solver.pyplaceholder is deleted. Nothing imported it.What changed from #361:
fence_between(first, second) -> bool, asked for two records of one instance withfirst.program_seq < second.program_seq, and caches each answer per record pair.Nonemeans no fence orders any pair. This replaces thefence_seqsfloat tuple and_fence_between, so the eager race detector can keep [FEAT] [RACE DETECTOR] Z3-based race detector #361's sequence-position rule while the compiled one uses its own. The solver owns no fence policy. The missing-fence diagnostic asks the same hook (its boolean argument is nowfenced, and its text says "no captured fence that orders them", which is right for either rule).Solverthe two-copy solver creates (race queries, feasibility, guard and overlap checks, enumeration cases) gets its own timeout. It comes from the constructor'stimeout_ms, defaulting toTwoCopySymbolicHBSolver.TIMEOUT_MS = 180_000(180 s, the eager budget);timeout_ms <= 0raisesValueError, because Z3 reads 0 as "no limit". No global Z3 parameter is set, so a solve cannot change Z3's defaults for another client such as the compiled sanitizer. In [FEAT] [RACE DETECTOR] Z3-based race detector #361 the compiled client set the globaltimeoutaround each solve, and the eager client set none. The enumeration fallback's 5 s per case and 10 s capped retry are nowmin(timeout_ms, limit), so they only ever shorten the caller's budget. For anytimeout_msof 10 s or more (180 s eager, 10 s / 120 s compiled) this is [FEAT] [RACE DETECTOR] Z3-based race detector #361's behaviour. The precheck's 500 ms and the division certificate's 200 ms stay fixed, per-solver budgets: an undecided precheck keeps the pair for the full query, and an undecided certificate keeps the original formula. Anunknownanswer still raisesUnsupportedSymbolicRaceQuery; Z3's reason (for exampletimeout) is only in the message for now, and the eager client in the next PR decides whether it needs it as an attribute.Removed while porting. #361's default path is fence-ordered and non-ablated, and these removals leave that path's behaviour unchanged:
fence_order=Falsereading (every earlier operation orders every later one), and with it thefence_orderargument of the diagnostic. A caller can still get that reading with a hook that returns True;token_orderand its guards). cuTile support comes much later;ablationsswitch ("hb","coherence"), which only the evaluation harness used;hb_common.minimal_atomic_read_from, which only the HBSolver demo used, and thegetattrfallbacks for demo events without a width, scope or pid. The unusediter_laneis also dropped;Kept: the compiled-only paths (
enum_fallback_grid,only_pairs,launch_ceiling,extra_assumptions, await records), which the compiled race detector needs. Splitting the solver across PRs would cost more review churn. Also kept: the private_ENABLE_*switches inconflict_simplification.pyandguarded_division.py, which tests use to compare a precheck path with the full query.Not ported:
bounds.py. Only #361's concrete-enumeration channel and compiled replay use it, and neither is in this PR, so it would be dead code here. It can come with the first PR that needs it.Tests
New tests in
tests/unit/race_detector/are pure Z3 on hand-built records, with no interpreter. They are a rewritten subset of #361's solver unit tests, one case per distinct behaviour. Most proofs have a twin, one changed premise away, that must flip to a report.test_two_copy_solver.py:RaceType;rf_unknown;only_pairsandextra_assumptions;Solverinstance created during construction,find_races()andcheck_feasibility()carriestimeout_ms; withlaunch_ceilingthe 10 s retry cap applies only whentimeout_msis larger, and enumeration cases never exceedtimeout_ms;timeout_ms=0is rejected;z3.get_param("timeout")is unchanged after a solve; a 50 ms timeout makes anx³ + y³ = z³query raiseUnsupportedSymbolicRaceQueryinstead of hanging (run in a thread with a 60 s join, so a regression fails instead of hanging CI);rf_unknown.test_rmw_solver.py:max/minwrite parts reaching a relaxed reader (a wrong write part would hide the data race);CountingScopeUnsupported, and one that wraps on a concrete grid raises the plain exception;rf_chainsource;addcounts as an unmodeled writer);rf_unknownescape for its own poll.test_hb_closure.py: the sparse closure against the layered recurrence (conditional edges, cycles, arrays, a custom context); program-order edges for every fence and dependency case; reads-through scope checks.test_conflict_precheck.py: mixed-radix relaxation and the premises it needs; the private switches; an unknown precheck falls back to the full query; snapshot lemmas (range, injectivity, holes, duplicates, conditional cells), and the solver'sSnapshotLemmaCacherefusing a table with a hole; launch scope versus any grid; the UNSAT query cache; the base-constraint cache following mutated families.test_guarded_division.py: mask-certified folding; absent or conditional masks keep zero and negative divisors; every certificate failure keeps the original formula; the applicability cache.solver_records.py: record builders and two fence hooks,fences_at(*positions)([FEAT] [RACE DETECTOR] Z3-based race detector #361's sequence rule) andevery_fence.Not ported: tests that need the eager client, the TTIR/cuTile readers, the compiled client, replay or concrete enumeration (for example
test_cutile_loop_conflicts.py,test_route2_snapshot_select.py,test_t0_symbolic_loop_bounds.py,test_await_abstraction.py, the end-to-endtest_fence_order.py).Equivalence check (not committed): before trimming, I ran #361's 22 pure-solver test files unchanged against the ported solver through a shim that maps
fence_order=Falseto a hook returning True andfence_order=True, fence_seqs=…to the sequence rule. 466 passed. The 19 token-order and ablation cases were skipped. The rename-only copy had passed all 485.Commands, Triton 3.8.0, CPU only (
CUDA_VISIBLE_DEVICES=""):The new solver tests: 129 passed, serial and
-n 4, in about 4 s. With the race detector skeleton tests: 139 passed.Mutation checks (not committed): each of these single-line mutations makes at least one new test fail: an undecided enumeration case read as UNSAT; the snapshot cache accepting a table with a hole;
max/minwrite parts replaced by the old value; a wrap-capableaddnot counted as an unmodeled writer; a bareSolver()in the guard-check or base solver; one cached fence answer reused for every pair; the enumeration's case and retry limits not capped bytimeout_ms.Regression gate: the same 20 files as #501. The base is a pristine worktree of #501's head (bc229ce).
-n 4Line counts, from
git diff --numstatagainst bc229ce:two_copy_symbolic_hb_solver.pyconflict_simplification.pysnapshot_lemmas.pyhb_common.pyguarded_division.pydata.pyreport_diagnostics.pyhb_solver.pyAdded test lines stay below added product lines (1771 ≤ 4209). The two-copy solver is 62% of the product lines. The precheck modules (
conflict_simplification.py,snapshot_lemmas.py,guarded_division.py, 1119 lines) only speed queries up: the precheck discards a pair only when it is UNSAT, and the folding keeps the query equivalent. They could move to a follow-up PR if that is easier to review; the solver would then ship first without the precheck and the folding.