Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions tests/unit/test_race_detector_symbolic_hb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""Tests for symbolic path HB solver integration."""

from unittest.mock import MagicMock

from triton_viz.clients.race_detector.data import (
AccessType,
SymbolicMemoryAccess,
)
from triton_viz.clients.race_detector.hb_solver import SymbolicHBSolver


def _make_sym_access(
access_type,
event_id,
ptr_sig="data_ptr",
atomic_op=None,
atomic_sem=None,
atomic_scope=None,
epoch=0,
):
"""Create a SymbolicMemoryAccess with a mock ptr_expr."""
ptr_expr = MagicMock()
ptr_expr._test_sig = ptr_sig # used by our fake signature fn
return SymbolicMemoryAccess(
access_type=access_type,
ptr_expr=ptr_expr,
mask_expr=None,
is_data_dependent=False,
event_id=event_id,
atomic_op=atomic_op,
atomic_sem=atomic_sem,
atomic_scope=atomic_scope,
epoch=epoch,
)


def _ptr_sig_fn(ptr_expr):
"""Fake ptr signature function that returns the test signature."""
return (ptr_expr._test_sig,)


def test_symbolic_release_acquire_conservative():
"""Release/acquire flag pattern in symbolic mode → conservatively reports race.

SymbolicHBSolver cannot prove reads-from or must-alias, so it
conservatively returns True and defers to the concrete fallback.
"""
store_data = _make_sym_access(AccessType.STORE, event_id=0, ptr_sig="data_ptr")
release_flag = _make_sym_access(
AccessType.ATOMIC,
event_id=1,
ptr_sig="flag_ptr",
atomic_op="rmw:xchg",
atomic_sem="release",
atomic_scope="gpu",
)
acquire_flag = _make_sym_access(
AccessType.ATOMIC,
event_id=2,
ptr_sig="flag_ptr",
atomic_op="rmw:xchg",
atomic_sem="acquire",
atomic_scope="gpu",
)
load_data = _make_sym_access(AccessType.LOAD, event_id=3, ptr_sig="data_ptr")

all_accesses = [store_data, release_flag, acquire_flag, load_data]

solver = SymbolicHBSolver(store_data, load_data, all_accesses, _ptr_sig_fn)
assert solver.check_race_possible() # conservative → race reported


def test_symbolic_relaxed_still_races():
"""Same pattern with sem='relaxed' → race."""
store_data = _make_sym_access(AccessType.STORE, event_id=0, ptr_sig="data_ptr")
relaxed_flag = _make_sym_access(
AccessType.ATOMIC,
event_id=1,
ptr_sig="flag_ptr",
atomic_op="rmw:xchg",
atomic_sem="relaxed",
atomic_scope="gpu",
)
relaxed_read = _make_sym_access(
AccessType.ATOMIC,
event_id=2,
ptr_sig="flag_ptr",
atomic_op="rmw:xchg",
atomic_sem="relaxed",
atomic_scope="gpu",
)
load_data = _make_sym_access(AccessType.LOAD, event_id=3, ptr_sig="data_ptr")

all_accesses = [store_data, relaxed_flag, relaxed_read, load_data]

solver = SymbolicHBSolver(store_data, load_data, all_accesses, _ptr_sig_fn)
assert solver.check_race_possible() # no sw → race


def test_symbolic_fallback_on_cas_control_flow():
"""CAS return used in control flow triggers concrete fallback.

This is tested implicitly: when _need_concrete_fallback is True,
the symbolic path is skipped and concrete execution happens instead.
The symbolic HB solver is never invoked in that case.

We just verify the solver handles the no-sync-events case correctly.
"""
store = _make_sym_access(AccessType.STORE, event_id=0, ptr_sig="data_ptr")
load = _make_sym_access(AccessType.LOAD, event_id=1, ptr_sig="data_ptr")

# No sync events at all
all_accesses = [store, load]
solver = SymbolicHBSolver(store, load, all_accesses, _ptr_sig_fn)
assert solver.check_race_possible() # no sync → race possible


def test_symbolic_hb_signature_same_no_suppress():
"""Matching ptr signatures + release/acquire pair → still returns True (no suppress).

Even though signatures match, symbolic HB cannot prove must-alias or
reads-from, so it conservatively reports a race.
"""
store_data = _make_sym_access(AccessType.STORE, event_id=0, ptr_sig="data_ptr")
release_flag = _make_sym_access(
AccessType.ATOMIC,
event_id=1,
ptr_sig="flag_ptr",
atomic_op="rmw:xchg",
atomic_sem="release",
atomic_scope="gpu",
)
acquire_flag = _make_sym_access(
AccessType.ATOMIC,
event_id=2,
ptr_sig="flag_ptr", # same signature as release
atomic_op="cas",
atomic_sem="acquire",
atomic_scope="gpu",
)
load_data = _make_sym_access(AccessType.LOAD, event_id=3, ptr_sig="data_ptr")

all_accesses = [store_data, release_flag, acquire_flag, load_data]

solver = SymbolicHBSolver(store_data, load_data, all_accesses, _ptr_sig_fn)
assert solver.check_race_possible() # no suppress — conservative


def test_symbolic_hb_acquire_unprovable_no_suppress():
"""Textbook producer-consumer symbolic pattern → returns True.

The symbolic engine lacks atomic_old to prove reads-from, so
even a structurally perfect release/acquire pattern cannot be
proven to establish ordering.
"""
# Producer pattern: store + release
store = _make_sym_access(AccessType.STORE, event_id=0, ptr_sig="shared_buf")
release = _make_sym_access(
AccessType.ATOMIC,
event_id=1,
ptr_sig="ready_flag",
atomic_op="rmw:xchg",
atomic_sem="release",
atomic_scope="sys",
)
# Consumer pattern: acquire + load
acquire = _make_sym_access(
AccessType.ATOMIC,
event_id=2,
ptr_sig="ready_flag",
atomic_op="rmw:xchg",
atomic_sem="acquire",
atomic_scope="sys",
)
load = _make_sym_access(AccessType.LOAD, event_id=3, ptr_sig="shared_buf")

all_accesses = [store, release, acquire, load]

solver = SymbolicHBSolver(store, load, all_accesses, _ptr_sig_fn)
assert solver.check_race_possible() # unprovable → race reported
38 changes: 37 additions & 1 deletion triton_viz/clients/race_detector/hb_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import numpy as np

from .data import MemoryAccess
from .data import MemoryAccess, SymbolicMemoryAccess, AccessType


def _release_like(sem: str | None) -> bool:
Expand Down Expand Up @@ -234,3 +234,39 @@ def _reachable(adj: dict[int, set[int]], src: int, dst: int) -> bool:
visited.add(neighbor)
queue.append(neighbor)
return False


class SymbolicHBSolver:
"""HB solver for symbolic accesses.

Models two copies of the program (block A and block B) since the symbolic
trace is a template executed by all blocks. po edges are within each copy,
sw edges are between copies.

Uses symbolic ptr signatures to determine address overlap for sw edges.
Like HBSolver, uses unconditional sw for qualifying release/acquire pairs.
"""

def __init__(
self,
acc_a: SymbolicMemoryAccess,
acc_b: SymbolicMemoryAccess,
all_accesses: list[SymbolicMemoryAccess],
ptr_signature_fn,
):
self._acc_a = acc_a
self._acc_b = acc_b
self._all_accesses = all_accesses
self._ptr_sig = ptr_signature_fn

def check_race_possible(self) -> bool:
"""Return True -- symbolic HB cannot currently prove ordering.

Sound suppression requires both:
1. Must-alias proof (not just signature equality)
2. Reads-from proof (atomic_old unavailable in symbolic accesses)

Until the symbolic engine provides these, conservatively report all
candidate races and let the concrete fallback handle suppression.
"""
return True
26 changes: 26 additions & 0 deletions triton_viz/clients/race_detector/race_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,18 @@ def _broadcast_mask(mask_z3, sub_pairs, n_lanes):
if race_type is None:
continue

# Symbolic HB check for mixed atomic/plain pairs
if race_type is not None and _has_symbolic_sync_metadata(
accesses, acc_a.epoch
):
from .hb_solver import SymbolicHBSolver

sym_hb = SymbolicHBSolver(
acc_a, acc_b, accesses, self._symbolic_ptr_signature
)
if not sym_hb.check_race_possible():
continue # UNSAT → proven ordered, suppress

# Get Z3 expressions for pointers
ptr_z3_a, ptr_constr_a = acc_a.ptr_expr.eval()
ptr_z3_b, ptr_constr_b = acc_b.ptr_expr.eval()
Expand Down Expand Up @@ -844,6 +856,20 @@ def _broadcast_mask(mask_z3, sub_pairs, n_lanes):
return races


def _has_symbolic_sync_metadata(
accesses: list[SymbolicMemoryAccess], epoch: int
) -> bool:
"""Check if any symbolic atomic access in the given epoch has sync metadata."""
for acc in accesses:
if acc.epoch != epoch:
continue
if acc.access_type != AccessType.ATOMIC:
continue
if acc.atomic_sem is not None:
return True
return False


def _classify_race_type(
ta: AccessType,
tb: AccessType,
Expand Down