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 dbb1157bc8 [FIX][IR] Complete lazy subscript realization (#20251)
dbb1157bc8 is described below

commit dbb1157bc8802a0069b3a99dc405ab17a01bfa91
Author: Hongyi Jin <[email protected]>
AuthorDate: Tue Sep 1 07:44:52 2026 -0400

    [FIX][IR] Complete lazy subscript realization (#20251)
    
    ## Motivation
    
    Since #20246, expression subscription returns a lazy `SubscriptProxy`
    until a consumer realizes the expression. Most parser expression
    boundaries already honor that contract, but PTX predicate operand
    coercion bypasses the generic realization path. As a result, code such
    as `setp(..., buffer[0], ...)` dispatches the proxy itself and rejects
    it as an invalid operand.
    
    The delayed realization also means source metadata must travel with the
    proxy. Without that, realizing `A[0]` creates a `TensorLoad` without the
    exact subscription span, so downstream diagnostics such as TIRx
    racecheck and GDN cannot attribute the access to its source location.
    
    ## Summary
    
    - realize lazy subscript operands used directly by PTX predicate inputs
    and destinations
    - preserve parser source spans through `SubscriptProxy` realization
    - propagate the span through tuple, Relax, and TIRx subscription
    callbacks
    
    This completes the lazy-subscript contract introduced by #20246 and
    fixes downstream regressions exposed by the TIRx migration in #20247.
    
    ## Testing
    
    - `cmake --build build -j 16`
    - `python -m pytest -q
    
tests/python/tvmscript/test_tvmscript_parser_source.py::test_parser_attaches_span_to_nested_tensor_load
    
tests/python/tvmscript/test_tvmscript_parser_source.py::test_parser_attaches_span_to_direct_call
    
tests/python/tirx/codegen/test_ptx_dialect.py::test_ptx_comparison_selection_dispatch`
    - downstream wiki-kernel integration: 5 passed
    - downstream full `tirx_tools` gate: 2933 passed
---
 python/tvm/backend/cuda/ptx/engine.py              |  4 ++--
 python/tvm/ir/expr.py                              | 21 ++++++++++++++++++---
 python/tvm/script/parser/core/evaluator.py         | 17 ++++++++++-------
 python/tvm/script/parser/core/parser.py            |  8 ++++++--
 src/ir/subscript_proxy.cc                          |  9 +++++----
 src/relax/ir/dependent_type.cc                     |  5 +++--
 src/tirx/ir/buffer.cc                              |  5 +++--
 tests/python/tirx/codegen/test_ptx_dialect.py      |  5 +++--
 .../tvmscript/test_tvmscript_parser_source.py      | 22 +++++++++++++++++++++-
 9 files changed, 71 insertions(+), 25 deletions(-)

diff --git a/python/tvm/backend/cuda/ptx/engine.py 
b/python/tvm/backend/cuda/ptx/engine.py
index 2529065127..5ffdecc785 100644
--- a/python/tvm/backend/cuda/ptx/engine.py
+++ b/python/tvm/backend/cuda/ptx/engine.py
@@ -444,7 +444,7 @@ def _coerce_pred_operand(entry, slot, values):
     like any other, and no syntax line offers a non-predicate alternative at
     the same position.
     """
-    (value,) = values
+    (value,) = [_realize_operand(value) for value in values]
     if slot.rw != "r":
         # The 0/1 materialization of a .pred result: a "=r" uint32 the caller
         # receives through a reference parameter, so it needs a writable
@@ -457,7 +457,7 @@ def _coerce_pred_operand(entry, slot, values):
             )
         return values
     if isinstance(value, PredArg):
-        value = getattr(value.value, "scalar", value.value)
+        value = _realize_operand(getattr(value.value, "scalar", value.value))
         if isinstance(value, bool | int):
             return [const(int(value), "uint32")]
         ty = getattr(value, "ty", None)
diff --git a/python/tvm/ir/expr.py b/python/tvm/ir/expr.py
index 3cbd19c356..5005737e26 100644
--- a/python/tvm/ir/expr.py
+++ b/python/tvm/ir/expr.py
@@ -39,7 +39,9 @@ class Expr(Node):
             # Tuple subscription is eager so Python's legacy sequence protocol
             # observes IndexError and terminates tuple iteration/unpacking.
             try:
-                return _ffi_api.SubscriptExprRealize(self, 
[SubscriptProxy._convert_index(index)])
+                return _ffi_api.SubscriptExprRealize(
+                    self, [SubscriptProxy._convert_index(index)], None
+                )
             except RuntimeError as err:
                 if "Index out of bounds" in err.args[0]:
                     raise IndexError from err
@@ -444,7 +446,7 @@ class SubscriptProxy(_ExprCallable, ExprOperand, 
ObjectConvertible):
     containing a slice must be realized before applying a region subscript.
     """
 
-    __slots__ = ("_result", "_slice", "_source")
+    __slots__ = ("_result", "_slice", "_source", "_span")
     __hash__ = object.__hash__
 
     def __init__(self, source: Expr, index):
@@ -453,12 +455,23 @@ class SubscriptProxy(_ExprCallable, ExprOperand, 
ObjectConvertible):
                 raise TypeError("Cannot chain a subscription after a slice")
             self._source = source._source
             self._slice = source._slice + self._flatten(index)
+            self._span = source._span
         else:
             _ffi_api.SubscriptExprCheck(source)
             self._source = source
             self._slice = self._flatten(index)
+            self._span = None
         self._result = None
 
+    def with_span(self, span: Span) -> "SubscriptProxy":
+        """Return an unrealized proxy carrying its frontend source span."""
+        result = object.__new__(SubscriptProxy)
+        result._source = self._source
+        result._slice = self._slice
+        result._span = span
+        result._result = None
+        return result
+
     @staticmethod
     def _flatten(index):
         return tuple(index) if isinstance(index, tuple | list) else (index,)
@@ -487,7 +500,9 @@ class SubscriptProxy(_ExprCallable, ExprOperand, 
ObjectConvertible):
         """Realize and cache the subscribed IR object."""
         if self._result is None:
             result = _ffi_api.SubscriptExprRealize(
-                self._source, [self._convert_index(index) for index in 
self._slice]
+                self._source,
+                [self._convert_index(index) for index in self._slice],
+                self._span,
             )
             if not isinstance(result, Object):
                 raise TypeError("__subscript_expr_realize__ must return an 
Object")
diff --git a/python/tvm/script/parser/core/evaluator.py 
b/python/tvm/script/parser/core/evaluator.py
index 06f6dc9b09..e22abf4b9a 100644
--- a/python/tvm/script/parser/core/evaluator.py
+++ b/python/tvm/script/parser/core/evaluator.py
@@ -131,7 +131,7 @@ class ExprEvaluator:
             return result.value
         raise TypeError(f"Unexpected result type: {type(result)}")
 
-    def _add_intermediate_result(self, value: Any) -> doc.Name:
+    def _add_intermediate_result(self, value: Any, node: doc.AST) -> doc.Name:
         """Add intermediate result during evaluation into value table.
 
         Parameters
@@ -139,13 +139,16 @@ class ExprEvaluator:
         value : Any
             The intermediate result.
 
+        node : doc.AST
+            The AST node that produced the intermediate result.
+
         Returns
         -------
         name : doc.Name
             The doc AST name node with intermediate name for intermediate 
result.
         """
         if self.parser is not None:
-            value = self.parser.annotate_current_source_span(value)
+            value = self.parser.annotate_current_source_span(value, node)
         name = f"__tvm_tmp_value_{self.new_value_count}"
         self.new_value_count += 1
         self.value_table[name] = value
@@ -193,13 +196,13 @@ class ExprEvaluator:
                 value = self._eval_bool_op(node)
             except Exception as err:  # pylint: disable=broad-except
                 self.parser.report_error(node, err)
-            return self._add_intermediate_result(value)
+            return self._add_intermediate_result(value, node)
         if isinstance(node, doc.IfExp):
             try:
                 value = self._eval_if_exp(node)
             except Exception as err:  # pylint: disable=broad-except
                 self.parser.report_error(node, err)
-            return self._add_intermediate_result(value)
+            return self._add_intermediate_result(value, node)
 
         args = []
         if (
@@ -262,7 +265,7 @@ class ExprEvaluator:
 
         if isinstance(node, doc.ListComp | doc.SetComp | doc.DictComp):
             value = self._eval_expr(node)
-            return self._add_intermediate_result(value)
+            return self._add_intermediate_result(value, node)
 
         fields = {}
         for field in node.__class__._FIELDS:  # pylint: 
disable=protected-access
@@ -284,7 +287,7 @@ class ExprEvaluator:
                 value = self._eval_expr(node.__class__(**fields))
         except Exception as err:  # pylint: disable=broad-except
             self.parser.report_error(node, err)
-        return self._add_intermediate_result(value)
+        return self._add_intermediate_result(value, node)
 
     def _eval_lambda(self, node: doc.Lambda) -> Any:
         """The doc AST lambda node evaluating method.
@@ -303,7 +306,7 @@ class ExprEvaluator:
             value = self._eval_expr(node)
         except Exception as err:  # pylint: disable=broad-except
             self.parser.report_error(node, err)
-        return self._add_intermediate_result(value)
+        return self._add_intermediate_result(value, node)
 
     def _eval_bool_op(self, node: doc.BoolOp) -> Any:
         """The doc AST boolean operator node evaluating method.
diff --git a/python/tvm/script/parser/core/parser.py 
b/python/tvm/script/parser/core/parser.py
index 7d0f2887df..94c74f24a1 100644
--- a/python/tvm/script/parser/core/parser.py
+++ b/python/tvm/script/parser/core/parser.py
@@ -596,8 +596,12 @@ class Parser(doc.NodeVisitor):
         with 
IRBuilder.current().with_source_span(self.diag.source.to_span(node)):
             yield
 
-    def annotate_current_source_span(self, value: Any) -> Any:
+    def annotate_current_source_span(self, value: Any, node: doc.AST | None = 
None) -> Any:
         """Attach the active parser span to an expression result, when 
applicable."""
+        from tvm.ir.expr import SubscriptProxy  # pylint: 
disable=import-outside-toplevel
+
+        if isinstance(value, SubscriptProxy) and node is not None:
+            return value.with_span(self.diag.source.to_span(node))
         if isinstance(value, Object) and IRBuilder.is_in_scope():
             return IRBuilder.current()._set_current_source_span(value)  # 
pylint: disable=protected-access
         return value
@@ -634,7 +638,7 @@ class Parser(doc.NodeVisitor):
         # normalize once at the parser boundary before statement dispatch.
         from tvm.ir.expr import _realize_operand  # pylint: 
disable=import-outside-toplevel
 
-        return _realize_operand(value)
+        return self.annotate_current_source_span(_realize_operand(value), node)
 
     def _duplicate_lhs_check(self, target: doc.expr) -> bool | set[str]:
         """Check whether duplicate lhs exists in assignment.
diff --git a/src/ir/subscript_proxy.cc b/src/ir/subscript_proxy.cc
index a92c961c61..c6e8e75109 100644
--- a/src/ir/subscript_proxy.cc
+++ b/src/ir/subscript_proxy.cc
@@ -36,7 +36,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   refl::EnsureTypeAttrColumn("__subscript_expr_realize__");
   refl::TypeAttrDef<TupleTypeNode>().def(
-      "__subscript_expr_realize__", [](Expr value, SubscriptSlice slice) -> 
ffi::ObjectRef {
+      "__subscript_expr_realize__",
+      [](Expr value, SubscriptSlice slice, Span span) -> ffi::ObjectRef {
         TVM_FFI_CHECK_EQ(slice.size(), 1, IndexError)
             << "A tuple expression requires exactly one index";
         auto index = slice[0].as<PrimExpr>();
@@ -44,7 +45,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
         const auto* imm = index.value().as<IntImmNode>();
         TVM_FFI_CHECK(imm != nullptr, TypeError)
             << "A tuple expression requires a constant integer index";
-        return TupleGetItem(value, static_cast<int>(imm->value));
+        return TupleGetItem(value, static_cast<int>(imm->value), span);
       });
   refl::GlobalDef().def("ir.SubscriptExprCheck", [](Expr value) {
     TVM_FFI_CHECK(value.defined(), TypeError) << "Cannot subscript an 
undefined expression";
@@ -53,14 +54,14 @@ TVM_FFI_STATIC_INIT_BLOCK() {
         << "Type " << value->ty->GetTypeKey() << " does not support subscript";
   });
   refl::GlobalDef().def(
-      "ir.SubscriptExprRealize", [](Expr value, SubscriptSlice slice) -> 
ffi::ObjectRef {
+      "ir.SubscriptExprRealize", [](Expr value, SubscriptSlice slice, Span 
span) -> ffi::ObjectRef {
         TVM_FFI_CHECK(value.defined(), TypeError) << "Cannot subscript an 
undefined expression";
         static refl::TypeAttrColumn 
realize_column("__subscript_expr_realize__");
         ffi::AnyView packed_realize = realize_column[value->ty->type_index()];
         TVM_FFI_CHECK(packed_realize != nullptr, TypeError)
             << "Type " << value->ty->GetTypeKey() << " does not support 
subscript";
         ffi::ObjectRef result =
-            packed_realize.cast<ffi::Function>()(value, 
slice).cast<ffi::ObjectRef>();
+            packed_realize.cast<ffi::Function>()(value, slice, 
span).cast<ffi::ObjectRef>();
         TVM_FFI_CHECK(result.defined(), TypeError)
             << "__subscript_expr_realize__ for type " << 
value->ty->GetTypeKey()
             << " returned an undefined object";
diff --git a/src/relax/ir/dependent_type.cc b/src/relax/ir/dependent_type.cc
index 2ac4898551..ff0bce1fb6 100644
--- a/src/relax/ir/dependent_type.cc
+++ b/src/relax/ir/dependent_type.cc
@@ -42,7 +42,8 @@ TVM_FFI_STATIC_INIT_BLOCK() {
          ffi::Array<ffi::Variant<
              ffi::Tuple<ffi::Optional<PrimExpr>, ffi::Optional<PrimExpr>, 
ffi::Optional<PrimExpr>>,
              PrimExpr>>
-             slice) -> ffi::ObjectRef {
+             slice,
+         Span span) -> ffi::ObjectRef {
         TVM_FFI_CHECK_EQ(slice.size(), 1, IndexError)
             << "A Relax expression requires exactly one index";
         auto index = slice[0].as<PrimExpr>();
@@ -50,7 +51,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
         const auto* imm = index.value().as<IntImmNode>();
         TVM_FFI_CHECK(imm != nullptr, TypeError)
             << "A Relax expression requires a constant integer index";
-        return TupleGetItem(value, static_cast<int>(imm->value));
+        return TupleGetItem(value, static_cast<int>(imm->value), span);
       });
 }
 
diff --git a/src/tirx/ir/buffer.cc b/src/tirx/ir/buffer.cc
index c715aedf0c..8c529e2a05 100644
--- a/src/tirx/ir/buffer.cc
+++ b/src/tirx/ir/buffer.cc
@@ -47,7 +47,8 @@ ffi::ObjectRef RealizeBufferSubscript(
     ffi::Array<ffi::Variant<
         ffi::Tuple<ffi::Optional<PrimExpr>, ffi::Optional<PrimExpr>, 
ffi::Optional<PrimExpr>>,
         PrimExpr>>
-        slice) {
+        slice,
+    Span span) {
   BufferVar buffer = value.as_or_throw<BufferVar>();
   BufferType buffer_ty = buffer.type();
   TVM_FFI_CHECK_LE(slice.size(), buffer_ty->shape.size(), IndexError)
@@ -70,7 +71,7 @@ ffi::ObjectRef RealizeBufferSubscript(
     for (const auto& item : slice) {
       indices.push_back(item.as<PrimExpr>().value());
     }
-    return BufferLoad(buffer, indices);
+    return BufferLoad(buffer, indices, span);
   }
 
   // Any slice or omitted trailing dimension denotes a region.  Rejecting
diff --git a/tests/python/tirx/codegen/test_ptx_dialect.py 
b/tests/python/tirx/codegen/test_ptx_dialect.py
index 010fb9ed92..743f5022e0 100644
--- a/tests/python/tirx/codegen/test_ptx_dialect.py
+++ b/tests/python/tirx/codegen/test_ptx_dialect.py
@@ -1356,12 +1356,13 @@ def test_ptx_comparison_selection_dispatch():
         q = T.local_scalar("uint32")
         d = T.local_scalar("uint32")
         f = T.local_scalar("float32")
-        T.ptx.setp.lt.s32(p, A[0], A[1])  # one destination
+        p_buffer = T.alloc_local((1,), "uint32")
+        T.ptx.setp.lt.s32(p_buffer[0], A[0], A[1])  # one destination
         T.ptx.setp.lt.s32(p, q, A[0], A[1])  # ... two: `p|q`, chosen by arity
         T.ptx.setp.lt.and_.s32(p, A[0], A[1], T.ptx.pred(q))  # ... plus a 
BoolOp
         T.ptx.setp.gt.or_.s32(p, q, A[2], A[3], T.ptx.pred(d))  # ... and both
         T.ptx.set.lt.u32.f32(d, f, f)  # writes a value, not a predicate
-        T.ptx.selp.b32(d, d, p, T.ptx.pred(q))  # predicate selects
+        T.ptx.selp.b32(d, d, p, T.ptx.pred(p_buffer[0]))  # predicate selects
         T.ptx.slct.ftz.b32.f32(d, d, p, f)  # a sign selects
         # slct treats d/a/b independently as bit-size values. This mixes all
         # three 32-bit carrier classes while c remains exactly .s32.
diff --git a/tests/python/tvmscript/test_tvmscript_parser_source.py 
b/tests/python/tvmscript/test_tvmscript_parser_source.py
index da6fff1a87..31c7581d9c 100644
--- a/tests/python/tvmscript/test_tvmscript_parser_source.py
+++ b/tests/python/tvmscript/test_tvmscript_parser_source.py
@@ -24,7 +24,7 @@ import tvm_ffi
 
 import tvm
 import tvm.testing
-from tvm.ir import Call, SequentialSpan, assert_structural_equal
+from tvm.ir import Call, SequentialSpan, TensorLoad, assert_structural_equal
 from tvm.script import tirx as T
 from tvm.script.parser.core import doc_core as doc
 from tvm.script.parser.core.diagnostics import Source
@@ -156,6 +156,26 @@ def test_parser_attaches_span_to_direct_call():
     assert _span_range(call.span) == _span_range(source.to_span(call_ast))
 
 
+def test_parser_attaches_span_to_nested_tensor_load():
+    @_tirx_source
+    def nested_load():
+        source_buffer = T.alloc_buffer((1,), "int32")
+        output = T.alloc_buffer((1,), "int32")
+        output[0] = source_buffer[0] + 1
+
+    source = Source(nested_load)
+    load_ast = source.as_ast().body[0].body[-1].value.left
+    func = T.prim_func(nested_load)
+    load = _find_ir_node(
+        func,
+        lambda node: (
+            isinstance(node, TensorLoad) and getattr(node.source, "name", 
None) == "source_buffer"
+        ),
+    )
+
+    assert _span_range(load.span) == _span_range(source.to_span(load_ast))
+
+
 def test_parser_retains_inline_call_site_and_definition_spans():
     def wait_impl(barrier):
         T.cuda.mbarrier_wait(barrier, 0)

Reply via email to