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 c3acd8c110 [Fix][Metal] Bound symbolic stack allocations (#20236)
c3acd8c110 is described below

commit c3acd8c110bdfdb8ecccf9e8c1cf0fe16e018159
Author: Akaash Parthasarathy <[email protected]>
AuthorDate: Thu Sep 3 12:06:17 2026 -0700

    [Fix][Metal] Bound symbolic stack allocations (#20236)
    
    Use analyzer-proven finite upper bounds when emitting fixed-size Metal
    arrays. Reject unbounded, nonpositive, and overflowing extents while
    allowing shapes such as `min(dynamic_extent, constant_limit)`.
---
 src/backend/metal/codegen/codegen_metal.cc        |  27 +++-
 tests/python/codegen/test_target_codegen_metal.py | 149 ++++++++++++++++++++++
 2 files changed, 171 insertions(+), 5 deletions(-)

diff --git a/src/backend/metal/codegen/codegen_metal.cc 
b/src/backend/metal/codegen/codegen_metal.cc
index 6b07baae96..7c844eb0c0 100644
--- a/src/backend/metal/codegen/codegen_metal.cc
+++ b/src/backend/metal/codegen/codegen_metal.cc
@@ -22,6 +22,7 @@
  */
 #include "codegen_metal.h"
 
+#include <tvm/arith/analyzer.h>
 #include <tvm/ffi/cast.h>
 #include <tvm/ffi/container/array.h>
 #include <tvm/ffi/container/map.h>
@@ -31,6 +32,7 @@
 
 #include <algorithm>
 #include <cmath>
+#include <limits>
 #include <sstream>
 #include <string>
 #include <unordered_map>
@@ -357,14 +359,29 @@ void CodeGenMetal::VisitStmt_(const AllocBufferNode* op) {
   std::string vid = AllocVarID(op->buffer.get());
 
   this->PrintIndent();
-  // Compute constant_size from buffer shape
+  // Compute a compile-time upper bound on the number of buffer elements.
   size_t constant_size = 1;
+  arith::Analyzer analyzer;
   for (const auto& dim : op->buffer->shape) {
-    const IntImmNode* dim_imm = dim.as<IntImmNode>();
-    TVM_FFI_ICHECK(dim_imm) << "Can only handle constant size stack allocation 
for now";
-    constant_size *= dim_imm->value;
+    const auto* dim_imm = dim.as<IntImmNode>();
+    int64_t dim_size = dim_imm ? dim_imm->value : 
analyzer->const_int_bound(dim)->max_value;
+    if (dim_imm == nullptr) {
+      // An integer dtype's intrinsic maximum is not a program-derived 
allocation bound.
+      TVM_FFI_ICHECK(dim_size != arith::ConstIntBound::kPosInf)
+          << "Metal allocation extent requires a finite compile-time upper 
bound, but got " << dim;
+      if (const auto* dtype_max = max_value(dim.ty()).as<IntImmNode>()) {
+        TVM_FFI_ICHECK_LT(dim_size, dtype_max->value)
+            << "Metal allocation extent requires a finite compile-time upper 
bound, but got "
+            << dim;
+      }
+    }
+    TVM_FFI_ICHECK_GT(dim_size, 0)
+        << "Metal allocation extent requires a positive compile-time upper 
bound, but got " << dim;
+    TVM_FFI_ICHECK_LE(static_cast<uint64_t>(dim_size),
+                      std::numeric_limits<size_t>::max() / constant_size)
+        << "Metal allocation element count is too large to represent";
+    constant_size *= static_cast<size_t>(dim_size);
   }
-  TVM_FFI_ICHECK_GT(constant_size, 0) << "Can only handle constant size stack 
allocation for now";
 
   auto scope = op->buffer.scope();
   alloc_storage_scope_[op->buffer.get()] = scope;
diff --git a/tests/python/codegen/test_target_codegen_metal.py 
b/tests/python/codegen/test_target_codegen_metal.py
index f0d9998b4c..150d1c40ce 100644
--- a/tests/python/codegen/test_target_codegen_metal.py
+++ b/tests/python/codegen/test_target_codegen_metal.py
@@ -384,6 +384,155 @@ def test_codegen_simdgroup_buffer_data():
     assert "simdgroup_multiply_accumulate(" in source
 
 
+def _build_metal(mod):
+    build = tvm.get_global_func("target.build.metal")
+    return build(mod, tvm.target.Target("metal"))
+
+
+def test_bounded_symbolic_stack_allocation():
+    @I.ir_module
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main(n: T.int32):
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "main",
+                    "target": T.target("metal"),
+                    "tirx.kernel_launch_params": [],
+                    "tirx.is_global_func": True,
+                }
+            )
+            scratch = T.alloc_buffer((T.min(n, 64), 2), "float32", 
scope="local")
+            T.evaluate(scratch.data)
+
+    source = _build_metal(Module).inspect_source()
+    assert "thread float scratch[128]" in source
+
+
+def test_bounded_uint64_symbolic_stack_allocation():
+    @I.ir_module
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main(n: T.uint64):
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "main",
+                    "target": T.target("metal"),
+                    "tirx.kernel_launch_params": [],
+                    "tirx.is_global_func": True,
+                }
+            )
+            scratch = T.alloc_buffer((T.min(n, T.uint64(64)),), "float32", 
scope="local")
+            T.evaluate(scratch.data)
+
+    source = _build_metal(Module).inspect_source()
+    assert "thread float scratch[64]" in source
+
+
+def test_unbounded_symbolic_stack_allocation_rejected():
+    @I.ir_module
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main(n: T.int32):
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "main",
+                    "target": T.target("metal"),
+                    "tirx.kernel_launch_params": [],
+                    "tirx.is_global_func": True,
+                }
+            )
+            scratch = T.alloc_buffer((n,), "float32", scope="local")
+            scratch[0] = 1.0
+            T.evaluate(scratch[0])
+
+    with pytest.raises(
+        tvm.error.InternalError,
+        match="Metal allocation extent requires a finite compile-time upper 
bound",
+    ):
+        _build_metal(Module)
+
+
+def test_unbounded_uint64_symbolic_stack_allocation_rejected():
+    @I.ir_module
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main(n: T.uint64):
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "main",
+                    "target": T.target("metal"),
+                    "tirx.kernel_launch_params": [],
+                    "tirx.is_global_func": True,
+                }
+            )
+            scratch = T.alloc_buffer((n,), "float32", scope="local")
+            scratch[0] = 1.0
+            T.evaluate(scratch[0])
+
+    with pytest.raises(
+        tvm.error.InternalError,
+        match="Metal allocation extent requires a finite compile-time upper 
bound",
+    ):
+        _build_metal(Module)
+
+
[email protected]("extent", [0, -1])
+def test_nonpositive_stack_allocation_rejected(extent):
+    @I.ir_module
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main():
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "main",
+                    "target": T.target("metal"),
+                    "tirx.kernel_launch_params": [],
+                    "tirx.is_global_func": True,
+                }
+            )
+            scratch = T.alloc_buffer((extent,), "float32", scope="local")
+            T.evaluate(scratch.data)
+
+    with pytest.raises(
+        tvm.error.InternalError,
+        match="Metal allocation extent requires a positive compile-time upper 
bound",
+    ):
+        _build_metal(Module)
+
+
+def test_stack_allocation_element_count_overflow_rejected():
+    @I.ir_module
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main(n: T.int32, m: T.int32, k: T.int32):
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "main",
+                    "target": T.target("metal"),
+                    "tirx.kernel_launch_params": [],
+                    "tirx.is_global_func": True,
+                }
+            )
+            scratch = T.alloc_buffer(
+                (T.min(n, 1 << 30), T.min(m, 1 << 30), T.min(k, 1 << 30)),
+                "uint8",
+                scope="local",
+            )
+            T.evaluate(scratch.data)
+
+    with pytest.raises(
+        tvm.error.InternalError, match="Metal allocation element count is too 
large to represent"
+    ):
+        _build_metal(Module)
+
+
 def test_codegen_pointer_byte_offsets_preserve_storage_scope():
     """Pointer byte offsets should preserve the source Metal address space."""
 

Reply via email to