This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 615abe2a56 [TIRx][CUDA] Add the declared synchronization-word wait 
(#20353)
615abe2a56 is described below

commit 615abe2a568893f109bc0f90e26626aa03a0934c
Author: Guanjie Wang <[email protected]>
AuthorDate: Tue Sep 15 20:47:24 2026 -0400

    [TIRx][CUDA] Add the declared synchronization-word wait (#20353)
    
    ## Problem
    
    This exists for `tirx-kernels`, where every cross-thread protocol is a
    hand-rolled spin on a global word -- `ld; while (!done) { ld; }`,
    sometimes with a `__nanosleep` between polls and a fence after the loop.
    `sm100_fp8_fp4_mega_moe` alone has eight. To a race checker that loop is
    N ordinary reads racing another CTA's write, and it has to report every
    one: nothing in the source separates a read that retries until a
    condition holds from a read that took whatever it got.
    
    `wait_until` is that shape, spelled once, and it can replace all of them
    -- the 18 such loops in the corpus are an acquiring poll, a relaxed
    poll, or a volatile poll with a backoff, and all three are this
    operation. Adopting it costs nothing: the table below measures the
    emitted form against the hand-written loop it replaces, on every kernel
    that owns one. What it buys is that the word becomes declared -- the
    address the wait names is the protocol's, and accesses to it are judged
    against the protocol instead of as an ordinary pair.
    
    ## The operation
    
    T.cuda.wait_until(dst, ptr, predicate, scope=, space=, ptx_type=,
    backoff_ns=)
    
    Spin on one global word until `predicate` holds; the exit value stays in
    `dst`. Publishers stay raw PTX: `st.release`, `red`, `atom`. The name is
    shared with `T.nvshmem.wait_until` deliberately -- that one names a
    symmetric object and an enumerated comparison, this one a global address
    and a predicate.
    
    ## Generated code
    
        ld_relaxed(dst, ptr);                           // peeled
        if (!predicate) {
          #pragma unroll 1
          do { ld_relaxed(dst, ptr); } while (!predicate);
        }
        { T edge; ld_acquire(edge, ptr); (void)edge; }   // the edge
    
    `ld.relaxed.<scope>.<space>.<ptx_type>` polls, one `ld.acquire.<scope>`
    closes. The closing read is discarded on purpose: it may observe a value
    later than the predicate accepted, and these predicates are not all
    monotone -- `mega_moe`'s grid barrier tests a sign-bit flip, its ring
    waits test equality -- so reaching `dst` would hand the caller a value
    its own predicate rejects. The edge survives: such words are published
    by release RMWs, so acquiring any contribution synchronizes with all
    earlier ones.
    
    ## Why this shape
    
    Three spellings of the same acquire. The baseline is the loop these
    kernels wrote by hand -- `ld.acquire` on every poll, no closing read --
    and both other columns are wall time relative to it, negative meaning
    faster. Interleaved, every benchmarkable kernel that owns a spin wait,
    first round dropped as warm-up.
    
    | kernel | sites | relaxed poll + acquire exit (emitted) | relaxed poll
    + `fence.acq_rel` exit |
    |---|---|---|---|
    | `sm100_fp8_fp4_mega_moe` | 8 | **-1.04%** | +49.9~51.8% |
    | `radix_topk_multi_cta` | 1 | **-0.50%** | +5.89~6.10% |
    | `agent_evolved_moe_fp8_blockscale_dsv3` | 3 | +0.03% | +1.53~2.15% |
    | `agent_evolved_kda_backward_packed` | 4 | -0.08% | -0.10~+0.03% |
    | `cudnn_sm100_flex_attention_backward` | 1 | -0.25% | -0.06~+0.96% |
    
    The fence exit is never faster than the baseline on any of the five.
    `fence.sc` costs what `fence.acq_rel` costs, and keeping the acquiring
    poll *and* adding a fence is just as bad, which puts the cost in the
    fence rather than in the loop body.
    
    `ld.volatile` polls within 0.1% of `ld.relaxed` everywhere, which is ISA
    8.4.2 measured; `.relaxed.<scope>` is emitted because it names the scope
    instead of resting on `volatile` meaning `.sys`.
    
    Peeling the first load gives ptxas one loop body to schedule, and the
    SASS then matches the hand-written loop this replaces. That axis was not
    swept separately.
    
    ## Other parameters
    
    - `scope`, `ptx_type` spell the access. `ptx_type` respells at the same
    width (a counter read as raw bits) and is required when the address is
    untyped. 128 bits is refused: that exit value is not a scalar a
    predicate can test.
    - `space` admits `global` only. A wait within a CTA or cluster belongs
    on an `mbarrier`; `shared` is refused with a message that says so.
    - `backoff_ns` puts `__nanosleep` between polls. Nothing helps: monotone
    degradation from 0, +1.35% at 20ns, +30.08% at 2us. Measured on short
    waits only, so it stays opt-in.
---
 python/tvm/backend/cuda/cpp/asm.py                | 236 +++++++++++++++
 python/tvm/backend/cuda/op.py                     | 142 +++++++++
 python/tvm/backend/cuda/script.py                 |   4 +
 src/backend/cuda/op/target_builtin.cc             |   1 +
 tests/python/tirx/codegen/test_cuda_wait_until.py | 353 ++++++++++++++++++++++
 5 files changed, 736 insertions(+)

diff --git a/python/tvm/backend/cuda/cpp/asm.py 
b/python/tvm/backend/cuda/cpp/asm.py
index 40171de139..b2c94b47f5 100644
--- a/python/tvm/backend/cuda/cpp/asm.py
+++ b/python/tvm/backend/cuda/cpp/asm.py
@@ -25,6 +25,9 @@ the PTX text, or the offset-scaling ``cp.async`` form the 
legacy
 ``InjectPTXAsyncCopy`` pass emits.
 """
 
+import re
+
+from tvm import ir
 from tvm.backend.cuda.op import cuda_func_call
 
 from ..codegen.registry import CODEGEN_REGISTRY, register_codegen
@@ -33,6 +36,239 @@ from ..codegen.types import PTXDataType
 from ..codegen.utils import parse_str
 
 
+# =============================================================================
+# Declared synchronization words. The four direct forms emit exactly what their
+# raw PTX spellings do; only the operation's identity differs, which is what
+# lets a checker tell a protocol's own accesses from a stray one. The wait is
+# the one that generates something new: a loop, which is why it lives here and
+# not in the instruction table.
+# =============================================================================
+_WAIT_UNTIL_SCALARS = {
+    "int32": "s32",
+    "uint32": "u32",
+    "int64": "s64",
+    "uint64": "u64",
+    "uint128": "b128",
+    "int128": "b128",
+}
+
+
+_WAIT_UNTIL_PTX_WIDTH = {
+    "b32": 32,
+    "s32": 32,
+    "u32": 32,
+    "b64": 64,
+    "s64": 64,
+    "u64": 64,
+    "b128": 128,
+}
+
+
+def _wait_until_scalar_suffix(ty, requested=""):
+    """The PTX type this access is spelled with.
+
+    Defaults to the word's own signedness. A caller may ask for another
+    spelling of the same width -- `b32` for a load or a store that only moves
+    the value, which is how kernels ordinarily write one -- but not for another
+    width, which would access a different number of bytes. Whether the
+    instruction has the requested form at all is settled by the instruction
+    table: `add` has no bit-typed form and rejects one there.
+    """
+    suffix = _WAIT_UNTIL_SCALARS.get(str(ty))
+    if suffix is None:
+        raise TypeError(f"sync word must be a 32/64/128-bit integer scalar, 
got {ty}")
+    if not requested:
+        return suffix
+    width = _WAIT_UNTIL_PTX_WIDTH.get(requested)
+    if width is None:
+        raise TypeError(f"invalid ptx_type {requested!r}")
+    if width != _WAIT_UNTIL_PTX_WIDTH[suffix]:
+        raise TypeError(
+            f"ptx_type {requested!r} is {width} bits but the sync word is "
+            f"{_WAIT_UNTIL_PTX_WIDTH[suffix]} bits"
+        )
+    return requested
+
+
+def _wait_until_pointee(ptr, what):
+    if not isinstance(ptr.ty, ir.PointerType):
+        raise TypeError(f"{what} ptr must be a pointer to the synchronization 
word")
+    return ptr.ty.element_type
+
+
+def _wait_until_word_suffix(ptr, requested, what):
+    """The PTX type the word is accessed as.
+
+    Normally the word's own type decides it and `ptx_type` may respell it at
+    the same width. A kernel that addresses its workspace by byte offset has
+    no pointee type to read -- the address is an untyped handle -- and there
+    `ptx_type` is not a respelling but the statement of how wide the word is,
+    so it is required.
+    """
+    pointee = str(_wait_until_pointee(ptr, what))
+    # A 128-bit word spans two 64-bit elements, so a pointer into the pair is
+    # how a kernel names it; asking for `.b128` there is not a respelling of
+    # the pointee but the statement that the word is the wider one.
+    if requested == "b128" and _WAIT_UNTIL_PTX_WIDTH.get(
+        _WAIT_UNTIL_SCALARS.get(pointee, "")
+    ) in (32, 64):
+        return requested
+    if pointee in _WAIT_UNTIL_SCALARS:
+        return _wait_until_scalar_suffix(pointee, requested)
+    if not requested:
+        raise TypeError(
+            f"{what} ptr is an untyped address; pass ptx_type to say how wide 
the word is"
+        )
+    if requested not in _WAIT_UNTIL_PTX_WIDTH:
+        raise TypeError(f"invalid ptx_type {requested!r}")
+    return requested
+
+
+def _wait_until_same_width(dst_ty, suffix, what):
+    """The destination register must be as wide as the word.
+
+    Not the same type: a bit-typed access moves 32 or 64 bits into whatever
+    register of that width the caller named, which is how kernels ordinarily
+    read a counter they treat as signed out of an unsigned word.
+    """
+    dst = _wait_until_scalar_suffix(dst_ty)
+    if _WAIT_UNTIL_PTX_WIDTH[dst] != _WAIT_UNTIL_PTX_WIDTH[suffix]:
+        raise TypeError(
+            f"{what} destination is {_WAIT_UNTIL_PTX_WIDTH[dst]} bits but the "
+            f"sync word is {_WAIT_UNTIL_PTX_WIDTH[suffix]} bits"
+        )
+
+
+def _wait_until_thread_local_scalar(dst, what):
+    if not isinstance(dst, ir.TensorLoad) or dst.source.scope() not in {
+        "local",
+        "local_scalar",
+        "register",
+        "reg",
+    }:
+        raise TypeError(f"{what} dst must be a writable thread-local scalar")
+    return dst.ty
+
+
+def _wait_until_forward(spelling, *args):
+    """Emit one instruction-table PTX operation and return its codegen 
result."""
+    from ..ptx import PTXNamespace  # pylint: disable=import-outside-toplevel
+
+    call = PTXNamespace()[spelling](*args)
+    return CODEGEN_REGISTRY[call.op.name](call.args)
+
+
+@register_codegen("cuda_wait_until")
+def cuda_wait_until(dst, ptr, condition, scope, space, ptx_type, backoff_ns):
+    """Lower a declared wait to a pre-tested loop around one scoped load."""
+    scope, space, ptx_type = (parse_str(x) for x in (scope, space, ptx_type))
+    dtype = _wait_until_thread_local_scalar(dst, "wait_until")
+    suffix = _wait_until_word_suffix(ptr, ptx_type, "wait_until")
+    _wait_until_same_width(dtype, suffix, "wait_until")
+
+    # The wait polls relaxed and closes with a single `ld.acquire`, rather than
+    # paying acquire semantics on every poll. Measured against the acquiring
+    # poll over every benchmarkable kernel that owns a spin wait, interleaved,
+    # round 1 dropped:
+    #
+    #     sm100_fp8_fp4_mega_moe                 8 sites   -1.04%
+    #     radix_topk_multi_cta                   1 site    -0.50%
+    #     agent_evolved_moe_fp8_blockscale_dsv3  3 sites   +0.03%
+    #     agent_evolved_kda_backward_packed      4 sites   -0.08%
+    #     cudnn_sm100_flex_attention_backward    1 site    -0.25%
+    #
+    # The closing read is what takes the edge, and it may observe a value later
+    # than the one that satisfied the predicate. That is still the edge the
+    # protocol means: these words are published by `red`/`atom` release RMWs,
+    # so every contribution sits in one release sequence and an acquire reading
+    # any of them synchronizes with all the earlier ones. It reads into a
+    # discarded temporary precisely so the later value cannot reach `dst` --
+    # the wait's exit value stays the value the predicate accepted, which
+    # matters because predicates here are not all monotone (`mega_moe`'s grid
+    # barrier tests a sign-bit flip, and the ring waits test equality).
+    #
+    # This is the wait's only lowering. `ld.volatile` polls the same way -- PTX
+    # ISA 8.4.2 puts it and `.relaxed` in one class, and measured they are
+    # within 0.1% everywhere -- but `.relaxed.<scope>` says the scope out loud
+    # instead of resting on `volatile` meaning `.sys`.
+    load_call, tags = _wait_until_forward(
+        f"ld.relaxed.{scope}.{space}.{suffix}", dst, ptr
+    )
+    # The helper is named after the poll, which is the load that repeats; the
+    # closing acquire hangs off that name with an `_acquire` suffix.
+    load_name = parse_str(load_call.args[0])
+    name = load_name.replace("ptx_ld_", "cuda_wait_until_")
+    source = load_call.args[-1].value.replace(load_name, name + "_load")
+    acquire_call, _ = _wait_until_forward(
+        f"ld.acquire.{scope}.{space}.{suffix}", dst, ptr
+    )
+    acquire_name = parse_str(acquire_call.args[0])
+    acquire_source = acquire_call.args[-1].value.replace(acquire_name, name + 
"_acquire")
+    source += "\n" + acquire_source
+    # NVRTC has no `__typeof__`, and `decltype` on the destination yields a
+    # reference that cannot be declared uninitialized, so the scratch takes
+    # the C type the generated helper already spells in its signature.
+    scratch_type = re.search(
+        rf"void\s+{re.escape(name)}_acquire\(\s*([\w:]+)\s*&", acquire_source
+    )
+    if scratch_type is None:  # pragma: no cover - the helper shape is fixed
+        raise RuntimeError(f"cannot read the destination type of 
{name}_acquire")
+    closing = (
+        f" {{ {scratch_type.group(1)} __tirx_wait_edge; "
+        f"{name}_acquire(__tirx_wait_edge, (ptr)); (void)__tirx_wait_edge; }}"
+    )
+    # The predicate stays at the call site: an ordinary bool argument would be
+    # evaluated once, before the load ever updates the destination.
+    # Load first, test after, which is how every spin loop in this repository
+    # is written: `ld; while (!done) { ld; }` reads once before it can decide
+    # anything, so a pre-tested loop would need the caller to seed the
+    # destination -- and the only way to seed it honestly is another load of
+    # the same word, which is one more unguaranteed read for a checker to
+    # judge. A `do`/`while` needs no seed.
+    #
+    # `unroll 1` is what a hand-written spin carries, and what the loop this
+    # replaces emitted. Without it nothing stops ptxas from duplicating the
+    # load into an unrolled body, which changes how often a waiter polls even
+    # though the instruction sequence is the same one.
+    #
+    # The first load and test are peeled out of the loop. The executed sequence
+    # is the same either way, but a single `do`/`while` gives ptxas one body to
+    # schedule and it stops emitting the early-exit branch a hand-written
+    # `ld; while (!done) { ld; }` gets -- so a waiter whose predicate already
+    # holds pays a second load and a poll it did not pay before. Measured on
+    # `sm100_fp8_fp4_mega_moe`'s grid barrier at +2.2% and on
+    # `radix_topk_multi_cta` at +1.25%, reproduced on two idle B200s with the
+    # states interleaved. Peeling costs nothing when the wait does spin.
+    #
+    # A backoff of zero is no backoff: the macro, and so the emitted code, is
+    # the one a wait without one produces, down to the argument list.
+    backoff = 0 if not hasattr(backoff_ns, "value") else int(backoff_ns.value)
+    if backoff == 0:
+        source += (
+            f"\n#define {name}(dst, ptr, predicate) "
+            f"do {{ {name}_load((dst), (ptr)); if (!(predicate)) {{ "
+            f"_Pragma(\"unroll 1\") "
+            f"do {{ {name}_load((dst), (ptr)); }} while (!(predicate)); }}"
+            f"{closing} }} while (0)\n"
+        )
+        operands = (condition,)
+    else:
+        # Between polls, never before the first one and never after the last:
+        # the shape `allgather_gemm` and `gemm_reduce_scatter` write by hand.
+        name = f"{name}_backoff"
+        source = source.replace(f"{name[: -len('_backoff')]}_load", 
f"{name}_load")
+        source += (
+            f"\n#define {name}(dst, ptr, predicate, backoff_ns) "
+            f"do {{ {name}_load((dst), (ptr)); if (!(predicate)) {{ "
+            f"_Pragma(\"unroll 1\") "
+            f"while (1) {{ __nanosleep(backoff_ns); {name}_load((dst), (ptr)); 
"
+            f"if (predicate) break; }} }}"
+            f"{closing} }} while (0)\n"
+        )
+        operands = (condition, backoff_ns)
+    return cuda_func_call(name, *load_call.args[1:-1], *operands, 
source_code=source), tags
+
+
 # =============================================================================
 # mbarrier waits — ``mbarrier.try_wait`` only polls once, so the body wraps it
 # in a branch loop that retries until the parity flips. The magic
diff --git a/python/tvm/backend/cuda/op.py b/python/tvm/backend/cuda/op.py
index ca8598aec0..0d6ea8806c 100644
--- a/python/tvm/backend/cuda/op.py
+++ b/python/tvm/backend/cuda/op.py
@@ -382,6 +382,148 @@ def cuda_float8tohalf8(src_addr, dst_addr):
     return call_intrin("", "tirx.cuda.float8tohalf8", src_addr, dst_addr)
 
 
+_WAIT_UNTIL_SCOPE = ("cta", "cluster", "gpu", "sys")
+# Global only, and that is the whole surface a declared word needs. A protocol
+# that waits inside a CTA or a cluster has `mbarrier`, which is the hardware's
+# own primitive for it and which the checker already models by generation; a
+# polled flag in shared memory would be a worse spelling of the same thing. The
+# shared-memory reads that look like a protocol in practice -- fetching a TMEM
+# allocation address out of a mailbox -- are ordinary reads after a barrier,
+# with no spin and no predicate, so they are not protocols at all.
+_WAIT_UNTIL_SPACE = ("global",)
+_WAIT_UNTIL_DTYPE = ("int32", "uint32", "int64", "uint64")
+# Widths, so a requested PTX type can be checked against the word it accesses.
+# Which spellings an instruction actually takes is the instruction table's
+# business: `add` has no `.b32` form, so a bit-typed arrival is rejected there
+# rather than listed as illegal here.
+_WAIT_UNTIL_PTX_TYPES = {
+    "b32": 32,
+    "s32": 32,
+    "u32": 32,
+    "b64": 64,
+    "s64": 64,
+    "u64": 64,
+    # A 128-bit word is a 16-byte naturally aligned span, moved by the `.b128`
+    # forms of `ld`/`st` (PTX ISA 8.3, sm_70). It carries a value no scalar
+    # predicate can test, so `wait` rejects it; `store` and `load` take it.
+    "b128": 128,
+}
+
+
+def _validate_wait_until_attrs(scope, space, ptx_type=None):
+    """The attributes a declared synchronization-word wait still carries."""
+    if scope not in _WAIT_UNTIL_SCOPE:
+        raise ValueError(f"invalid scope={scope!r}; expected one of 
{_WAIT_UNTIL_SCOPE}")
+    if space not in _WAIT_UNTIL_SPACE:
+        detail = (
+            "; a protocol that waits within a CTA or cluster belongs on an "
+            "`mbarrier`, which the checker models by generation"
+            if space == "shared"
+            else ""
+        )
+        raise ValueError(
+            f"invalid space={space!r}; expected one of 
{_WAIT_UNTIL_SPACE}{detail}"
+        )
+    if ptx_type is not None and ptx_type not in _WAIT_UNTIL_PTX_TYPES:
+        raise ValueError(
+            f"invalid ptx_type={ptx_type!r}; expected one of "
+            f"{tuple(sorted(_WAIT_UNTIL_PTX_TYPES))}"
+        )
+
+
+def _reject_wide_word_for_predicate(ptx_type, what):
+    """A predicate tests one scalar, so a 128-bit word cannot be waited on.
+
+    The exit value of a 16-byte word is not a number the loop can compare, and
+    the checker's record of a declared word's writes holds one scalar per
+    write. A kernel that has to read one spells the `ld` itself.
+    """
+    if ptx_type is not None and _WAIT_UNTIL_PTX_TYPES[ptx_type] > 64:
+        raise ValueError(
+            f"{what} does not take a {_WAIT_UNTIL_PTX_TYPES[ptx_type]}-bit 
word: "
+            "its exit value is not a scalar a predicate can test; read it with 
"
+            "a plain `ld` instead"
+        )
+
+
+def cuda_wait_until(
+    dst,
+    ptr,
+    predicate,
+    scope="gpu",
+    space="global",
+    ptx_type=None,
+    backoff_ns=None,
+):
+    """Read the global word at ``ptr`` into ``dst`` until ``predicate`` holds,
+    and leave the exit value there.
+
+    Waiting on an address this way is also what declares it a synchronization
+    word: the checker judges every access to that address against the protocol
+    the wait names, rather than as an ordinary pair of memory accesses.
+
+    ``dst`` is an initialized thread-local scalar; its current value is tested
+    first, so an already satisfied predicate performs no load. ``predicate`` is
+    a trace-time callable taking the current value, or the boolean expression
+    itself. It is re-evaluated on every iteration, so it may test ``dst``
+    against a loop-carried scalar such as a barrier's phase: the loop body only
+    loads, and nothing it does can move that scalar.
+
+    The wait always synchronizes with the contributions that made the
+    predicate hold, so data those threads published elsewhere is visible when
+    it returns. It polls ``ld.relaxed.<scope>`` and closes with one
+    ``ld.acquire.<scope>`` into a discarded register, which is the cheapest
+    spelling of that edge rather than a separate mode: paying acquire on every
+    poll costs more, and closing with an ``acq_rel``/``sc`` fence instead costs
+    far more, because the fence loses the loop's fast-path exit.
+
+    There is no way to ask for less. A wait whose exit value is the whole
+    message does take an edge it has no use for, and the two such waits in the
+    kernel corpus were measured against this form on three shapes: every ratio
+    landed inside the band that identical code measured against itself, and
+    the two smaller shapes disagreed on the sign. So a relaxed mode would buy
+    nothing here, while what it asks for is a promise made at the call site --
+    that nothing the word guards is read afterwards -- which the call site
+    cannot show and the next edit can silently break.
+
+    ``T.nvshmem.wait_until`` shares this name deliberately: both block until a
+    value satisfies a condition. They differ in what they wait on and how the
+    condition is written -- that one names a symmetric object across PEs and
+    takes an enumerated comparison; this one names an address in this device's
+    global memory and takes a predicate. A protocol that waits within a CTA or
+    cluster belongs on an ``mbarrier`` instead, which is why ``space`` admits
+    only ``global``.
+
+    ``backoff_ns`` puts a ``__nanosleep`` before each retry, as a contended
+    wait is ordinarily written. It goes before the load, so a predicate that
+    holds on entry still performs no load and no sleep, and a wait whose first
+    poll succeeds pays nothing. A kernel that spells the backoff itself writes
+    ``ld`` once and then waits, which is the same instruction sequence.
+
+    The backoff is the only thing a wait carries besides its own load, and it
+    stays a scalar for a reason: it runs every iteration, touches no memory,
+    and cannot move what the predicate reads, so it changes nothing the
+    checker concludes. A timeout that has to print and trap is not that, and
+    belongs to a loop the kernel writes itself.
+    """
+    _validate_wait_until_attrs(scope, space, ptx_type)
+    _reject_wide_word_for_predicate(ptx_type, "wait_until")
+    if tirx.is_buffer_var(dst):
+        dst = dst[0]
+    condition = tirx.convert(predicate(dst) if callable(predicate) else 
predicate)
+    return call_intrin(
+        "",
+        "tirx.cuda.wait_until",
+        dst,
+        ptr,
+        condition,
+        scope,
+        space,
+        ptx_type or "",
+        tirx.convert(0 if backoff_ns is None else backoff_ns),
+    )
+
+
 def _validate_mbarrier_arrive_attrs(sem, scope, space, remote):
     if (sem == "") != (scope == ""):
         raise ValueError("mbarrier.arrive sem and scope must be specified 
together")
diff --git a/python/tvm/backend/cuda/script.py 
b/python/tvm/backend/cuda/script.py
index 136a89e9dd..e5c1c846af 100644
--- a/python/tvm/backend/cuda/script.py
+++ b/python/tvm/backend/cuda/script.py
@@ -148,6 +148,10 @@ class CUDANamespace:
         self.mov_sreg: Callable[..., Any] = _op_wrapper(_cuda_op.cuda_mov_sreg)
         # Spin-until-ready mbarrier waits: label-loop asm blocks, not single
         # PTX instructions -- which is why they live here and not in T.ptx.
+        # One declared synchronization word: every access a protocol makes to
+        # it goes through these, so a checker can separate them from a stray
+        # access and read the word's write history off the declaration.
+        self.wait_until = _op_wrapper(_cuda_op.cuda_wait_until)
         self.mbarrier_wait = _op_wrapper(_cuda_op.cuda_mbarrier_wait)
         self.mbarrier_wait_acquire_cluster = _op_wrapper(
             _cuda_op.cuda_mbarrier_wait_acquire_cluster
diff --git a/src/backend/cuda/op/target_builtin.cc 
b/src/backend/cuda/op/target_builtin.cc
index 19526f319f..5619bcc9aa 100644
--- a/src/backend/cuda/op/target_builtin.cc
+++ b/src/backend/cuda/op/target_builtin.cc
@@ -167,6 +167,7 @@ const DeviceIntrinsicRegistration kDeviceIntrinsics[] = {
     TIRX_DEVICE_INTRIN_ALIAS(cuda_any_sync, cuda, kPure),
     TIRX_DEVICE_INTRIN_ALIAS(cuda_atomic_add, cuda, kOpaque),
     TIRX_DEVICE_INTRIN_ALIAS(cuda_atomic_cas, cuda, kOpaque),
+    TIRX_DEVICE_INTRIN_ALIAS(cuda_wait_until, cuda, kOpaque),
     TIRX_DEVICE_INTRIN_ALIAS(cuda_ballot_sync, cuda, kOpaque),
     TIRX_DEVICE_INTRIN_ALIAS(cuda_bfloat1622float2, cuda, kOpaque),
     TIRX_DEVICE_INTRIN_ALIAS(cuda_bfloat162float, cuda, kOpaque),
diff --git a/tests/python/tirx/codegen/test_cuda_wait_until.py 
b/tests/python/tirx/codegen/test_cuda_wait_until.py
new file mode 100644
index 0000000000..26fa522626
--- /dev/null
+++ b/tests/python/tirx/codegen/test_cuda_wait_until.py
@@ -0,0 +1,353 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Codegen contract for the declared wait.
+
+`wait_until` is the only operation of the set that survives. The four
+direct forms were removed: they emitted exactly what their raw spellings do, so
+they bought nothing at codegen, and a protocol's word is now recognized by the
+address the wait names rather than by each access carrying an identity. A
+publisher is spelled `red`/`atom`/`st` directly, as these tests do.
+
+What the wait itself generates is a loop, and the shape of that loop is the
+contract asserted here: it polls relaxed, closes an acquiring wait with one
+`ld.acquire`, peels the first poll, and never lets the closing read reach the
+caller's destination. The protocols exercised are the shapes real kernels use.
+"""
+
+import pytest
+
+import tvm
+from tvm.script import tirx as T
+
+
+def build(func):
+    target = tvm.target.Target({"kind": "cuda", "arch": "sm_100a"})
+    with target:
+        mod = tvm.compile(tvm.IRModule({"main": func}), target=target, 
tir_pipeline="tirx")
+    return getattr(mod, "mod", mod).imports[0].inspect_source("")
+
+
+def rendezvous(backoff_ns=None, ptx_type=None):
+    """An N-way barrier on a monotone counter, as radix_topk_multi_cta writes 
it."""
+
+    @T.prim_func
+    def kernel(state: T.Buffer((1,), "int32"), participants: T.int32):
+        T.device_entry()
+        T.cta_id([2])
+        lane = T.thread_id([32])
+        spin = T.alloc_local((1,), "int32")
+        phase = T.alloc_local((1,), "int32")
+        if lane == 0:
+            phase[0] = T.int32(0)
+            spin[0] = T.int32(0)
+            T.ptx.red.release.gpu.global_.add.s32(state.ptr_to([0]), 
T.int32(1))
+            T.cuda.wait_until(
+                spin[0],
+                state.ptr_to([0]),
+                predicate=lambda v: v >= (phase[0] + T.int32(1)) * 
participants,
+                scope="gpu",
+                ptx_type=ptx_type,
+                backoff_ns=backoff_ns,
+            )
+
+    return kernel
+
+
+def packed_contribution():
+    """Counter in the high half, payload in the low half: DeepEP's notify 
slot."""
+
+    @T.prim_func
+    def kernel(slot: T.Buffer((1,), "uint64"), out: T.Buffer((1,), "uint64"), 
n: T.int32):
+        T.device_entry()
+        T.cta_id([2])
+        lane = T.thread_id([32])
+        observed = T.alloc_local((1,), "uint64")
+        if lane == 0:
+            T.ptx.red.relaxed.gpu.global_.add.u64(
+                slot.ptr_to([0]),
+                T.bitwise_or(T.shift_left(T.uint64(1), T.uint64(32)), 
T.uint64(7)),
+            )
+            observed[0] = T.uint64(0)
+            T.cuda.wait_until(
+                observed[0],
+                slot.ptr_to([0]),
+                predicate=lambda v: T.shift_right(v, T.uint64(32)) == 
T.Cast("uint64", n),
+                scope="gpu",
+            )
+            out[0] = T.bitwise_and(observed[0], T.uint64(0xFFFFFFFF))
+
+    return kernel
+
+
+def _wait_macro(source):
+    return next(
+        line
+        for line in source.splitlines()
+        if line.startswith("#define") and "wait_until" in line
+    )
+
+
+# =============================================================================
+# The emitted loop
+# =============================================================================
+
+
+def test_the_wait_is_a_loop_beside_a_raw_publisher():
+    """The publisher is ordinary PTX; only the wait generates something new."""
+
+    source = build(rendezvous())
+    assert "red.release.gpu.global.add.s32" in source
+    assert "while (!(" in source
+
+
+def test_a_wait_loads_before_it_tests():
+    """`do { ld } while (!done)`, the way every spin here is written by hand.
+
+    A pre-tested loop would need the caller to seed the destination, and the
+    only honest seed is another load of the same word -- one more unguaranteed
+    read, in the kernel, for a checker to have an opinion about. Loading first
+    needs none.
+    """
+
+    macro = _wait_macro(build(rendezvous()))
+    assert "do { tvm_builtin_cuda_wait_until" in macro
+    assert macro.index("_load(") < macro.index("while (!(")
+
+
+def test_an_acquiring_wait_polls_relaxed_and_closes_with_one_acquire():
+    """The poll carries no ordering; a single closing `ld.acquire` takes the 
edge.
+
+    Measured against the acquiring poll over every benchmarkable kernel that
+    owns a spin wait, interleaved, round 1 dropped: -1.04% on
+    `sm100_fp8_fp4_mega_moe` (8 wait sites), -0.50% on `radix_topk_multi_cta`,
+    and within 0.3% on the other three. Paying acquire semantics on every poll
+    buys nothing -- only the last read decides what the waiter goes on to
+    observe.
+    """
+
+    source = build(rendezvous())
+    macro = _wait_macro(source)
+    assert "ld.relaxed.gpu.global.s32" in source
+    assert "ld.acquire.gpu.global.s32" in source
+    # the loop polls relaxed, and the acquire is reached once, after it
+    assert macro.count("_load(") == 2
+    assert macro.count("_acquire(") == 1
+    assert macro.index("while (!(") < macro.index("_acquire(")
+
+
+def test_the_closing_acquire_cannot_overwrite_the_waited_value():
+    """The edge read goes to a scratch, never to the caller's destination.
+
+    It may observe a value later than the one the predicate accepted, and the
+    predicates here are not all monotone -- `sm100_fp8_fp4_mega_moe`'s grid
+    barrier tests a sign-bit flip and its ring waits test equality -- so 
letting
+    it reach `dst` would hand the caller a value its own predicate rejects.
+
+    The edge survives that: these words are published by `red`/`atom` release
+    RMWs, so every contribution sits in one release sequence and an acquire
+    reading any of them synchronizes with all the earlier ones.
+    """
+
+    macro = _wait_macro(build(rendezvous()))
+    scratch = "__tirx_wait_edge"
+    assert f"{scratch};" in macro
+    assert f"_acquire({scratch}, (ptr))" in macro
+    # every write to the caller's destination comes from a polling load
+    assert "_acquire((dst)" not in macro
+
+
+def test_every_wait_takes_the_edge():
+    """There is one lowering, so a caller cannot ask for a wait worth less.
+
+    A wait that consumes only its own exit value takes an edge it does not
+    need. Both such waits in the kernel corpus were measured against this form
+    and neither could tell the difference, so the mode they would have needed
+    is not worth the thing it costs: a promise, made at the call site, that
+    nothing the word guards is read afterwards -- which is a promise the call
+    site cannot show.
+    """
+
+    macro = _wait_macro(build(rendezvous()))
+    assert "_acquire(__tirx_wait_edge, (ptr))" in macro
+    assert "ld.volatile" not in build(rendezvous())
+
+
+def test_a_bit_typed_word_keeps_its_handwritten_ptx():
+    """A kernel picks the PTX type per operation, not per word.
+
+    `red.add` has no bit-typed form, while a load that only moves the value is
+    ordinarily spelled `.b32`. Reproducing that split is what lets a migration
+    leave the emitted PTX unchanged.
+    """
+
+    source = build(rendezvous(ptx_type="b32"))
+    assert "red.release.gpu.global.add.s32" in source
+    assert "ld.relaxed.gpu.global.b32" in source
+    assert "ld.acquire.gpu.global.b32" in source
+    # the default spelling must be gone, or the override did nothing
+    assert "ld.acquire.gpu.global.s32" not in source
+
+
+def test_a_packed_word_round_trips():
+    source = build(packed_contribution())
+    assert "red.relaxed.gpu.global.add.u64" in source
+    assert "ld.relaxed.gpu.global.u64" in source
+
+
+def test_a_backoff_sleeps_between_polls_and_never_around_them():
+    """Sleep between polls, as the contended waits are written by hand: never
+    before the first poll, never after the last.
+
+    The first poll is peeled out of the loop, so the shape is
+    `ld; if (!done) { while (1) { sleep; ld; if (done) break; } }`. That runs
+    the same sequence a hand-written `while (1) { ld; if (done) break; sleep; 
}`
+    runs -- load, test, sleep, load, test -- and the peel is what keeps the
+    early exit a hand-written spin gets."""
+
+    source = build(rendezvous(backoff_ns=40))
+    macro = _wait_macro(source)
+    assert "__nanosleep(backoff_ns)" in macro
+    # The peeled poll runs before any sleep: nothing sleeps before polling 
once.
+    assert macro.index("_load(") < macro.index("__nanosleep")
+    # The peeled poll is guarded, so an already satisfied predicate never
+    # reaches the loop at all.
+    assert macro.index("_load(") < macro.index("if (!(predicate))")
+    # Inside the loop the sleep precedes the poll it separates, and the break
+    # follows that poll, so nothing sleeps after the last one.
+    loop = macro[macro.index("while (1)") :]
+    assert loop.index("__nanosleep") < loop.index("_load(")
+    assert loop.index("_load(") < loop.index("if (predicate) break;")
+    # The call site is what fixes the 40.
+    assert "_backoff(" in source and ", 40)" in source
+
+    # No backoff is the spelling a wait had before the field existed.
+    plain = build(rendezvous())
+    assert "__nanosleep" not in plain
+    assert "_backoff" not in plain
+
+
+def test_a_backoff_still_closes_the_acquire_after_the_last_poll():
+    """The edge read belongs after the loop, never inside it."""
+
+    macro = _wait_macro(build(rendezvous(backoff_ns=40)))
+    assert macro.index("if (predicate) break;") < macro.index("_acquire(")
+
+
+def test_a_wait_may_test_against_a_thread_local_scalar():
+    """A barrier tests the counter against its loop-carried phase.
+
+    The macro re-evaluates the predicate each iteration, and the loop body only
+    loads, so the phase cannot move under the wait even though it is re-read.
+    """
+
+    source = build(rendezvous())
+    assert "while (!(" in source
+    assert "phase" in source
+
+
+# =============================================================================
+# What the wait refuses
+# =============================================================================
+
+
[email protected](
+    "kwargs, error, message",
+    [
+        ({"order": "acquire"}, TypeError, "order"),
+        ({"impl": "volatile"}, TypeError, "impl"),
+        ({"scope": "warp"}, ValueError, "scope"),
+    ],
+)
+def test_a_wait_rejects_attributes_it_cannot_mean(kwargs, error, message):
+    """`order` and `impl` are gone, so a kernel still passing one is told.
+
+    They named the two axes this wait was measured on. Both settled, and a
+    silently accepted keyword would let a kernel written against the old
+    spelling keep compiling while meaning something else.
+    """
+
+    from tvm.backend.cuda.op import cuda_wait_until
+
+    with pytest.raises(error, match=message):
+        cuda_wait_until(None, None, None, **kwargs)
+
+
+def test_a_ptx_type_of_another_width_is_refused():
+    with pytest.raises(Exception, match="64 bits but the sync word is 32 
bits"):
+        build(rendezvous(ptx_type="b64"))
+
+
+def test_a_wide_word_cannot_be_waited_on():
+    """A predicate tests one scalar, and a 16-byte exit value is not one.
+
+    The wait refuses the width rather than testing half of it. The refusal 
lands
+    while the function is traced, so the definition is what is guarded here.
+    """
+
+    with pytest.raises(Exception, match="does not take a 128-bit word"):
+
+        @T.prim_func
+        def kernel(response: T.Buffer((2,), "uint64")):
+            T.device_entry()
+            T.cta_id([1])
+            lane = T.thread_id([32])
+            seen = T.local_scalar("uint64")
+            if lane == 0:
+                seen = T.uint64(0)
+                T.cuda.wait_until(
+                    seen,
+                    response.ptr_to([0]),
+                    seen != T.uint64(0),
+                    scope="gpu",
+                    ptx_type="b128",
+                )
+
+
+def test_a_declared_word_is_global_and_says_where_a_shared_wait_belongs():
+    """Shared memory is out of scope by design, not by omission.
+
+    A protocol that waits within a CTA or a cluster has `mbarrier`, which is
+    the hardware's primitive for it and which the checker models by generation.
+    A polled flag in shared memory would be a worse spelling of the same thing,
+    so the refusal points at the primitive that does belong there.
+    """
+
+    with pytest.raises(ValueError, match="mbarrier"):
+        T.cuda.wait_until(
+            None, None, None, scope="cta", space="shared", ptx_type="b32"
+        )
+
+
+def test_the_four_direct_forms_are_gone():
+    """Only the wait remains: a publisher is spelled in raw PTX.
+
+    The removed forms emitted exactly what their raw spellings do, so they 
never
+    changed codegen; the protocol's word is recognized by the address the wait
+    names instead.
+    """
+
+    retired = (
+        "atomic_ref_store",
+        "atomic_ref_add",
+        "atomic_ref_fetch_add",
+        "atomic_ref_load",
+        # The surviving wait kept the family's name until it was the only
+        # member left; `atomic` claimed an atomicity its scoped loads never
+        # had, and `ref` grouped a family of one.
+        "atomic_ref_wait",
+    )
+    assert [name for name in retired if hasattr(T.cuda, name)] == []

Reply via email to