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

spectrometerHBH 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 f20fa692d5 [TIRx][CUDA] Add PTX address expressions with immediate 
byte offsets (#20153)
f20fa692d5 is described below

commit f20fa692d5dd71d875a9e310eae3e754169888fb
Author: Bohan Hou <[email protected]>
AuthorDate: Tue Aug 18 12:34:09 2026 -0400

    [TIRx][CUDA] Add PTX address expressions with immediate byte offsets 
(#20153)
    
    This PR adds `T.ptx.addr(base, byte_offset)` to the TIRx PTX dialect: a
    pure expression that folds a compile-time signed byte displacement into
    the PTX address operand, rendering as `[%N+imm]` instead of requiring a
    separate address computation before the instruction.
    
    - `tirx.ptx.addr` is an expression only in the outer PTX call's IR: the
    helper still receives the coerced base register, while the displacement
    becomes renderer metadata baked into the instruction text and the helper
    name (`_addr<slot>_p<imm>` / `_m<imm>`).
    - Table-level `allow_imm_offset` classification of address slots, with
    validation that rejects immediate offsets on operand classes that cannot
    take them (e.g. `tmem` addresses).
    - Immediate operands are now validated to be compile-time `IntImm` at
    CUDA codegen, with an actionable error pointing at explicitly-unrolled
    loops.
    - Displacements are range-checked to int32; zero offsets normalize to
    the bare form so existing helper names are untouched.
    
    Also includes a small test fix:
    `test_tirx_kernels_registry_correctness.py` accepts both the old and new
    MegaMoE kernel registry names (`deepgemm_fp8_fp4_mega_moe` /
    `sm100_fp8_fp4_mega_moe`), so the test works against tirx-kernels
    checkouts from either side of the rename.
    
    Tested with `tests/python/tirx/codegen/test_ptx_addr.py` (new, 12
    cases), plus the full `tests/python/tirx/` suite on sm100.
---
 python/tvm/backend/cuda/ptx/__init__.py            |   5 +-
 python/tvm/backend/cuda/ptx/engine.py              | 176 ++++++++++-
 python/tvm/backend/cuda/ptx/gen_stubs.py           |   1 +
 python/tvm/backend/cuda/ptx/render.py              |  61 +++-
 python/tvm/backend/cuda/ptx/table.py               | 188 ++++++-----
 python/tvm/script/tirx.pyi                         |   1 +
 tests/python/tirx/codegen/test_ptx_addr.py         | 351 +++++++++++++++++++++
 tests/python/tirx/codegen/test_ptx_dialect.py      |  87 ++++-
 tests/python/tirx/codegen/test_ptx_ld_st_ops.py    |  19 ++
 .../tirx/test_tirx_kernels_registry_correctness.py |   9 +-
 10 files changed, 793 insertions(+), 105 deletions(-)

diff --git a/python/tvm/backend/cuda/ptx/__init__.py 
b/python/tvm/backend/cuda/ptx/__init__.py
index ec1b48643b..e30175828a 100644
--- a/python/tvm/backend/cuda/ptx/__init__.py
+++ b/python/tvm/backend/cuda/ptx/__init__.py
@@ -23,9 +23,10 @@ registers every table entry as a TVM Op with a generic 
codegen; the
 ``register_backend()`` via ``script_namespaces()``.
 """
 
-from .engine import PTXNamespace, register_table
+from .engine import PTXNamespace, register_addr, register_table
 from .table import TABLE
 
+register_addr()
 register_table(TABLE)
 
-__all__ = ["TABLE", "PTXNamespace", "register_table"]
+__all__ = ["TABLE", "PTXNamespace", "register_addr", "register_table"]
diff --git a/python/tvm/backend/cuda/ptx/engine.py 
b/python/tvm/backend/cuda/ptx/engine.py
index 3e29523af2..2b5069491f 100644
--- a/python/tvm/backend/cuda/ptx/engine.py
+++ b/python/tvm/backend/cuda/ptx/engine.py
@@ -40,6 +40,7 @@ per-instruction generated or hand-written code:
 from tvm.backend.cuda.codegen.registry import register_codegen
 from tvm.backend.cuda.codegen.utils import parse_str
 from tvm.backend.cuda.op import cuda_cvta_generic_to_shared, cuda_func_call
+from tvm.ir import Call
 from tvm.ir.op import register_op_attr
 from tvm.ir.type import PointerType, PrimType
 from tvm.runtime import const
@@ -63,6 +64,22 @@ from .table import (
 # It is also the honest answer: "do not touch my instruction" is exactly the
 # contract a hand-written PTX call wants.
 _EFFECT_OPAQUE = CallEffectKind.Opaque.value
+_EFFECT_PURE = CallEffectKind.Pure.value
+_ADDR_OP_NAME = "tirx.ptx.addr"
+_INT32_MIN = -(1 << 31)
+_INT32_MAX = (1 << 31) - 1
+_INTEGER_DTYPES = frozenset(
+    {
+        "int8",
+        "int16",
+        "int32",
+        "int64",
+        "uint8",
+        "uint16",
+        "uint32",
+        "uint64",
+    }
+)
 
 # ---------------------------------------------------------------------------
 # Registration (import time)
@@ -91,6 +108,22 @@ def register_table(table: dict[str, InstructionEntry]) -> 
None:
         register_codegen(f"ptx.{entry.name}")(_make_codegen(entry))
 
 
+def register_addr() -> None:
+    """Register the pure address-expression op consumed by PTX instructions."""
+    register_op_attr(_ADDR_OP_NAME, "TCallEffectKind", _EFFECT_PURE)
+    register_op_attr(_ADDR_OP_NAME, "TScriptPrinterName", "ptx.addr", level=20)
+    register_op_attr(_ADDR_OP_NAME, "TIRxOpCategory", "device_intrin")
+    register_op_attr(_ADDR_OP_NAME, "TDeviceIntrinsicNamespace", "ptx")
+    register_codegen("ptx.addr")(_unconsumed_addr_codegen)
+
+
+def _unconsumed_addr_codegen(*_args):
+    raise ValueError(
+        "T.ptx.addr(...) must be consumed by a PTX address operand that 
supports "
+        "immediate byte offsets"
+    )
+
+
 # ---------------------------------------------------------------------------
 # Codegen (compile time): table -> asm volatile helper
 # ---------------------------------------------------------------------------
@@ -109,6 +142,32 @@ def arg_dtype(value) -> str:
     return ty.dtype if isinstance(ty, PrimType) else type(value).__name__
 
 
+def _is_addr_call(value) -> bool:
+    return isinstance(value, Call) and getattr(value.op, "name", None) == 
_ADDR_OP_NAME
+
+
+def _codegen_addr_offset(entry, slot, value) -> tuple[object, int]:
+    """Unpack one nested ``tirx.ptx.addr`` call at CUDA codegen time."""
+    if not slot.allow_imm_offset:
+        raise ValueError(f"{entry.name}: operand '{slot.name}' does not 
support T.ptx.addr(...)")
+    if len(value.args) != 2:
+        raise ValueError("malformed tirx.ptx.addr call: expected base and 
byte_offset")
+    base, offset = value.args
+    if _is_addr_call(base):
+        raise ValueError("T.ptx.addr(...) cannot be nested")
+    if not isinstance(offset, IntImm) or arg_dtype(offset) not in 
_INTEGER_DTYPES:
+        raise ValueError(
+            f"{entry.name}: T.ptx.addr byte_offset must become a compile-time "
+            "signed int32 constant before CUDA codegen (use an 
explicitly-unrolled loop)"
+        )
+    offset = int(offset)
+    if not _INT32_MIN <= offset <= _INT32_MAX:
+        raise ValueError(
+            f"{entry.name}: T.ptx.addr byte_offset {offset} is outside signed 
int32 range"
+        )
+    return base, offset
+
+
 def _make_codegen(entry: InstructionEntry):
     n_slots = len(entry.slots)
 
@@ -126,7 +185,7 @@ def _make_codegen(entry: InstructionEntry):
         predicated = "pred" in flags
         preserve_dst = "keep" in flags
         tokens = [parse_str(a) for a in args[len(args) - n_slots - 1 : -1]]
-        rest = args[: len(args) - n_slots - 1]  # operands, plus pred when 
present
+        rest = list(args[: len(args) - n_slots - 1])  # operands, plus pred 
when present
         mod_map = mods(entry, tokens)
         layout = operand_layout(entry, mod_map)
         n_operands = sum(n for _, _, n in layout)
@@ -164,11 +223,37 @@ def _make_codegen(entry: InstructionEntry):
             return operand_dtypes(slot, mod_map)[0]
 
         dtypes = tuple(_slot_dtype(slot, i, n) for slot, i, n in layout if 
slot.kind == "reg")
-        # Caller-chosen immediates ride the Call as IntImm args but are baked
-        # into the instruction text, so they are read here and NOT forwarded to
-        # the helper (which has no parameter for them).
+        # Caller-chosen immediates ride the Call until device codegen so an
+        # explicitly-unrolled loop may specialize them. They must be IntImm by
+        # this point, then are baked into the instruction text and NOT
+        # forwarded to the helper (which has no parameter for them).
         imm_at = {i for slot, i, _ in layout if slot.kind == "imm"}
-        imms = tuple(str(int(rest[at[i]])) for i in sorted(imm_at))
+        imm_values = [rest[at[i]] for i in sorted(imm_at)]
+        if any(not isinstance(value, IntImm) for value in imm_values):
+            raise ValueError(
+                f"{entry.name}: immediate operands must become compile-time 
constants "
+                "before CUDA codegen (use an explicitly-unrolled loop)"
+            )
+        imms = tuple(str(int(value)) for value in imm_values)
+        # ``tirx.ptx.addr`` is an expression only in the outer PTX call's IR.
+        # The helper still receives the coerced base, while the signed byte
+        # displacement becomes renderer metadata baked into ``[%N+imm]``.
+        addr_offsets = []
+        logical_addr_slot = 0
+        for slot, i, lanes in layout:
+            if slot.kind != "addr":
+                continue
+            if lanes != 1:
+                raise AssertionError(
+                    f"{entry.name}: address operand '{slot.name}' must occupy 
one register"
+                )
+            value = rest[at[i]]
+            if _is_addr_call(value):
+                base, offset = _codegen_addr_offset(entry, slot, value)
+                rest[at[i]] = base
+                if offset:
+                    addr_offsets.append((logical_addr_slot, offset))
+            logical_addr_slot += 1
         _, helper, source = render_variant(
             entry,
             tokens,
@@ -177,6 +262,7 @@ def _make_codegen(entry: InstructionEntry):
             imms,
             sinks,
             preserve_dst=preserve_dst,
+            addr_offsets=tuple(addr_offsets),
         )
         # Every helper is void; a destination is an ordinary argument, printed
         # by the C codegen as the lvalue it binds the reference parameter to.
@@ -217,6 +303,48 @@ class _Sink:
 SINK = _Sink()
 
 
+class AddrArg:
+    """Temporary trace-time wrapper for ``T.ptx.addr(base, byte_offset)``.
+
+    It deliberately is not a TIR expression. An eligible outer PTX address
+    operand supplies the state space, coerces ``base``, and only then creates
+    the nested pure ``tirx.ptx.addr`` Call.
+    """
+
+    __slots__ = ("base", "byte_offset")
+
+    def __init__(self, base, byte_offset):
+        if isinstance(base, AddrArg):
+            raise ValueError("T.ptx.addr(...) cannot be nested")
+        self.base = base
+        self.byte_offset = _coerce_addr_offset(byte_offset)
+
+    def __repr__(self):
+        return f"T.ptx.addr({self.base!r}, {self.byte_offset!r})"
+
+
+def _coerce_addr_offset(value):
+    """Validate the byte displacement while preserving unrollable 
expressions."""
+    if isinstance(value, bool):
+        raise ValueError("T.ptx.addr byte_offset must be a signed int32 
integer, not bool")
+    if isinstance(value, IntImm):
+        if arg_dtype(value) not in _INTEGER_DTYPES:
+            raise ValueError(
+                f"T.ptx.addr byte_offset must be a signed int32 integer, got 
{arg_dtype(value)}"
+            )
+        value = int(value)
+    if isinstance(value, int):
+        if not _INT32_MIN <= value <= _INT32_MAX:
+            raise ValueError(f"T.ptx.addr byte_offset {value} is outside 
signed int32 range")
+        return const(value, "int32")
+    dtype = arg_dtype(value)
+    if dtype in _INTEGER_DTYPES:
+        # An explicitly-unrolled loop may specialize this expression later.
+        # CUDA codegen performs the final IntImm and range checks.
+        return value
+    raise ValueError(f"T.ptx.addr byte_offset must be a scalar integer 
expression, got {dtype}")
+
+
 class PredArg:
     """``T.ptx.pred(x)`` -- "this operand is a ``.pred`` register".
 
@@ -264,6 +392,17 @@ def _coerce_operand(entry, slot, values, mod_map):
     # unwrapped here: whether the tag is present is the discriminator, so each
     # branch below has to be able to see it.
     values = [v if isinstance(v, PredArg) else getattr(v, "scalar", v) for v 
in values]
+    addr_args = [v for v in values if isinstance(v, AddrArg)]
+    if addr_args:
+        if slot.kind != "addr" or not slot.allow_imm_offset:
+            raise ValueError(
+                f"{entry.name}: operand '{slot.name}' does not support 
T.ptx.addr(...)"
+            )
+        if len(addr_args) != len(values):
+            raise ValueError(
+                f"{entry.name}: operand '{slot.name}' cannot mix offset and 
plain addresses"
+            )
+        return [_coerce_addr_arg(entry, slot, value, mod_map) for value in 
values]
     is_pred = slot.kind == "reg" and operand_type(slot, mod_map) == "pred"
     tagged = [v for v in values if isinstance(v, PredArg)]
     if tagged and not is_pred:
@@ -280,6 +419,12 @@ def _coerce_operand(entry, slot, values, mod_map):
     return _coerce_typed(entry, slot, values, mod_map)
 
 
+def _coerce_addr_arg(entry, slot, value, mod_map):
+    base = getattr(value.base, "scalar", value.base)
+    base = _coerce_address(entry, slot, base, mod_map)
+    return call_intrin(base.ty, _ADDR_OP_NAME, base, value.byte_offset)
+
+
 def _coerce_pred_operand(entry, slot, values):
     """Coerce a ``.pred`` operand -- the one register class the C boundary 
cannot bind.
 
@@ -445,6 +590,20 @@ def _coerce_imm(entry, slot, value):
     if isinstance(value, IntImm):
         value = value.value
     if not isinstance(value, int) or isinstance(value, bool):
+        # Open immediates may be produced by an explicitly-unrolled TIR loop.
+        # Keep the integer expression in the Call; the unroll/simplify pipeline
+        # must turn it into IntImm before the codegen hook bakes it into text.
+        if slot.choices is None and arg_dtype(value) in (
+            "int8",
+            "int16",
+            "int32",
+            "int64",
+            "uint8",
+            "uint16",
+            "uint32",
+            "uint64",
+        ):
+            return value
         raise ValueError(
             f"{entry.name}: operand '{slot.name}' is an immediate in the 
instruction "
             f"text; it needs a compile-time integer constant, got 
{type(value).__name__}"
@@ -735,6 +894,11 @@ class PTXNamespace:
         """Tag an operand as a ``.pred`` register -- see :class:`PredArg`."""
         return PredArg(value)
 
+    @staticmethod
+    def addr(base, byte_offset):
+        """Form ``[base+byte_offset]`` for an eligible PTX address operand."""
+        return AddrArg(base, byte_offset)
+
     def _family(self, token):
         cands = self._by_family.get(token)
         return _InstrChain(list(cands)) if cands else None
@@ -771,7 +935,7 @@ class PTXNamespace:
 
     def __dir__(self):
         """Family names — drives tab completion."""
-        return sorted(self._family_names() | set(super().__dir__()))
+        return sorted(self._family_names() | {"addr"} | set(super().__dir__()))
 
     def __repr__(self):
         return f"<T.ptx: {len(self._family_names())} instruction families>"
diff --git a/python/tvm/backend/cuda/ptx/gen_stubs.py 
b/python/tvm/backend/cuda/ptx/gen_stubs.py
index 2d8c22222c..85b2526033 100644
--- a/python/tvm/backend/cuda/ptx/gen_stubs.py
+++ b/python/tvm/backend/cuda/ptx/gen_stubs.py
@@ -172,6 +172,7 @@ def generate() -> str:
     out.append("class _PTX:")
     for family in sorted(families):
         out.append(f"    {escape_token(family)}: _Chain_{family}")
+    out.append("    def addr(self, base: Any, byte_offset: Any) -> Any: ...")
     out.append("    def __getitem__(self, text: str) -> Any: ...")
     out.append("")
     out.append("ptx: _PTX")
diff --git a/python/tvm/backend/cuda/ptx/render.py 
b/python/tvm/backend/cuda/ptx/render.py
index 96bdab0a91..b2b2f70846 100644
--- a/python/tvm/backend/cuda/ptx/render.py
+++ b/python/tvm/backend/cuda/ptx/render.py
@@ -147,8 +147,40 @@ BRIDGE = {
 }
 
 
+def _normalize_addr_offsets(entry: InstructionEntry, addr_offsets) -> 
tuple[tuple[int, int], ...]:
+    """Validate and canonicalize ``(logical_address_slot, byte_offset)`` 
metadata."""
+    address_slots = tuple(slot for slot in entry.operands if slot.kind == 
"addr")
+    normalized = {}
+    for logical_slot, offset in addr_offsets or ():
+        if isinstance(logical_slot, bool) or not isinstance(logical_slot, int):
+            raise ValueError(f"{entry.name}: address slot index must be an 
integer")
+        if not 0 <= logical_slot < len(address_slots):
+            raise ValueError(f"{entry.name}: no address slot {logical_slot}")
+        slot = address_slots[logical_slot]
+        if not slot.allow_imm_offset:
+            raise ValueError(
+                f"{entry.name}: operand '{slot.name}' does not support an 
immediate offset"
+            )
+        if isinstance(offset, bool) or not isinstance(offset, int):
+            raise ValueError(f"{entry.name}: address byte offset must be an 
integer")
+        if not -(1 << 31) <= offset <= (1 << 31) - 1:
+            raise ValueError(f"{entry.name}: address byte offset {offset} is 
outside int32 range")
+        if logical_slot in normalized:
+            raise ValueError(f"{entry.name}: duplicate address slot 
{logical_slot}")
+        if offset:
+            normalized[logical_slot] = offset
+    return tuple(sorted(normalized.items()))
+
+
 def _helper_name(
-    entry: InstructionEntry, written, imms, dtypes, canonical, mod_map, 
sinks=()
+    entry: InstructionEntry,
+    written,
+    imms,
+    dtypes,
+    canonical,
+    mod_map,
+    sinks=(),
+    addr_offsets=(),
 ) -> str:
     """The helper's C identifier: the instruction's ISA identity, plus a
     signature discriminator only when it is no longer enough.
@@ -184,7 +216,11 @@ def _helper_name(
         f"sink_{name}" + "".join(str(lane) for _, lane in sorted(group))
         for name, group in itertools.groupby(sorted(sinks), key=lambda pair: 
pair[0])
     ]
-    isa_name = [entry.name, *written, *(imms or ()), *sunk]
+    offset_suffixes = [
+        f"addr{logical_slot}_{'p' if offset > 0 else 'm'}{abs(offset)}"
+        for logical_slot, offset in addr_offsets
+    ]
+    isa_name = [entry.name, *written, *(imms or ()), *offset_suffixes, *sunk]
     discriminator = (
         []
         if all(dtype == canon for dtype, canon in present)
@@ -203,6 +239,7 @@ def render_variant(
     imms=None,
     sinks=frozenset(),
     preserve_dst=False,
+    addr_offsets=(),
 ):
     """Render one variant: ``(opcode, helper_name, helper_source)``.
 
@@ -231,6 +268,8 @@ def render_variant(
     value on the inactive path.
     """
     mod_map = mods(entry, tokens)
+    addr_offsets = _normalize_addr_offsets(entry, addr_offsets)
+    addr_offset_of = dict(addr_offsets)
     written = [tok for tok in tokens if tok]
     opcode = ".".join([entry.ptx_name, *written])
     canonical = canonical_dtypes(entry, tokens)
@@ -241,7 +280,7 @@ def render_variant(
         # derived the same way so dispatch, stubs and certification do not
         # need to know the difference.
         assert not predicated, f"{opcode}: raw entries have no @p twin"
-        helper = _helper_name(entry, written, imms, dtypes, canonical, 
mod_map, sinks)
+        helper = _helper_name(entry, written, imms, dtypes, canonical, 
mod_map, sinks, addr_offsets)
         return opcode, helper, entry.raw_render(entry, opcode, helper, tokens, 
tuple(dtypes))
     # A helper name is the instruction's ISA identity plus, only when it is no
     # longer enough, a signature discriminator. The opcode alone stopped being
@@ -250,7 +289,7 @@ def render_variant(
     # ones that changed collides whenever two operands swap which of them is
     # non-canonical (atom's d and b do exactly that).
     imm_of = dict(zip(imm_slots(entry), imms or (), strict=True))
-    helper = _helper_name(entry, written, imms, dtypes, canonical, mod_map, 
sinks)
+    helper = _helper_name(entry, written, imms, dtypes, canonical, mod_map, 
sinks, addr_offsets)
     assert not preserve_dst or entry.has_dst, "preserve_dst requires a written 
destination"
     if predicated:
         if entry.has_dst:
@@ -269,6 +308,7 @@ def render_variant(
     bridge_counts: dict[str, int] = collections.defaultdict(int)
     dtype_of = dict(zip(entry.typed_operands, dtypes, strict=True))
     idx = 0
+    logical_addr_slot = 0
     for slot in entry.operands:
         pname = f"__{slot.name}"
         if slot.kind == "imm":
@@ -350,9 +390,11 @@ def render_variant(
                 
inputs.append(f'"{cb.constraint}"({cb.to_carrier.format(lname)})')
             bridge = BRIDGE.get(operand_type(slot, mod_map)) if slot.kind == 
"reg" else None
             if bridge is None:
-                regs.append(
-                    f"[%{idx}]" if slot.kind == "addr" and slot.bracket is 
None else f"%{idx}"
-                )
+                if slot.kind == "addr" and slot.bracket is None:
+                    offset = addr_offset_of.get(logical_addr_slot)
+                    regs.append(f"[%{idx}{f'+{offset}' if offset is not None 
else ''}]")
+                else:
+                    regs.append(f"%{idx}")
             else:
                 # The C side above bound the carrier; the instruction names a
                 # block-local register of the class the ISA actually asks for,
@@ -368,6 +410,8 @@ def render_variant(
                 regs.append(reg)
             idx += 1
         rendered.append((slot, "{" + ", ".join(regs) + "}" if is_group else 
regs[0]))
+        if slot.kind == "addr":
+            logical_addr_slot += 1
 
     # Adjacent slots naming the same `pipe` are one operand written `p|q`
     # (setp's two predicate destinations). Merged first, and into a plain text
@@ -379,7 +423,8 @@ def render_variant(
         if key is None:
             piped.extend((slot.bracket, text) for slot, text in members)
         else:
-            piped.append((members[0][0].bracket, "|".join(text for _, text in 
members)))
+            slot = members[0][0]
+            piped.append((slot.bracket, "|".join(text for _, text in members)))
 
     # Adjacent slots naming the same `bracket` are one composite memory 
operand:
     # `[tensorMap, {c0, c1}]` is a single PTX operand whose members keep their
diff --git a/python/tvm/backend/cuda/ptx/table.py 
b/python/tvm/backend/cuda/ptx/table.py
index e55c4b095b..209651c59a 100644
--- a/python/tvm/backend/cuda/ptx/table.py
+++ b/python/tvm/backend/cuda/ptx/table.py
@@ -265,6 +265,10 @@ class OperandSlot:
     space: str | None = None
     dtype: str | DtypeFn | None = None
     dtypes: tuple[str, ...] | DtypesFn | None = None
+    # Whether this independent byte-address operand accepts
+    # ``T.ptx.addr(base, byte_offset)``. Composite address members and tmem
+    # addresses are different PTX operand classes and must leave this false.
+    allow_imm_offset: bool = False
     # kind="imm" is a value in the instruction *text* (never a C parameter),
     # in one of three states, by who owns the value:
     #   literal set   -- the ISA fixed it; invisible to programs.
@@ -3722,7 +3726,7 @@ _ENTRIES = [
         check=_check_ld,
         operands=(
             OperandSlot("d", rw="w", dtypes=_ld_dst_dtypes),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
     ),
@@ -3771,7 +3775,7 @@ _ENTRIES = [
         check=_check_ld_vec,
         operands=(
             OperandSlot("d", rw="w", lanes=_vec_lanes),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
     ),
@@ -3798,7 +3802,7 @@ _ENTRIES = [
         operands=(
             # `_` means this element is not read from memory.
             OperandSlot("d", rw="w", lanes=_vec_lanes, sinkable=_sink256),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
         ),
     ),
     # Complete scalar `st` per PTX ISA 9.7.9.11, at parity with `ld`.
@@ -3823,7 +3827,7 @@ _ENTRIES = [
         ),
         check=_check_st,
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value"),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
@@ -3847,7 +3851,7 @@ _ENTRIES = [
         ),
         check=_check_st_vec,
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value", lanes=_vec_lanes),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
@@ -3868,7 +3872,7 @@ _ENTRIES = [
         cert_arch="sm_100",
         check=_check_st_vec256,
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             # ISA 9.7.9.11 puts the sink in "vector expression b" -- the data
             # being stored -- so here `_` means this element is not written to
             # memory. Sink is not a destination-only spelling.
@@ -3892,7 +3896,7 @@ _ENTRIES = [
             ModifierSlot("space", ("shared::cta",), optional=True),
         ),
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("size", dtype="u64"),
             OperandSlot("initval", kind="imm", literal="0"),
         ),
@@ -3911,7 +3915,7 @@ _ENTRIES = [
             ModifierSlot("tensormap", ("tensormap",), optional=True),
         ),
         check=_check_prefetch,
-        operands=(OperandSlot("addr", kind="addr"),),
+        operands=(OperandSlot("addr", kind="addr", allow_imm_offset=True),),
     ),
     # st.async per PTX ISA 9.7.9.12. Two syntax blocks that share nothing but
     # the mnemonic: one signals completion through an mbarrier, the other is a
@@ -3934,12 +3938,12 @@ _ENTRIES = [
             check=_check_st_async if vec else None,
             cert_arch="sm_90",
             operands=(
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("b", lanes=_vec_lanes if vec else 1),
                 # The mbarrier lives in the same state space as the destination
                 # ("`.ss` specifies the state space of the destination operand
                 # a and the mbarrier operand mbar").
-                OperandSlot("mbar", kind="addr"),
+                OperandSlot("mbar", kind="addr", allow_imm_offset=True),
             ),
         )
         for vec in (False, True)
@@ -3957,7 +3961,7 @@ _ENTRIES = [
         check=_check_st_async_rel,
         cert_arch="sm_100",
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("b"),
         ),
     ),
@@ -3986,7 +3990,7 @@ _ENTRIES = [
         cert_arch="sm_90",
         operands=(
             OperandSlot("d", rw="w"),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
         ),
     ),
     InstructionEntry(
@@ -4001,7 +4005,7 @@ _ENTRIES = [
         check=_check_multimem_int,
         cert_arch="sm_90",
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("b"),
         ),
     ),
@@ -4021,7 +4025,7 @@ _ENTRIES = [
         check=_check_multimem_int,
         cert_arch="sm_90",
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("b"),
         ),
     ),
@@ -4052,11 +4056,11 @@ _ENTRIES = [
                 *(
                     (
                         OperandSlot("d", rw="w", lanes=_vec_lanes if vec else 
1),
-                        OperandSlot("addr", kind="addr"),
+                        OperandSlot("addr", kind="addr", 
allow_imm_offset=True),
                     )
                     if mnem == "ld_reduce"
                     else (
-                        OperandSlot("addr", kind="addr"),
+                        OperandSlot("addr", kind="addr", 
allow_imm_offset=True),
                         OperandSlot("b", lanes=_vec_lanes if vec else 1),
                     )
                 ),
@@ -4109,7 +4113,7 @@ _ENTRIES = [
                 "createpolicy_range",
                 "range",
                 (
-                    OperandSlot("addr", kind="addr"),
+                    OperandSlot("addr", kind="addr", allow_imm_offset=True),
                     OperandSlot("primary_size", dtype="u32"),
                     OperandSlot("total_size", dtype="u32"),
                 ),
@@ -4150,7 +4154,7 @@ _ENTRIES = [
         ),
         cert_arch="sm_90",
         operands=(
-            OperandSlot("src_mem", kind="addr", space="global"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="global"),
             OperandSlot("size", dtype="u32"),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
@@ -4182,7 +4186,7 @@ _ENTRIES = [
             ),
             cert_arch="sm_90a",
             operands=(
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 *ops,
             ),
         )
@@ -4228,7 +4232,7 @@ _ENTRIES = [
             ),
             cert_arch="sm_90a",
             operands=(
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("new_val", kind="imm", choices=values),
             ),
         )
@@ -4287,7 +4291,7 @@ _ENTRIES = [
         ),
         operands=(
             OperandSlot("d", rw="w"),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
         ),
     ),
     InstructionEntry(
@@ -4305,7 +4309,7 @@ _ENTRIES = [
         check=_check_vec128,
         operands=(
             OperandSlot("d", rw="w", lanes=_vec_lanes),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
         ),
     ),
     # prefetchu per PTX ISA 9.7.9.16, the fourth line of that subsection: the
@@ -4314,7 +4318,7 @@ _ENTRIES = [
     InstructionEntry(
         name="prefetchu",
         slots=(ModifierSlot("level", ("L1",)),),
-        operands=(OperandSlot("addr", kind="addr"),),
+        operands=(OperandSlot("addr", kind="addr", allow_imm_offset=True),),
     ),
     # applypriority / discard per PTX ISA 9.7.9.17, 9.7.9.18. Same shape: an
     # address range and a cache level, one hinting how to evict it and the
@@ -4332,7 +4336,7 @@ _ENTRIES = [
                 ModifierSlot("level", (level,)),
             ),
             operands=(
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("size", kind="imm", literal="128"),
             ),
         )
@@ -5201,8 +5205,8 @@ _ENTRIES = [
             ),
             cert_arch="sm_90",
             operands=(
-                OperandSlot("dst_mem", kind="addr", space="shared"),
-                OperandSlot("src_mem", kind="addr", space="global"),
+                OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="shared"),
+                OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="global"),
                 OperandSlot(
                     "cp_size", kind="imm", choices=("4", "8", "16") if cop == 
"ca" else ("16",)
                 ),
@@ -5305,8 +5309,8 @@ _ENTRIES = [
         ),
         cert_arch="sm_90",
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="shared::cta"),
-            OperandSlot("src_mem", kind="addr", space="global"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="global"),
             OperandSlot("size", dtype="u32"),
             OperandSlot(
                 "ignore_bytes_left",
@@ -5320,7 +5324,7 @@ _ENTRIES = [
                 lanes=_ignore_oob_lanes,
                 vector=False,
             ),
-            OperandSlot("mbar", kind="addr", space="shared"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared"),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
         ),
     ),
@@ -5338,10 +5342,10 @@ _ENTRIES = [
         ),
         cert_arch="sm_90",
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="shared::cluster"),
-            OperandSlot("src_mem", kind="addr", space="global"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="shared::cluster"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="global"),
             OperandSlot("size", dtype="u32"),
-            OperandSlot("mbar", kind="addr", space="shared"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared"),
             OperandSlot("cta_mask", dtype="u16", lanes=_tma_mask_lanes, 
vector=False),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
         ),
@@ -5358,10 +5362,10 @@ _ENTRIES = [
         ),
         cert_arch="sm_90",
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="shared::cluster"),
-            OperandSlot("src_mem", kind="addr", space="shared::cta"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="shared::cluster"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("size", dtype="u32"),
-            OperandSlot("mbar", kind="addr", space="shared"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared"),
         ),
     ),
     InstructionEntry(  # shared::cta -> global
@@ -5396,8 +5400,8 @@ _ENTRIES = [
         ),
         cert_arch="sm_100a",
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="global"),
-            OperandSlot("src_mem", kind="addr", space="shared::cta"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="global"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("size", dtype="u32"),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
             # "the 16-bit wide byteMask operand" -- the legacy helper bound it
@@ -5441,10 +5445,10 @@ _ENTRIES = [
         cert_arch="sm_100a",
         check=_check_tma_gather4,
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="shared::cluster"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="shared::cluster"),
             OperandSlot("tmap", kind="addr", space="global", bracket="src"),
             OperandSlot("coords", dtype="s32", lanes=_tma_coords_lanes, 
bracket="src"),
-            OperandSlot("mbar", kind="addr", space="shared"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared"),
             OperandSlot("cta_mask", dtype="u16", lanes=_tma_mask_lanes, 
vector=False),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
         ),
@@ -5467,10 +5471,10 @@ _ENTRIES = [
         cert_arch="sm_100a",
         check=_check_tma_gather4,
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="shared::cta"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("tmap", kind="addr", space="global", bracket="src"),
             OperandSlot("coords", dtype="s32", lanes=_tma_coords_lanes, 
bracket="src"),
-            OperandSlot("mbar", kind="addr", space="shared"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared"),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
         ),
     ),
@@ -5493,7 +5497,7 @@ _ENTRIES = [
         operands=(
             OperandSlot("tmap", kind="addr", space="global", bracket="dst"),
             OperandSlot("coords", dtype="s32", lanes=_tma_coords_lanes, 
bracket="dst"),
-            OperandSlot("src_mem", kind="addr", space="shared::cta"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
         ),
     ),
@@ -5517,7 +5521,7 @@ _ENTRIES = [
         operands=(
             OperandSlot("tmap", kind="addr", space="global", bracket="dst"),
             OperandSlot("coords", dtype="s32", lanes=_tma_coords_lanes, 
bracket="dst"),
-            OperandSlot("src_mem", kind="addr", space="shared::cta"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("cache_policy", dtype="u64", lanes=_tma_cache_lanes, 
vector=False),
         ),
     ),
@@ -5918,7 +5922,7 @@ _ENTRIES = [
         ),
         orders_memory=True,
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             # "The only supported value for the size operand is 128, which must
             # be a constant integer literal" -- ISA 9.7.14.4.
             OperandSlot("size", kind="imm", literal="128"),
@@ -5940,7 +5944,7 @@ _ENTRIES = [
         check=_check_atomic,
         operands=(
             OperandSlot("d", rw="w"),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value"),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
@@ -5963,7 +5967,7 @@ _ENTRIES = [
         ),
         operands=(
             OperandSlot("d", rw="w"),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("compare"),
             OperandSlot("value"),
         ),
@@ -5984,7 +5988,7 @@ _ENTRIES = [
         check=_check_cache_hint,
         operands=(
             OperandSlot("d", rw="w"),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value"),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
@@ -6008,7 +6012,7 @@ _ENTRIES = [
             check=_check_cache_hint,
             operands=(
                 *((OperandSlot("d", rw="w"),) if mnem == "atom" else ()),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("value"),
                 OperandSlot(
                     "cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False
@@ -6042,7 +6046,7 @@ _ENTRIES = [
             check=_check_atom_vec,
             operands=(
                 OperandSlot("d", rw="w", lanes=_vec_lanes),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("value", lanes=_vec_lanes),
                 OperandSlot(
                     "cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False
@@ -6067,7 +6071,7 @@ _ENTRIES = [
         ),
         check=_check_atomic,
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value"),
             OperandSlot("cache_policy", dtype="u64", 
lanes=_present_lanes("cache"), vector=False),
         ),
@@ -6134,7 +6138,7 @@ _ENTRIES = [
             ModifierSlot("type", ("b64",)),
         ),
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("count", dtype="u32"),
         ),
     ),
@@ -6172,7 +6176,7 @@ _ENTRIES = [
             check=_check_mbarrier_sem_scope,
             operands=(
                 OperandSlot("state", kind="imm", literal="_"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
             ),
         )
         for act in ("arrive", "arrive_drop")
@@ -6191,7 +6195,7 @@ _ENTRIES = [
             check=_check_mbarrier_sem_scope,
             operands=(
                 OperandSlot("state", kind="imm", literal="_"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("count", dtype="u32"),
             ),
         )
@@ -6212,7 +6216,7 @@ _ENTRIES = [
             check=_check_mbarrier_sem_scope,
             operands=(
                 OperandSlot("state", kind="imm", literal="_"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("tx_count", dtype="u32"),
             ),
         )
@@ -6242,7 +6246,7 @@ _ENTRIES = [
                 # carrier, and ptxas rejects an .f64 register as the state 
operand
                 # ("Arguments mismatch for instruction 'mbarrier.arrive'").
                 OperandSlot("state", rw="rw", dtype="b64i"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("count", dtype="u32"),
             ),
         )
@@ -6268,7 +6272,7 @@ _ENTRIES = [
             check=_check_mbarrier_sem_scope,
             operands=(
                 OperandSlot("wait_complete", rw="w", dtype="pred"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("phase", dtype="u32"),
                 *((OperandSlot("time_hint", dtype="u32"),) if act == 
"try_wait" else ()),
             ),
@@ -6289,7 +6293,7 @@ _ENTRIES = [
         check=_check_mbarrier_sem_scope,
         operands=(
             OperandSlot("wait_complete", rw="w", dtype="pred"),
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("phase", dtype="u32"),
         ),
     ),
@@ -6306,7 +6310,7 @@ _ENTRIES = [
             ),
             check=_check_mbarrier_sem_scope,
             operands=(
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("tx_count", dtype="u32"),
             ),
         )
@@ -6320,7 +6324,7 @@ _ENTRIES = [
             ModifierSlot("space", ("shared", "shared::cta"), optional=True),
             ModifierSlot("type", ("b64",)),
         ),
-        operands=(OperandSlot("addr", kind="addr"),),
+        operands=(OperandSlot("addr", kind="addr", allow_imm_offset=True),),
     ),
     # The state-returning arrive lines (PTX ISA 9.7.14.16.16 / .17). The
     # entries above bake `_` into the text, which is the only spelling the
@@ -6344,7 +6348,7 @@ _ENTRIES = [
                 # integer register of either signedness and rejects a float one
                 # ("Arguments mismatch"), which is what `b64i` names.
                 OperandSlot("state", rw="w", dtype="b64i"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 *((OperandSlot("count", dtype="u32"),) if count else ()),
             ),
         )
@@ -6375,7 +6379,7 @@ _ENTRIES = [
             check=_check_mbarrier_sem_scope,
             operands=(
                 OperandSlot("wait_complete", rw="w", dtype="pred"),
-                OperandSlot("addr", kind="addr"),
+                OperandSlot("addr", kind="addr", allow_imm_offset=True),
                 OperandSlot("state", dtype="b64i"),  # the token an arrive 
returned
                 *((OperandSlot("time_hint", dtype="u32"),) if hint else ()),
             ),
@@ -6401,8 +6405,8 @@ _ENTRIES = [
         ),
         cert_arch="sm_90",
         operands=(
-            OperandSlot("dst_mem", kind="addr", space="global"),
-            OperandSlot("src_mem", kind="addr", space="shared::cta"),
+            OperandSlot("dst_mem", kind="addr", allow_imm_offset=True, 
space="global"),
+            OperandSlot("src_mem", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("size", kind="imm", literal="128"),
         ),
     ),
@@ -6423,9 +6427,9 @@ _ENTRIES = [
         check=_check_red_async,
         cert_arch="sm_90",
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value"),
-            OperandSlot("mbar", kind="addr"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True),
         ),
     ),
     # The release line of the same subsection, which reduces straight into
@@ -6447,7 +6451,7 @@ _ENTRIES = [
         check=_check_st_async_rel,
         cert_arch="sm_100",
         operands=(
-            OperandSlot("addr", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
             OperandSlot("value"),
         ),
     ),
@@ -6480,7 +6484,7 @@ _ENTRIES = [
         # sm_90+, so pinning the operand to shared would bind a 32-bit register
         # under the space-omitted spelling. `operand_space` reads the entry's
         # `space` slot instead, so the carrier follows the spelling.
-        operands=(OperandSlot("addr", kind="addr"),),
+        operands=(OperandSlot("addr", kind="addr", allow_imm_offset=True),),
     ),
     InstructionEntry(  # mbarrier.pending_count.b64 count, state;
         # The reader of the `state` result the two `.noComplete` entries above
@@ -6534,8 +6538,8 @@ _ENTRIES = [
         # `operand_space` read the entry's `space` slot gives each variant the
         # carrier its own spelling promises -- the mbarrier-family rule.
         operands=(
-            OperandSlot("addr", kind="addr"),
-            OperandSlot("mbar", kind="addr"),
+            OperandSlot("addr", kind="addr", allow_imm_offset=True),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True),
         ),
     ),
     # clusterlaunchcontrol.query_cancel per PTX ISA 9.7.14.19: decode the
@@ -6942,7 +6946,7 @@ _ENTRIES = [
         ),
         operands=(
             OperandSlot("r", rw="w", dtype="b32", lanes=_ldmatrix_lanes),
-            OperandSlot("p", kind="addr"),
+            OperandSlot("p", kind="addr", allow_imm_offset=True),
         ),
     ),
     InstructionEntry(  # line 1, .m16n16.b8: "only .x1 and .x2 are valid"
@@ -6964,7 +6968,7 @@ _ENTRIES = [
         cert_arch="sm_100a",
         operands=(
             OperandSlot("r", rw="w", dtype="b32", lanes=_ldmatrix_lanes),
-            OperandSlot("p", kind="addr"),
+            OperandSlot("p", kind="addr", allow_imm_offset=True),
         ),
     ),
     InstructionEntry(  # lines 2+3: the 6/4-bit decompression loads
@@ -6984,7 +6988,7 @@ _ENTRIES = [
         check=_check_ldmatrix_b8fmt,
         operands=(
             OperandSlot("r", rw="w", dtype="b32", lanes=_ldmatrix_lanes),
-            OperandSlot("p", kind="addr"),
+            OperandSlot("p", kind="addr", allow_imm_offset=True),
         ),
     ),
     # stmatrix per PTX ISA 9.7.15.5.16 -- the store mirror of ldmatrix. One
@@ -7010,7 +7014,7 @@ _ENTRIES = [
         ),
         cert_arch="sm_90",  # ISA: "Requires sm_90 or higher."
         operands=(
-            OperandSlot("p", kind="addr"),
+            OperandSlot("p", kind="addr", allow_imm_offset=True),
             OperandSlot("r", dtype="b32", lanes=_matrix_num_lanes),
         ),
     ),
@@ -7029,7 +7033,7 @@ _ENTRIES = [
         ),
         cert_arch="sm_100a",
         operands=(
-            OperandSlot("p", kind="addr"),
+            OperandSlot("p", kind="addr", allow_imm_offset=True),
             OperandSlot("r", dtype="b32", lanes=_matrix_num_lanes),
         ),
     ),
@@ -7465,7 +7469,7 @@ _ENTRIES = [
         cert_arch="sm_100a",
         orders_memory=True,
         operands=(
-            OperandSlot("dst", kind="addr", space="shared::cta"),
+            OperandSlot("dst", kind="addr", allow_imm_offset=True, 
space="shared::cta"),
             OperandSlot("ncols", dtype="u32"),
         ),
     ),
@@ -7795,7 +7799,9 @@ _ENTRIES = [
         ),
         cert_arch="sm_100a",
         orders_memory=True,
-        operands=(OperandSlot("mbar", kind="addr", space="shared::cluster"),),
+        operands=(
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared::cluster"),
+        ),
     ),
     # tcgen05.commit...{.shared::cluster}.multicast::cluster.b64 [mbar], 
ctaMask;
     # The multicast form: `pred` is keyword-only, so the trailing mask
@@ -7815,7 +7821,7 @@ _ENTRIES = [
         cert_arch="sm_100a",
         orders_memory=True,
         operands=(
-            OperandSlot("mbar", kind="addr", space="shared::cluster"),
+            OperandSlot("mbar", kind="addr", allow_imm_offset=True, 
space="shared::cluster"),
             OperandSlot("mask", dtype="u16"),
         ),
     ),
@@ -7840,6 +7846,34 @@ _ENTRIES = [
     ),
 ]
 
+
+def _validate_imm_offset_slots(entries) -> None:
+    """Reject capability bits on operand classes that cannot spell 
``[addr+imm]``."""
+    errors = []
+    for entry in entries:
+        for slot in entry.operands:
+            if not slot.allow_imm_offset:
+                continue
+            reasons = []
+            if slot.kind != "addr":
+                reasons.append(f"kind={slot.kind!r}, expected 'addr'")
+            if slot.bracket is not None:
+                reasons.append("is a composite bracket member")
+            if slot.space == "tmem" or (
+                slot.space is None
+                and any(
+                    modifier.name == "space" and "tmem" in modifier.choices
+                    for modifier in entry.slots
+                )
+            ):
+                reasons.append("is a tmem address")
+            if reasons:
+                errors.append(f"{entry.name}.{slot.name}: " + "; 
".join(reasons))
+    if errors:
+        raise ValueError("invalid allow_imm_offset slots:\n  " + "\n  
".join(errors))
+
+
+_validate_imm_offset_slots(_ENTRIES)
 TABLE: dict[str, InstructionEntry] = {e.name: e for e in _ENTRIES}
 # Keying by name silently drops a duplicate, and a dropped entry is an ISA line
 # that stops being reachable. Two entries never legitimately share a name.
diff --git a/python/tvm/script/tirx.pyi b/python/tvm/script/tirx.pyi
index ac9e092c12..89c3d9fd11 100644
--- a/python/tvm/script/tirx.pyi
+++ b/python/tvm/script/tirx.pyi
@@ -2286,6 +2286,7 @@ class _PTX:
     vote_sync: _Chain_vote_sync
     wgmma: _Chain_wgmma
     xor: _Chain_xor
+    def addr(self, base: Any, byte_offset: Any) -> Any: ...
     def __getitem__(self, text: str) -> Any: ...
 
 ptx: _PTX
diff --git a/tests/python/tirx/codegen/test_ptx_addr.py 
b/tests/python/tirx/codegen/test_ptx_addr.py
new file mode 100644
index 0000000000..fa4dac30b6
--- /dev/null
+++ b/tests/python/tirx/codegen/test_ptx_addr.py
@@ -0,0 +1,351 @@
+# 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.
+"""Tests for ``T.ptx.addr(base, byte_offset)``."""
+
+import pytest
+
+import tvm
+from tvm.ir import Call, Op
+from tvm.runtime import const
+from tvm.script import tirx as T
+from tvm.tirx.expr import Broadcast, CallEffectKind
+
+TARGET = tvm.target.Target("cuda")
+
+
+def _cuda_source(func) -> str:
+    with TARGET:
+        mod = tvm.compile(tvm.IRModule({"main": func}), target=TARGET, 
tir_pipeline="tirx")
+    return mod.mod.imports[0].inspect_source("cuda")
+
+
+def _calls(func, op_name):
+    calls = []
+
+    def visit(node):
+        if isinstance(node, Call) and getattr(node.op, "name", None) == 
op_name:
+            calls.append(node)
+
+    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    return calls
+
+
+def test_ptx_addr_registration_and_table_capabilities():
+    from tvm.backend.cuda.codegen.registry import CODEGEN_REGISTRY
+    from tvm.backend.cuda.ptx.table import TABLE
+
+    op = Op.get("tirx.ptx.addr")
+    assert int(op.get_attr("TCallEffectKind")) == CallEffectKind.Pure.value
+    assert op.get_attr("TScriptPrinterName") == "ptx.addr"
+    assert "tirx.ptx.addr" in CODEGEN_REGISTRY
+
+    addresses = [slot for entry in TABLE.values() for slot in entry.operands 
if slot.kind == "addr"]
+    assert len(addresses) == 145
+    assert sum(slot.allow_imm_offset for slot in addresses) == 115
+    assert sum(slot.bracket is not None and not slot.allow_imm_offset for slot 
in addresses) == 5
+    assert sum(slot.space == "tmem" and not slot.allow_imm_offset for slot in 
addresses) == 25
+
+
+def test_ptx_addr_table_validation_rejects_wrong_operand_classes():
+    from tvm.backend.cuda.ptx.table import (
+        InstructionEntry,
+        OperandSlot,
+        _validate_imm_offset_slots,
+    )
+
+    bad_entries = (
+        InstructionEntry("reg", (OperandSlot("x", allow_imm_offset=True),)),
+        InstructionEntry("ptr", (OperandSlot("x", kind="ptr", 
allow_imm_offset=True),)),
+        InstructionEntry(
+            "composite",
+            (OperandSlot("x", kind="addr", bracket="pair", 
allow_imm_offset=True),),
+        ),
+        InstructionEntry(
+            "tmem",
+            (OperandSlot("x", kind="addr", space="tmem", 
allow_imm_offset=True),),
+        ),
+    )
+    with pytest.raises(ValueError, match="reg.x.*kind='reg'"):
+        _validate_imm_offset_slots(bad_entries)
+
+
+def test_ptx_addr_coercion_ir_order_and_shared_codegen():
+    @T.prim_func
+    def kernel(global_buf: T.Buffer((8,), "uint64"), raw_shared: T.uint32, 
raw_global: T.uint64):
+        T.device_entry()
+        tx = T.thread_id([32])
+        shared_buf = T.alloc_buffer((8,), "uint64", scope="shared")
+        value = T.local_scalar("uint64")
+        if tx == 0:
+            T.ptx.ld.shared.b64(value, T.ptx.addr(shared_buf.data, 4))
+            T.ptx.ld.shared.b64(value, T.ptx.addr(raw_shared, 8))
+            T.ptx.ld.global_.b64(value, T.ptx.addr(global_buf.data, 12))
+            T.ptx.ld.global_.b64(value, T.ptx.addr(raw_global, 16))
+
+    calls = _calls(kernel, "tirx.ptx.addr")
+    assert len(calls) == 4
+    assert getattr(calls[0].args[0].op, "name", None) == 
"tirx.cuda.cvta_generic_to_shared"
+    assert not isinstance(calls[1].args[0], Call)
+    assert getattr(calls[2].args[0].op, "name", None) == "tirx.buffer_data"
+    assert getattr(calls[3].args[0].op, "name", None) == "tirx.reinterpret"
+    assert [int(call.args[1]) for call in calls] == [4, 8, 12, 16]
+    assert all(call.ty == call.args[0].ty for call in calls)
+
+    source = _cuda_source(kernel)
+    assert "ld.shared.b64 %0, [%1+4];" in source
+    assert "ld.shared.b64 %0, [%1+8];" in source
+    # The shared pointer needs cvta; the raw uint32 shared-window address does 
not.
+    assert source.count("__cvta_generic_to_shared") == 1
+
+
+def test_ptx_addr_scalar_vector_cache_predicate_and_multi_address_codegen():
+    @T.prim_func
+    def kernel(
+        src: T.Buffer((64,), "uint32"),
+        dst: T.Buffer((64,), "uint32"),
+        policy: T.Buffer((1,), "uint64"),
+    ):
+        T.device_entry()
+        tx = T.thread_id([32])
+        shared_buf = T.alloc_buffer((64,), "uint32", scope="shared")
+        barrier = T.alloc_buffer((1,), "uint64", scope="shared")
+        values = T.alloc_local((2,), "uint32")
+        T.ptx.ld.global_.b32(values[0], T.ptx.addr(src.data, 16))
+        T.ptx.ld.global_.L2__cache_hint.b32(values[1], T.ptx.addr(src.data, 
-16), policy[0])
+        T.ptx.ld.shared.v2.b32(values[0], values[1], 
T.ptx.addr(shared_buf.data, 8))
+        T.ptx.st.global_.b32(T.ptx.addr(dst.data, 0), values[0])
+        T.ptx.st.global_.v2.b32(T.ptx.addr(dst.data, 32), values[0], 
values[1], pred=tx == 0)
+        T.ptx["cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes"](
+            T.ptx.addr(shared_buf.data, 16),
+            T.ptx.addr(src.data, -16),
+            T.uint32(16),
+            T.ptx.addr(barrier.data, 8),
+        )
+
+    source = _cuda_source(kernel)
+    assert "ld.global.b32 %0, [%1+16];" in source
+    assert "ld.global.L2::cache_hint.b32 %0, [%1+-16], %2;" in source
+    assert "ld.shared.v2.b32 {%0, %1}, [%2+8];" in source
+    assert "st.global.b32 [%0], %1;" in source
+    assert "@p st.global.v2.b32 [%0+32], {%1, %2};" in source
+    assert (
+        "cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes "
+        "[%0+16], [%1+-16], %2, [%3+8];"
+    ) in source
+
+
+def test_ptx_addr_zero_sign_boundaries_and_helper_names():
+    from tvm.backend.cuda.ptx.render import render_variant
+    from tvm.backend.cuda.ptx.table import TABLE, tokens_for
+
+    entry = TABLE["ld"]
+    tokens = tokens_for(entry, space="global", type="b32")
+    bare = render_variant(entry, tokens)
+    zero = render_variant(entry, tokens, addr_offsets=((0, 0),))
+    positive = render_variant(entry, tokens, addr_offsets=((0, 16),))
+    negative = render_variant(entry, tokens, addr_offsets=((0, -16),))
+    low = render_variant(entry, tokens, addr_offsets=((0, -(1 << 31)),))
+    high = render_variant(entry, tokens, addr_offsets=((0, (1 << 31) - 1),))
+
+    assert zero == bare
+    assert bare[1] == "tvm_builtin_ptx_ld_global_b32"
+    assert positive[1].endswith("_addr0_p16")
+    assert negative[1].endswith("_addr0_m16")
+    assert "[%1+16]" in positive[2]
+    assert "[%1+-16]" in negative[2]
+    assert "[%1+-2147483648]" in low[2]
+    assert "[%1+2147483647]" in high[2]
+
+    from tvm.backend.cuda.ptx.table import renderings
+
+    cp_entry = TABLE["cp_async_ca"]
+    cp_tokens, cp_dtypes, cp_predicated, cp_imms, cp_sinks = 
next(iter(renderings(cp_entry)))
+    _, helper, source = render_variant(
+        cp_entry,
+        cp_tokens,
+        cp_predicated,
+        cp_dtypes,
+        cp_imms,
+        cp_sinks,
+        addr_offsets=((0, 16), (1, -16)),
+    )
+    assert helper.endswith("_addr0_p16_addr1_m16")
+    assert "[%0+16], [%1+-16]" in source
+
+    for value in (-(1 << 31) - 1, 1 << 31):
+        with pytest.raises(ValueError, match="outside int32 range"):
+            render_variant(entry, tokens, addr_offsets=((0, value),))
+
+
+def test_ptx_addr_unrolled_expression_and_dynamic_rejection():
+    @T.prim_func
+    def unrolled(src: T.Buffer((16,), "uint32")):
+        T.device_entry()
+        tx = T.thread_id([32])
+        value = T.local_scalar("uint32")
+        if tx == 0:
+            for i in T.unroll(3):
+                T.ptx.ld.global_.b32(value, T.ptx.addr(src.data, i * 16))
+
+    source = _cuda_source(unrolled)
+    assert "ld.global.b32 %0, [%1];" in source
+    assert "ld.global.b32 %0, [%1+16];" in source
+    assert "ld.global.b32 %0, [%1+32];" in source
+
+    @T.prim_func
+    def thread_dynamic(src: T.Buffer((16,), "uint32")):
+        T.device_entry()
+        tx = T.thread_id([32])
+        value = T.local_scalar("uint32")
+        T.ptx.ld.global_.b32(value, T.ptx.addr(src.data, tx * 4))
+
+    @T.prim_func
+    def loop_dynamic(src: T.Buffer((16,), "uint32")):
+        T.device_entry()
+        tx = T.thread_id([32])
+        value = T.local_scalar("uint32")
+        if tx == 0:
+            for i in T.serial(2):
+                T.ptx.ld.global_.b32(value, T.ptx.addr(src.data, i * 4))
+
+    for func in (thread_dynamic, loop_dynamic):
+        with pytest.raises(
+            (ValueError, tvm.error.InternalError), match="must become a 
compile-time"
+        ):
+            _cuda_source(func)
+
+
+def test_ptx_addr_offset_type_and_range_rejections():
+    for value in (True, 1.5, Broadcast(const(1, "int32"), 4)):
+        with pytest.raises(ValueError, match="byte_offset"):
+            T.ptx.addr(None, value)
+    for value in (-(1 << 31) - 1, 1 << 31):
+        with pytest.raises(ValueError, match="outside signed int32 range"):
+            T.ptx.addr(None, value)
+    with pytest.raises(ValueError, match="cannot be nested"):
+        T.ptx.addr(T.ptx.addr(None, 0), 4)
+
+
+def test_ptx_addr_pointer_and_raw_address_validation():
+    with pytest.raises(
+        (ValueError, tvm.error.DiagnosticError), match="uint32 address 
requires shared"
+    ):
+
+        @T.prim_func
+        def global_u32(raw: T.uint32):
+            T.device_entry()
+            value = T.local_scalar("uint32")
+            T.ptx.ld.global_.b32(value, T.ptx.addr(raw, 4))
+
+    with pytest.raises(
+        (ValueError, tvm.error.DiagnosticError), match="does not support 
T.ptx.addr"
+    ):
+
+        @T.prim_func
+        def ptr_operand(src: T.Buffer((8,), "uint32")):
+            T.device_entry()
+            result = T.local_scalar("uint32")
+            T.ptx.isspacep.global_(result, T.ptx.addr(src.data, 4))
+
+
+def test_ptx_addr_tma_tmem_and_independent_immediate_rejections():
+    with pytest.raises(
+        (ValueError, tvm.error.DiagnosticError), match="does not support 
T.ptx.addr"
+    ):
+
+        @T.prim_func
+        def tma(tmap: T.Buffer((8,), "uint64")):
+            T.device_entry()
+            shared_buf = T.alloc_buffer((16,), "uint32", scope="shared")
+            barrier = T.alloc_buffer((1,), "uint64", scope="shared")
+            
T.ptx["cp.async.bulk.tensor.1d.shared::cta.global.mbarrier::complete_tx::bytes"](
+                shared_buf.data, T.ptx.addr(tmap.data, 16), T.int32(0), 
barrier.data
+            )
+
+    with pytest.raises(
+        (ValueError, tvm.error.DiagnosticError), match="does not support 
T.ptx.addr"
+    ):
+
+        @T.prim_func
+        def tmem(raw: T.uint32):
+            T.device_entry()
+            value = T.local_scalar("uint32")
+            T.ptx["tcgen05.ld.sync.aligned.32x32b.x1.b32"](value, 
T.ptx.addr(raw, 16))
+
+    from tvm.backend.cuda.ptx.render import render_variant
+    from tvm.backend.cuda.ptx.table import TABLE, variants
+
+    entry = TABLE["tcgen05_ld_split"]
+    tokens = variants(entry)[0]
+    _, helper, source = render_variant(entry, tokens, imms=("16",))
+    assert helper.endswith("_16")
+    assert "[%" in source and "], 16;" in source
+    with pytest.raises(ValueError, match="does not support an immediate 
offset"):
+        render_variant(entry, tokens, imms=("16",), addr_offsets=((0, 16),))
+
+
+def test_ptx_addr_printer_script_and_json_roundtrip():
+    @T.prim_func
+    def kernel(src: T.Buffer((8,), "uint32"), dst: T.Buffer((8,), "uint32")):
+        T.device_entry()
+        value = T.local_scalar("uint32")
+        T.ptx.ld.global_.b32(value, T.ptx.addr(src.data, -16))
+        T.ptx.st.global_.b32(T.ptx.addr(dst.data, 16), value)
+
+    script = kernel.script()
+    assert script.count("T.ptx.addr(") == 2
+    tvm.ir.assert_structural_equal(kernel, tvm.script.from_source(script))
+    tvm.ir.assert_structural_equal(kernel, 
tvm.ir.load_json(tvm.ir.save_json(kernel)))
+
+
+def test_ptx_addr_legacy_positional_offsets_rejected():
+    with pytest.raises((ValueError, tvm.error.DiagnosticError)):
+
+        @T.prim_func
+        def scalar_load(src: T.Buffer((8,), "uint32")):
+            T.device_entry()
+            value = T.local_scalar("uint32")
+            T.ptx.ld.global_.b32(value, src.data, 16)
+
+    with pytest.raises((ValueError, tvm.error.DiagnosticError)):
+
+        @T.prim_func
+        def vector_load(src: T.Buffer((8,), "uint32")):
+            T.device_entry()
+            values = T.alloc_local((2,), "uint32")
+            T.ptx.ld.global_.v2.b32(values[0], values[1], src.data, 16)
+
+    with pytest.raises((ValueError, tvm.error.DiagnosticError)):
+
+        @T.prim_func
+        def scalar_store(dst: T.Buffer((8,), "uint32")):
+            T.device_entry()
+            T.ptx.st.global_.b32(dst.data, 16, T.uint32(0))
+
+    with pytest.raises((ValueError, tvm.error.DiagnosticError)):
+
+        @T.prim_func
+        def vector_store(dst: T.Buffer((8,), "uint32")):
+            T.device_entry()
+            T.ptx.st.global_.v2.b32(dst.data, 16, T.uint32(0), T.uint32(0))
+
+
+def test_ptx_addr_unconsumed_codegen_diagnostic():
+    from tvm.backend.cuda.codegen.registry import CODEGEN_REGISTRY
+
+    with pytest.raises(ValueError, match="must be consumed by a PTX address 
operand"):
+        CODEGEN_REGISTRY["tirx.ptx.addr"]([const(0, "uint64"), const(16, 
"int32")])
diff --git a/tests/python/tirx/codegen/test_ptx_dialect.py 
b/tests/python/tirx/codegen/test_ptx_dialect.py
index f08d6ea419..fe1ffaebe0 100644
--- a/tests/python/tirx/codegen/test_ptx_dialect.py
+++ b/tests/python/tirx/codegen/test_ptx_dialect.py
@@ -16,6 +16,7 @@
 # under the License.
 """Tests for the table-driven PTX dialect (``T.ptx``)."""
 
+import itertools
 import os
 import re
 import shutil
@@ -1112,16 +1113,35 @@ def test_ptx_logic_shift_dispatch():
             T.device_entry()
             T.ptx.shl.s32(out[0], T.int32(1), T.uint32(2))
 
-    # The LUT byte lives in the instruction text, so it has to be a constant.
-    with pytest.raises((ValueError, tvm.error.DiagnosticError), 
match="compile-time integer"):
-
-        @T.prim_func
-        def lut_runtime(a_ptr: T.handle):
-            A = T.match_buffer(a_ptr, (1,), "uint32")
-            T.device_entry()
+    # Open immediates may survive tracing so explicitly-unrolled expressions
+    # can specialize, but a runtime LUT byte still has no register form and is
+    # rejected at CUDA codegen.
+    @T.prim_func
+    def lut_runtime(a_ptr: T.handle):
+        A = T.match_buffer(a_ptr, (1,), "uint32")
+        T.device_entry()
+        tx = T.thread_id([32])
+        if tx == 0:
             d = T.local_scalar("uint32")
             T.ptx.lop3.b32(d, A[0], A[0], A[0], A[0])
 
+    with pytest.raises((ValueError, tvm.error.InternalError), 
match="compile-time constants"):
+        _cuda_source(lut_runtime)
+
+    @T.prim_func
+    def lut_unrolled(a_ptr: T.handle):
+        A = T.match_buffer(a_ptr, (1,), "uint32")
+        T.device_entry()
+        tx = T.thread_id([32])
+        if tx == 0:
+            d = T.local_scalar("uint32")
+            for i in T.unroll(2):
+                T.ptx.lop3.b32(d, A[0], A[0], A[0], i * 128)
+
+    unrolled_src = _cuda_source(lut_unrolled)
+    assert "lop3.b32 %0, %1, %2, %3, 0;" in unrolled_src
+    assert "lop3.b32 %0, %1, %2, %3, 128;" in unrolled_src
+
 
 def test_ptx_data_movement_dispatch():
     """ISA 9.7.9's newly registered instructions, end to end.
@@ -2128,6 +2148,36 @@ def _as_render_args(rendering):
     return tokens, predicated, dtypes, imms, sinks
 
 
+def _addr_offset_samples(entry):
+    """Small certification axis for address immediates, separate from modifier 
products."""
+    from tvm.backend.cuda.ptx.table import renderings
+
+    enabled = [
+        logical_slot
+        for logical_slot, slot in enumerate(s for s in entry.operands if 
s.kind == "addr")
+        if slot.allow_imm_offset
+    ]
+    if not enabled:
+        return ()
+    representative = _as_render_args(next(iter(renderings(entry))))
+    samples = [
+        (representative, ((logical_slot, offset),))
+        for logical_slot in enabled
+        for offset in (16, -16)
+    ]
+    if len(enabled) > 1:
+        samples.append(
+            (
+                representative,
+                tuple(
+                    (logical_slot, 16 if index % 2 == 0 else -16)
+                    for index, logical_slot in enumerate(enabled)
+                ),
+            )
+        )
+    return tuple(samples)
+
+
 def _sole_instruction(asm_text):
     """The single PTX statement in ``asm_text``, or None if it is not exactly 
one.
 
@@ -2275,6 +2325,10 @@ def test_ptx_all_variants_render_unique():
                     or f"; {opcode};" in source
                 )
             total += not predicated  # a @p twin is not a separate variant
+        for args, addr_offsets in _addr_offset_samples(entry):
+            _, helper, _ = render_variant(entry, *args, 
addr_offsets=addr_offsets)
+            assert helper not in names, f"address-offset helper name 
collision: {helper}"
+            names.add(helper)
     assert total == 200018  # update when the table grows
 
 
@@ -2434,6 +2488,9 @@ def test_ptx_sampled_helpers_assemble():
         for i in rng.sample(range(len(rendered)), min(48, len(rendered))):
             _, _, source = render_variant(entry, *_as_render_args(rendered[i]))
             by_arch.setdefault(arch, 
[]).append(source.replace("__forceinline__ ", ""))
+        for args, addr_offsets in _addr_offset_samples(entry):
+            _, _, source = render_variant(entry, *args, 
addr_offsets=addr_offsets)
+            by_arch.setdefault(arch, 
[]).append(source.replace("__forceinline__ ", ""))
     for arch, sources in by_arch.items():
         _assert_ptxas_ok("\n".join([_CERT_PRELUDE, *sources]), rdc=True, 
arch=arch)
 
@@ -2473,13 +2530,21 @@ def test_ptx_all_helpers_certify(shard):
 
     by_arch = {}
     covered = 0
-    for index, (entry, rendering) in enumerate(
-        (TABLE[name], r) for name in sorted(TABLE) for r in 
renderings(TABLE[name])
-    ):
+    baseline = (
+        (TABLE[name], _as_render_args(rendering), ())
+        for name in sorted(TABLE)
+        for rendering in renderings(TABLE[name])
+    )
+    address_samples = (
+        (TABLE[name], args, addr_offsets)
+        for name in sorted(TABLE)
+        for args, addr_offsets in _addr_offset_samples(TABLE[name])
+    )
+    for index, (entry, args, addr_offsets) in 
enumerate(itertools.chain(baseline, address_samples)):
         if index % _CERT_SHARDS == shard:
             covered += 1
             arch = entry.cert_arch or PTX_ARCH
-            _, _, src = render_variant(entry, *_as_render_args(rendering))
+            _, _, src = render_variant(entry, *args, addr_offsets=addr_offsets)
             by_arch.setdefault(arch, []).append(src.replace("__forceinline__ 
", ""))
     assert covered, "empty shard: lower _CERT_SHARDS"
     for arch, sources in by_arch.items():
diff --git a/tests/python/tirx/codegen/test_ptx_ld_st_ops.py 
b/tests/python/tirx/codegen/test_ptx_ld_st_ops.py
index 88d8fc818e..53bbdec724 100644
--- a/tests/python/tirx/codegen/test_ptx_ld_st_ops.py
+++ b/tests/python/tirx/codegen/test_ptx_ld_st_ops.py
@@ -162,6 +162,25 @@ def test_ptx_ld_st_raw_shared_address_codegen():
     assert '"q"(__value)' in src
 
 
+def test_ptx_ld_st_immediate_offset_codegen():
+    """An immediate displacement must stay inside the PTX memory operand."""
+
+    @T.prim_func
+    def main(src: T.Buffer((4,), "uint64"), out: T.Buffer((4,), "uint64")):
+        T.device_entry()
+        tx = T.thread_id([32])
+        values = T.alloc_local((2,), "uint64")
+        if tx == 0:
+            T.ptx.ld.global_.v2.b64(values[0], values[1], T.ptx.addr(src.data, 
16))
+            T.ptx.st.global_.v2.b64(T.ptx.addr(out.data, 16), values[0], 
values[1])
+
+    with TARGET:
+        mod = tvm.compile(tvm.IRModule({"main": main}), target=TARGET, 
tir_pipeline="tirx")
+    src = mod.mod.imports[0].inspect_source("cuda")
+    assert "ld.global.v2.b64 {%0, %1}, [%2+16];" in src
+    assert "st.global.v2.b64 [%0+16], {%1, %2};" in src
+
+
 def test_ptx_ld_global_nc_v8_codegen():
     """FlashMLA index loads need ``ld.global.nc`` with a 256B prefetch."""
 
diff --git a/tests/python/tirx/test_tirx_kernels_registry_correctness.py 
b/tests/python/tirx/test_tirx_kernels_registry_correctness.py
index 5485477188..43b8a71b98 100644
--- a/tests/python/tirx/test_tirx_kernels_registry_correctness.py
+++ b/tests/python/tirx/test_tirx_kernels_registry_correctness.py
@@ -51,7 +51,14 @@ _KERNELS = {
     for kernel_name in sorted({workload["kernel"] for workload in _WORKLOADS})
 }
 _DISTRIBUTED_KERNELS = frozenset(
-    {"allgather_gemm", "deepgemm_fp8_fp4_mega_moe", "gemm_reduce_scatter"}
+    # Both MegaMoE names are listed so the test works across tirx-kernels
+    # checkouts from before and after the sm100_fp8_fp4_mega_moe rename.
+    {
+        "allgather_gemm",
+        "deepgemm_fp8_fp4_mega_moe",
+        "gemm_reduce_scatter",
+        "sm100_fp8_fp4_mega_moe",
+    }
 )
 _MEGA_MOE_KERNELS = frozenset({"deepgemm_fp8_fp4_mega_moe", 
"sm100_fp8_fp4_mega_moe"})
 _XDIST_CUDA_DEVICE = None

Reply via email to