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

tlopex 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 6cd73cd1bf [Relax] Legalize shape_to_tensor to device kernel (#19957)
6cd73cd1bf is described below

commit 6cd73cd1bf1c4b08a07d44dbadd95fdb086e3b53
Author: Guan-Ming Chiu <[email protected]>
AuthorDate: Sun Jul 12 10:39:09 2026 +0800

    [Relax] Legalize shape_to_tensor to device kernel (#19957)
    
    ## Why
    
    Fixes #19925. `relax.shape_to_tensor` had no legalization, so it always
    lowered to the host packed func `relax.run.shape_to_tensor`, producing a
    CPU tensor regardless of the target device.
    
    ## How
    
    - Register a legalization that emits the shape values as a `call_tir` TE
    kernel, passing symbolic dims via `tir_vars`.
    - Fall back to the packed func when the shape values are unknown
    (`ShapeStructInfo` without values).
---
 python/tvm/relax/transform/legalize_ops/create.py  |  27 ++++-
 src/relax/transform/fold_constant.cc               |   3 +
 tests/python/relax/test_transform_fold_constant.py |  25 +++++
 .../test_transform_legalize_ops_create_datatype.py | 119 ++++++++++++++++++++-
 4 files changed, 172 insertions(+), 2 deletions(-)

diff --git a/python/tvm/relax/transform/legalize_ops/create.py 
b/python/tvm/relax/transform/legalize_ops/create.py
index 00383f8326..f4514e496e 100644
--- a/python/tvm/relax/transform/legalize_ops/create.py
+++ b/python/tvm/relax/transform/legalize_ops/create.py
@@ -21,7 +21,7 @@
 import numpy as np
 
 import tvm
-from tvm import tirx, topi
+from tvm import te, tirx, topi
 from tvm.ir import Call
 
 from ...block_builder import BlockBuilder
@@ -130,6 +130,31 @@ def _arange(bb: BlockBuilder, call: Call) -> Expr:
         return bb.call_te(topi.arange, start, end, step, dtype)
 
 
+@register_legalize("relax.shape_to_tensor")
+def _shape_to_tensor(bb: BlockBuilder, call: Call) -> Expr:
+    shape = call.args[0]
+    values = shape.values if isinstance(shape, ShapeExpr) else shape.ty.values
+    if values is None:
+        return call
+    values = list(values)
+    n = len(values)
+    symbolic = [v for v in values if not isinstance(v, tirx.IntImm)]
+
+    def te_shape_to_tensor(*sym):
+        sym = list(sym)
+        resolved = [v if isinstance(v, tirx.IntImm) else sym.pop(0) for v in 
values]
+
+        def fcompute(i):
+            result = tirx.const(0, "int64")
+            for idx in range(n - 1, -1, -1):
+                result = tirx.if_then_else(i == idx, tirx.Cast("int64", 
resolved[idx]), result)
+            return result
+
+        return te.compute((n,), fcompute, name="shape_to_tensor")
+
+    return bb.call_te(te_shape_to_tensor, *symbolic, 
primfunc_name_hint="shape_to_tensor")
+
+
 @register_legalize("relax.hamming_window")
 def _hamming_window(bb: BlockBuilder, call: Call) -> Expr:
     assert len(call.args) == 4
diff --git a/src/relax/transform/fold_constant.cc 
b/src/relax/transform/fold_constant.cc
index 274ad7b0a5..3930cf33a3 100644
--- a/src/relax/transform/fold_constant.cc
+++ b/src/relax/transform/fold_constant.cc
@@ -280,6 +280,9 @@ class ConstantFolder : public ExprMutator {
 
     if (!func || !arr_args) return {};
 
+    // tir_vars are passed as extra scalar arguments to the PrimFunc, which we 
cannot supply here.
+    if (call->args.size() > 2) return {};
+
     // Handle tuple output: ty_args[0] is a TupleType.
     if (const auto* tuple_ty = call->ty_args[0].as<TupleTypeNode>()) {
       return ConstEvaluateCallTIRTuple(func.value(), arr_args.value(), 
tuple_ty);
diff --git a/tests/python/relax/test_transform_fold_constant.py 
b/tests/python/relax/test_transform_fold_constant.py
index 0867a9d630..f6720131c0 100644
--- a/tests/python/relax/test_transform_fold_constant.py
+++ b/tests/python/relax/test_transform_fold_constant.py
@@ -585,5 +585,30 @@ def test_fold_large_op_with_tensor_input():
     tvm.ir.assert_structural_equal(after, expected)
 
 
+def test_call_tir_with_tir_vars_not_folded():
+    """call_tir with symbolic tir_vars cannot be const-evaluated."""
+
+    @tvm.script.ir_module
+    class Module:
+        @T.prim_func(private=True, s_tir=True)
+        def shape_to_tensor(out: T.Buffer((T.int64(1),), "int64"), m: T.int64):
+            for i in range(T.int64(1)):
+                with T.sblock("out"):
+                    vi = T.axis.remap("S", [i])
+                    out[vi] = m
+
+        @R.function
+        def main(x: R.Tensor(("m",), "float32")):
+            m = T.int64()
+            cls = Module
+            gv = relax.call_tir(
+                cls.shape_to_tensor, R.tuple(), R.Tensor((1,), "int64"), 
tir_vars=R.shape([m])
+            )
+            return gv
+
+    after = relax.transform.FoldConstant()(Module)
+    tvm.ir.assert_structural_equal(after, Module)
+
+
 if __name__ == "__main__":
     tvm.testing.main()
diff --git a/tests/python/relax/test_transform_legalize_ops_create_datatype.py 
b/tests/python/relax/test_transform_legalize_ops_create_datatype.py
index dda27def44..255da3671d 100644
--- a/tests/python/relax/test_transform_legalize_ops_create_datatype.py
+++ b/tests/python/relax/test_transform_legalize_ops_create_datatype.py
@@ -14,7 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-# ruff: noqa: E501
+# ruff: noqa: E501, F841
 
 import tvm
 import tvm.testing
@@ -617,6 +617,123 @@ def test_arange_symbolic():
     tvm.ir.assert_structural_equal(mod, Expected)
 
 
+def test_shape_to_tensor():
+    # fmt: off
+    @tvm.script.ir_module
+    class ShapeToTensor:
+        @R.function
+        def main(x: R.Tensor((2, 3, 4), "float32")):
+            gv = R.shape_to_tensor(R.shape_of(x))
+            return gv
+
+    @tvm.script.ir_module
+    class Expected:
+        @R.function
+        def main(x: R.Tensor((2, 3, 4), "float32")) -> R.Tensor((3,), "int64"):
+            cls = Expected
+            gv: R.Shape([2, 3, 4]) = R.shape_of(x)
+            gv_1 = R.call_tir(cls.shape_to_tensor, R.tuple(), 
out_ty=R.Tensor((3,), dtype="int64"))
+            return gv_1
+
+        @T.prim_func(private=True, s_tir=True)
+        def shape_to_tensor(shape_to_tensor: T.Buffer((T.int64(3),), "int64")):
+            T.func_attr({"tirx.noalias": True})
+            for i in range(T.int64(3)):
+                with T.sblock("shape_to_tensor"):
+                    v_i = T.axis.spatial(T.int64(3), i)
+                    shape_to_tensor[v_i] = T.if_then_else(v_i == T.int64(0), 
T.int64(2), T.if_then_else(v_i == T.int64(1), T.int64(3), T.if_then_else(v_i == 
T.int64(2), T.int64(4), T.int64(0))))
+    # fmt: on
+
+    mod = LegalizeOps()(ShapeToTensor)
+    tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def test_shape_to_tensor_symbolic():
+    # fmt: off
+    @tvm.script.ir_module
+    class ShapeToTensor:
+        @R.function
+        def main(x: R.Tensor(("m", "n"), "float32")):
+            gv = R.shape_to_tensor(R.shape_of(x))
+            return gv
+
+    @tvm.script.ir_module
+    class Expected:
+        @R.function
+        def main(x: R.Tensor(("m", "n"), "float32")) -> R.Tensor((2,), 
"int64"):
+            m = T.int64()
+            n = T.int64()
+            cls = Expected
+            gv: R.Shape([m, n]) = R.shape_of(x)
+            gv_1 = R.call_tir(cls.shape_to_tensor, R.tuple(), 
out_ty=R.Tensor((2,), dtype="int64"), tir_vars=R.shape([m, n]))
+            return gv_1
+
+        @T.prim_func(private=True, s_tir=True)
+        def shape_to_tensor(shape_to_tensor: T.Buffer((T.int64(2),), "int64"), 
m: T.int64, n: T.int64):
+            T.func_attr({"tirx.noalias": True})
+            for i in range(T.int64(2)):
+                with T.sblock("shape_to_tensor"):
+                    v_i = T.axis.spatial(T.int64(2), i)
+                    shape_to_tensor[v_i] = T.if_then_else(v_i == T.int64(0), 
m, T.if_then_else(v_i == T.int64(1), n, T.int64(0)))
+    # fmt: on
+
+    mod = LegalizeOps()(ShapeToTensor)
+    tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def test_shape_to_tensor_mixed():
+    # fmt: off
+    @tvm.script.ir_module
+    class ShapeToTensor:
+        @R.function
+        def main(x: R.Tensor(("m", 3), "float32")):
+            gv = R.shape_to_tensor(R.shape_of(x))
+            return gv
+
+    @tvm.script.ir_module
+    class Expected:
+        @R.function
+        def main(x: R.Tensor(("m", 3), "float32")) -> R.Tensor((2,), "int64"):
+            m = T.int64()
+            cls = Expected
+            gv: R.Shape([m, 3]) = R.shape_of(x)
+            gv_1 = R.call_tir(cls.shape_to_tensor, R.tuple(), 
out_ty=R.Tensor((2,), dtype="int64"), tir_vars=R.shape([m]))
+            return gv_1
+
+        @T.prim_func(private=True, s_tir=True)
+        def shape_to_tensor(shape_to_tensor: T.Buffer((T.int64(2),), "int64"), 
m: T.int64):
+            T.func_attr({"tirx.noalias": True})
+            for i in range(T.int64(2)):
+                with T.sblock("shape_to_tensor"):
+                    v_i = T.axis.spatial(T.int64(2), i)
+                    shape_to_tensor[v_i] = T.if_then_else(v_i == T.int64(0), 
m, T.if_then_else(v_i == T.int64(1), T.int64(3), T.int64(0)))
+    # fmt: on
+
+    mod = LegalizeOps()(ShapeToTensor)
+    tvm.ir.assert_structural_equal(mod, Expected)
+
+
+def test_shape_to_tensor_unknown_values():
+    @tvm.script.ir_module
+    class ShapeToTensor:
+        @R.function
+        def main(s: R.Shape(ndim=2)):
+            gv = R.shape_to_tensor(s)
+            return gv
+
+    @tvm.script.ir_module
+    class Expected:
+        @R.function
+        def main(s: R.Shape(ndim=2)) -> R.Tensor((2,), "int64"):
+            gv: R.Tensor((2,), dtype="int64") = R.call_pure_packed(
+                "relax.run.shape_to_tensor", s, ty_args=(R.Tensor((2,), 
dtype="int64"),)
+            )
+            return gv
+
+    mod = LegalizeOps()(ShapeToTensor)
+    tvm.ir.assert_structural_equal(mod, Expected)
+
+
 def test_tril():
     # fmt: off
     @tvm.script.ir_module

Reply via email to