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 28f4bc47c9 [BugFix][Metal] Preserve pointer address spaces for byte 
offsets (#20101)
28f4bc47c9 is described below

commit 28f4bc47c9152cd4542deca4d95ba0b1a46706ec
Author: Gengyuan Bai <[email protected]>
AuthorDate: Sun Aug 9 04:13:02 2026 -0400

    [BugFix][Metal] Preserve pointer address spaces for byte offsets (#20101)
    
    ## Problem
    
    1. A Metal kernel that derives typed and untyped aliases from shared
    memory can reach the MSL compiler with pointer declarations and
    byte-offset casts that have no address-space qualifier.
    
    2. Metal rejects those generic pointers before kernel execution with a
    diagnostic such as:
    
    ```text
    error: pointer type must have explicit address space qualifier
    ```
    
    3. Before this change, the generic C codegen path could emit the
    equivalent of:
    
    ```metal
    half* typed_alias = (half*)((half*)(((char*)shared) + 4));
    void* void_alias = (void*)((char*)shared + 8);
    ```
    
    The source allocation is `threadgroup`, but the alias declarations,
    result casts, and intermediate `char*` casts do not carry that address
    space.
    
    ## Root cause
    
    1. The TIR result types of `tirx.ptr_byte_offset` and
    `tirx.handle_add_byte_offset` retain the pointer storage scope.
    
    2. `CodeGenMetal` previously inherited the generic `CodeGenC` handling
    for both the pointer-valued `Bind` nodes and these byte-offset
    operations. That path emits ordinary C pointer casts and does not
    express Metal address spaces.
    
    3. The loss occurs at the TIR-to-MSL codegen boundary, before Metal
    runtime compilation or GPU execution.
    
    ## What changed
    
    1. Handle pointer-valued `Bind` nodes in `CodeGenMetal` and emit the
    storage scope recorded in their `PointerType`.
    
    2. Lower typed and untyped byte-offset operations with the same address
    space on both the result pointer and the intermediate `char*` cast.
    
    3. Keep the existing generic C behavior for pointers without an explicit
    storage scope.
    
    4. The corrected source now has the equivalent form:
    
    ```metal
    threadgroup half* typed_alias =
        (threadgroup half*)((threadgroup half*)(((threadgroup char*)shared) + 
4));
    threadgroup void* void_alias =
        (threadgroup void*)((threadgroup void*)(((threadgroup char*)shared) + 
8));
    ```
    
    ## Validation
    
    1. Added a source-generation regression covering both typed and untyped
    aliases derived from a `float16` shared allocation.
    
    2. Added an end-to-end Metal test using the standard compilation
    pipeline. The generated source contains the expected `threadgroup`
    allocation, typed alias, void alias, and byte-offset casts.
    
    3. Ran the end-to-end test on a MacBook Air (`Mac14,2`) with an Apple M2
    GPU, macOS 15.6.1 (`24G90`), Metal 3, Command Line Tools, and the macOS
    15.5 SDK. TVM compiled MSL 2.3 through the Metal runtime without the
    offline `metal` or `metallib` tools. The kernel returned `[10.0, 11.0,
    12.0]`, matching the expected output.
    
    4. The focused tests pass:
    
    ```text
    python -m pytest -q \
      
tests/python/codegen/test_target_codegen_metal.py::test_codegen_pointer_byte_offsets_preserve_storage_scope
 \
      
tests/python/codegen/test_target_codegen_metal.py::test_pointer_byte_offsets_execute_in_threadgroup_memory
    # 2 passed
    ```
    
    5. Changed-file pre-commit checks pass:
    
    ```text
    pre-commit run --files \
      src/backend/metal/codegen/codegen_metal.cc \
      src/backend/metal/codegen/codegen_metal.h \
      tests/python/codegen/test_target_codegen_metal.py
    ```
    
    ## Scope
    
    1. This change only affects Metal pointer code generation when the TIR
    pointer type carries an explicit storage scope.
    
    2. The issue is independent of the pointee dtype: the source-generation
    regression uses `float16`, while the end-to-end Metal test exercises the
    same path with `float32`.
    
    3. It does not change compilation pipelines, Metal runtime
    implementation, public APIs, or serialized formats.
    
    4. Hardware validation is limited to the Apple M2 configuration above.
---
 src/backend/metal/codegen/codegen_metal.cc        | 46 ++++++++++++++
 src/backend/metal/codegen/codegen_metal.h         |  1 +
 tests/python/codegen/test_target_codegen_metal.py | 73 +++++++++++++++++++++++
 3 files changed, 120 insertions(+)

diff --git a/src/backend/metal/codegen/codegen_metal.cc 
b/src/backend/metal/codegen/codegen_metal.cc
index 78a0d7495b..361a9ee3d2 100644
--- a/src/backend/metal/codegen/codegen_metal.cc
+++ b/src/backend/metal/codegen/codegen_metal.cc
@@ -327,6 +327,31 @@ void CodeGenMetal::PrintStorageScope(const std::string& 
scope, std::ostream& os)
   }
 }
 
+void CodeGenMetal::VisitStmt_(const BindNode* op) {
+  const auto* pointer_type = op->var->ty.as<PointerTypeNode>();
+  if (pointer_type == nullptr || pointer_type->storage_scope.empty()) {
+    return CodeGenC::VisitStmt_(op);
+  }
+
+  const std::string& storage_scope = pointer_type->storage_scope;
+  alloc_storage_scope_[op->var.get()] = storage_scope;
+  RegisterHandleTypeFromPointer(op->var, &op->value);
+  std::string value = PrintExpr(op->value);
+  if (print_ssa_form_) {
+    TVM_FFI_ICHECK(!var_idmap_.count(op->var.get()));
+    var_idmap_[op->var.get()] = value;
+    return;
+  }
+
+  PrintIndent();
+  PrintStorageScope(storage_scope, stream);
+  PrintType(pointer_type->element_type, stream);
+  stream << "* " << AllocVarID(op->var.get()) << " = (";
+  PrintStorageScope(storage_scope, stream);
+  PrintType(pointer_type->element_type, stream);
+  stream << "*)" << value << ";\n";
+}
+
 void CodeGenMetal::VisitStmt_(const AllocBufferNode* op) {
   TVM_FFI_ICHECK(op->buffer.defined());
   std::string vid = AllocVarID(op->buffer.get());
@@ -445,6 +470,27 @@ void CodeGenMetal::VisitExpr_(const CallNode* op, 
std::ostream& os) {  // NOLINT
        << PrintExpr(a) << "[" << PrintExpr(op->args[3]) << "], "  //
        << PrintExpr(b) << "[" << PrintExpr(op->args[5]) << "], "  //
        << PrintExpr(c) << "[" << PrintExpr(op->args[7]) << "])";
+  } else if (op->op.same_as(builtin::ptr_byte_offset()) ||
+             op->op.same_as(builtin::handle_add_byte_offset())) {
+    bool is_typed_offset = op->op.same_as(builtin::ptr_byte_offset());
+    TVM_FFI_ICHECK_EQ(op->args.size(), is_typed_offset ? 3U : 2U);
+    const auto* pointer_type = op->ty.as<PointerTypeNode>();
+    TVM_FFI_ICHECK(pointer_type)
+        << "Metal pointer byte offsets must have a pointer result type, but 
got " << op->ty;
+    if (pointer_type->storage_scope.empty()) {
+      return CodeGenC::VisitExpr_(op, os);
+    }
+
+    os << "((";
+    PrintStorageScope(pointer_type->storage_scope, os);
+    PrintType(pointer_type->element_type, os);
+    os << "*)(((";
+    PrintStorageScope(pointer_type->storage_scope, os);
+    os << "char*)";
+    PrintExpr(op->args[0], os);
+    os << ") + ";
+    PrintExpr(op->args[1], os);
+    os << "))";
   } else if (op->op.same_as(builtin::reinterpret())) {
     if (!op->ty.as<PrimTypeNode>() || !op->args[0]->ty.as<PrimTypeNode>()) {
       return CodeGenC::VisitExpr_(op, os);
diff --git a/src/backend/metal/codegen/codegen_metal.h 
b/src/backend/metal/codegen/codegen_metal.h
index b54852ffa1..2ad84578ac 100644
--- a/src/backend/metal/codegen/codegen_metal.h
+++ b/src/backend/metal/codegen/codegen_metal.h
@@ -52,6 +52,7 @@ class CodeGenMetal final : public CodeGenC {
   void PrintVecElemStore(const std::string& vec, const PrimType& t, int i,
                          const std::string& value) final;
   // overload visitor
+  void VisitStmt_(const BindNode* op) final;                         // 
NOLINT(*)
   void VisitStmt_(const AllocBufferNode* op) final;                  // 
NOLINT(*)
   void VisitExpr_(const SelectNode* op, std::ostream& os) final;     // 
NOLINT(*)
   void VisitExpr_(const BroadcastNode* op, std::ostream& os) final;  // 
NOLINT(*)
diff --git a/tests/python/codegen/test_target_codegen_metal.py 
b/tests/python/codegen/test_target_codegen_metal.py
index a1fb713656..f0d9998b4c 100644
--- a/tests/python/codegen/test_target_codegen_metal.py
+++ b/tests/python/codegen/test_target_codegen_metal.py
@@ -384,5 +384,78 @@ def test_codegen_simdgroup_buffer_data():
     assert "simdgroup_multiply_accumulate(" in source
 
 
+def test_codegen_pointer_byte_offsets_preserve_storage_scope():
+    """Pointer byte offsets should preserve the source Metal address space."""
+
+    @I.ir_module(s_tir=True)
+    class Module:
+        @T.prim_func(s_tir=True)
+        def kernel():
+            T.func_attr(
+                {
+                    "calling_conv": 2,
+                    "global_symbol": "kernel",
+                    "tirx.kernel_launch_params": [],
+                }
+            )
+            shared = T.alloc_buffer((16,), "float16", scope="shared")
+            typed_alias = T.ptr_byte_offset(shared.data, 4, "float16")
+            typed_buffer = T.decl_buffer((14,), "float16", data=typed_alias, 
scope="shared")
+            void_alias = T.handle_add_byte_offset(shared.data, 8)
+            void_buffer = T.decl_buffer((12,), "float16", data=void_alias, 
scope="shared")
+            typed_buffer[0] = T.float16(1)
+            void_buffer[0] = T.float16(2)
+
+    metal_codegen = tvm.get_global_func("target.build.metal")
+    module = metal_codegen(Module, tvm.target.Target("metal"))
+    source = module.inspect_source()
+
+    assert "threadgroup half* typed_alias" in source
+    assert "threadgroup void* void_alias" in source
+    assert source.count("threadgroup char*") == 2
+
+
[email protected]
[email protected](not env.has_metal(), reason="need metal")
+def test_pointer_byte_offsets_execute_in_threadgroup_memory():
+    """Pointer byte offsets should execute in Metal threadgroup memory."""
+
+    @I.ir_module(s_tir=True)
+    class Module:
+        @T.prim_func(s_tir=True)
+        def main(A: T.Buffer((16,), "float32"), B: T.Buffer((16,), "float32")):
+            for bx in T.thread_binding(1, thread="blockIdx.x"):
+                for tx in T.thread_binding(1, thread="threadIdx.x"):
+                    shared = T.alloc_buffer((16,), "float32", scope="shared")
+                    typed_alias = T.ptr_byte_offset(shared.data, 4, "float32")
+                    typed_buffer = T.decl_buffer((15,), "float32", 
data=typed_alias, scope="shared")
+                    void_alias = T.handle_add_byte_offset(shared.data, 8)
+                    void_buffer = T.decl_buffer((14,), "float32", 
data=void_alias, scope="shared")
+                    shared[0] = A[0]
+                    typed_buffer[0] = A[1]
+                    void_buffer[0] = A[2]
+                    B[0] = shared[0]
+                    B[1] = typed_buffer[0]
+                    B[2] = void_buffer[0]
+
+    executable = tvm.compile(Module, target="metal")
+    source = executable.mod.imports[0].inspect_source()
+
+    assert "threadgroup float shared[16]" in source
+    assert "threadgroup float* typed_alias" in source
+    assert "threadgroup void* void_alias" in source
+    assert source.count("threadgroup char*") == 2
+
+    def run_and_check():
+        dev = tvm.metal(0)
+        host_input = np.arange(16, dtype="float32") + 10
+        input_tensor = tvm.runtime.tensor(host_input, dev)
+        output_tensor = tvm.runtime.tensor(np.zeros(16, dtype="float32"), dev)
+        executable(input_tensor, output_tensor)
+        tvm.testing.assert_allclose(output_tensor.numpy()[:3], host_input[:3])
+
+    tvm.testing.run_with_gpu_lock(run_and_check)
+
+
 if __name__ == "__main__":
     tvm.testing.main()

Reply via email to