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

MasterJH5574 pushed a commit to branch v0.27.0
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/v0.27.0 by this push:
     new 7b5acb32bd [FIX][TIRx][CUDA] Support low-precision, tensor-map and 
cluster kernels in CUDA-host bundles (#20489)
7b5acb32bd is described below

commit 7b5acb32bd271342a24130f4ef439ff6c2e52c6c
Author: Bohan Hou <[email protected]>
AuthorDate: Tue Sep 29 15:07:09 2026 -0400

    [FIX][TIRx][CUDA] Support low-precision, tensor-map and cluster kernels in 
CUDA-host bundles (#20489)
    
    A CUDA-host bundle (`tvm.backend.cuda.export_cuda_host`, #20395)
    compiles device and host code as one NVCC C++ translation unit. Building
    the native kernels of mlc-ai/TIRx-kernels this way exposed several
    patterns that the bundle could not compile or launch:
    
    - **Low-precision types in the host pass.** The CUDA device header
    included `cuda_fp16.h`, `cuda_bf16.h`, `cuda_fp8.h`, `cuda_fp6.h` and
    `cuda_fp4.h` (and defined the `fp8_e4_t`-style aliases) only under
    `defined(__CUDA_ARCH__)`. NVCC's host pass then fails on kernel
    signatures that use `half` or `nv_bfloat16`. The guards now also admit
    the host pass (`!defined(__CUDA_ARCH__) || __CUDA_ARCH__ >= N`); NVRTC
    and device-only NVCC compilation always define `__CUDA_ARCH__` and are
    unchanged.
    - **Tensor-map parameters.** The host wrapper bound a `T.TensorMap()`
    parameter as `CUtensorMap* x = ((void*)...)`, which C accepts and C++
    rejects. It now casts explicitly.
    - **Argument types at the launch.** Host C code spells some device types
    differently (bfloat16 is `uint16_t*` on the host, `nv_bfloat16*` in the
    kernel), so the direct `<<<>>>` launch did not type-check. Launches now
    go through a small helper that converts each argument to the kernel's
    own parameter type and calls `cudaLaunchKernelEx`.
    - **Launch attributes.** `clusterCtaIdx.*`, `preferredClusterCtaIdx.*`,
    programmatic dependent launch and cooperative launch were rejected. They
    now become the corresponding `cudaLaunchAttribute`s, following
    `CUDAWrappedFunc` in the CUDA runtime module (including
    `cudaFuncAttributeNonPortableClusterSizeAllowed` for cluster launches
    and omitting a unit preferred cluster). Required block dimensions remain
    unsupported and are still diagnosed.
---
 python/tvm/backend/cuda/codegen/header.py        |  13 ++-
 src/backend/cuda/codegen/codegen_cuda_host.cc    | 127 ++++++++++++++++++---
 tests/python/codegen/test_target_codegen_cuda.py | 138 ++++++++++++++++++++++-
 3 files changed, 256 insertions(+), 22 deletions(-)

diff --git a/python/tvm/backend/cuda/codegen/header.py 
b/python/tvm/backend/cuda/codegen/header.py
index 55b9839426..edb682af06 100644
--- a/python/tvm/backend/cuda/codegen/header.py
+++ b/python/tvm/backend/cuda/codegen/header.py
@@ -113,9 +113,12 @@ def header_generator(tags):
 #endif
 """
 
+    # The low-precision type headers below are also visible to NVCC's host pass
+    # (no __CUDA_ARCH__), which parses kernel signatures when this source 
shares
+    # a translation unit with host code, e.g. 
tvm.backend.cuda.export_cuda_host.
     if "fp16" in tags:
         header += R"""
-#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 530)
+#if !defined(__CUDA_ARCH__) || (__CUDA_ARCH__ >= 530)
 #include <cuda_fp16.h>
 __device__ half max(half a, half b)
 {
@@ -173,7 +176,7 @@ CUDA_UNSUPPORTED_HALF_MATH_UNARY(hexp, exp)
 
     if "bf16" in tags:
         header += R"""
-#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
+#if !defined(__CUDA_ARCH__) || (__CUDA_ARCH__ >= 800)
 #include <cuda_bf16.h>
 __device__ nv_bfloat16 max(nv_bfloat16 a, nv_bfloat16 b)
 {
@@ -224,7 +227,7 @@ CUDA_UNSUPPORTED_HALF_MATH_UNARY(herf, erf)
 
     if "fp8" in tags:
         header += R"""
-#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 890)
+#if !defined(__CUDA_ARCH__) || (__CUDA_ARCH__ >= 890)
 #include <cuda_fp8.h>
 using fp8_e4_t = __nv_fp8_e4m3;
 using fp8_e4x2_t = __nv_fp8x2_e4m3;
@@ -258,7 +261,7 @@ struct fp8_e8x16_t {
 
     if "fp6" in tags:
         header += R"""
-#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 1000)
+#if !defined(__CUDA_ARCH__) || (__CUDA_ARCH__ >= 1000)
 #include <cuda_fp6.h>
 using fp6_e2_t = __nv_fp6_e2m3;
 using fp6_e2x2_t = __nv_fp6x2_e2m3;
@@ -283,7 +286,7 @@ struct fp6_e3x16_t {
 
     if "fp4" in tags:
         header += R"""
-#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 800)
+#if !defined(__CUDA_ARCH__) || (__CUDA_ARCH__ >= 800)
 #include <cuda_fp4.h>
 using fp4_e2_t = __nv_fp4_e2m1;
 using fp4_e2x2_t = __nv_fp4x2_e2m1;
diff --git a/src/backend/cuda/codegen/codegen_cuda_host.cc 
b/src/backend/cuda/codegen/codegen_cuda_host.cc
index 5fb4aee7e5..c910d84234 100644
--- a/src/backend/cuda/codegen/codegen_cuda_host.cc
+++ b/src/backend/cuda/codegen/codegen_cuda_host.cc
@@ -62,6 +62,16 @@ class CodeGenCUDAHost : public CodeGenCHost {
                 << "  TVMFFIErrorSetRaisedFromCStrParts(\"CUDAError\", parts, 
2);\n"
                 << "  cudaGetLastError();\n"
                 << "  return -1;\n}\n";
+    // Host C types spell some device types differently (bfloat16 is uint16_t 
on
+    // the host), so convert each argument to the kernel's own parameter type.
+    launch_ = name_supply_->FreshName("tvm_cuda_host_launch");
+    decl_stream << "template <typename... Params, typename... Args>\n"
+                << "static cudaError_t " << launch_
+                << "(void (*kernel)(Params...), const cudaLaunchConfig_t* 
config, "
+                << "Args... args) {\n"
+                << "  static_assert(sizeof...(Params) == sizeof...(Args), "
+                << "\"kernel argument count mismatch\");\n"
+                << "  return cudaLaunchKernelEx(config, kernel, 
((Params)args)...);\n}\n";
   }
 
   using CodeGenCHost::PrintType;
@@ -102,6 +112,20 @@ class CodeGenCUDAHost : public CodeGenCHost {
 
   ffi::Array<ffi::String> GetFunctionNames() { return function_names_; }
 
+  void Dispatch_(const BindNode* op) override {
+    // C accepts the implicit void* conversion emitted for descriptor handles;
+    // the CUDA translation unit is C++, which requires an explicit cast.
+    auto* ptr = op->var->ty.as<PointerTypeNode>();
+    if (print_ssa_form_ || !ptr || 
!ptr->element_type.as<tirx::TensorMapTypeNode>()) {
+      CodeGenC::Dispatch_(op);
+      return;
+    }
+    std::string value = PrintExpr(op->value);
+    PrintIndent();
+    stream << "CUtensorMap* " << AllocVarID(op->var.get()) << " = 
(CUtensorMap*)(" << value
+           << ");\n";
+  }
+
   void Dispatch_(const CallNode* op, std::ostream& os) override {
     if (op->op.same_as(tirx::builtin::tvm_stack_alloca()) &&
         op->args[0].as_or_throw<StringImm>()->value == "tensormap") {
@@ -168,10 +192,13 @@ class CodeGenCUDAHost : public CodeGenCHost {
         << "cuda_host kernel calls require a string kernel symbol";
     const auto& symbol = op->args[0].as<StringImmNode>()->value;
 
-    std::array<int, 6> axes;
+    // Match LaunchParamConfig's layout: grid, block, cluster and preferred 
cluster.
+    std::array<int, 12> axes;
     axes.fill(-1);
     int shared_memory = -1;
     size_t num_launch_values = 0;
+    bool use_programmatic_dependent_launch = false;
+    bool use_cooperative_launch = false;
     std::unordered_set<std::string> seen;
     for (size_t i = 0; i < attr->launch_params.size(); ++i) {
       std::string tag = attr->launch_params[i];
@@ -179,19 +206,24 @@ class CodeGenCUDAHost : public CodeGenCHost {
           << "cuda_host duplicate launch parameter: " << tag;
       // These are flags, not values in the argument suffix.  Reject 
unsupported
       // launch semantics before decoding any operands, rather than dropping 
them.
-      if (tag == runtime::launch_param::kUseProgramaticDependentLaunch ||
-          tag == runtime::launch_param::kUseCooperativeLaunch ||
-          tag == runtime::launch_param::kUseRequiredBlockDimension) {
+      if (tag == runtime::launch_param::kUseProgramaticDependentLaunch) {
+        use_programmatic_dependent_launch = true;
+      } else if (tag == runtime::launch_param::kUseCooperativeLaunch) {
+        use_cooperative_launch = true;
+      } else if (tag == runtime::launch_param::kUseRequiredBlockDimension) {
         TVM_FFI_THROW(ValueError) << "cuda_host does not support launch flag: 
" << tag;
       } else if (tag == runtime::launch_param::kUseDynamicSharedMemoryTag) {
         TVM_FFI_CHECK_EQ(i + 1, attr->launch_params.size(), ValueError)
             << "cuda_host dynamic shared memory must be the last launch 
parameter";
         shared_memory = static_cast<int>(num_launch_values++);
       } else {
+        static const char* kScopes[] = {"blockIdx.", "threadIdx.", 
"clusterCtaIdx.",
+                                        "preferredClusterCtaIdx."};
         int axis = -1;
-        for (int j = 0; j < 3; ++j) {
-          if (tag == std::string("blockIdx.") + "xyz"[j]) axis = j;
-          if (tag == std::string("threadIdx.") + "xyz"[j]) axis = j + 3;
+        for (int rank = 0; rank < 4; ++rank) {
+          for (int j = 0; j < 3; ++j) {
+            if (tag == std::string(kScopes[rank]) + "xyz"[j]) axis = rank * 3 
+ j;
+          }
         }
         TVM_FFI_CHECK_GE(axis, 0, ValueError)
             << "cuda_host does not support launch parameter: " << tag;
@@ -229,7 +261,7 @@ class CodeGenCUDAHost : public CodeGenCHost {
       arguments.push_back(name);
     }
     auto launch_arg = [&](int index) { return arguments[launch_begin - 1 + 
index]; };
-    std::array<std::string, 6> dimensions;
+    std::array<std::string, 12> dimensions;
     for (size_t i = 0; i < axes.size(); ++i) {
       if (axes[i] < 0) {
         dimensions[i] = "1";
@@ -262,16 +294,78 @@ class CodeGenCUDAHost : public CodeGenCHost {
       PrintIndent();
       stream << "}\n";
     }
+    auto has_axis = [&](int rank) {
+      return axes[rank * 3] >= 0 || axes[rank * 3 + 1] >= 0 || axes[rank * 3 + 
2] >= 0;
+    };
+    bool use_cluster = has_axis(2);
+    bool use_preferred_cluster = has_axis(3);
+    if (use_cluster) {
+      CheckError("cudaFuncSetAttribute(::" + std::string(symbol) +
+                 ", cudaFuncAttributeNonPortableClusterSizeAllowed, 1)");
+    }
+    std::string config = name_supply_->FreshName("cuda_config");
+    PrintIndent();
+    stream << "cudaLaunchConfig_t " << config << " = {};\n";
+    PrintIndent();
+    stream << config << ".gridDim = dim3(" << dimensions[0] << ", " << 
dimensions[1] << ", "
+           << dimensions[2] << ");\n";
+    PrintIndent();
+    stream << config << ".blockDim = dim3(" << dimensions[3] << ", " << 
dimensions[4] << ", "
+           << dimensions[5] << ");\n";
+    PrintIndent();
+    stream << config << ".dynamicSmemBytes = " << bytes << ";\n";
     PrintIndent();
-    stream << "::" << symbol << "<<<dim3(" << dimensions[0] << ", " << 
dimensions[1] << ", "
-           << dimensions[2] << "), dim3(" << dimensions[3] << ", " << 
dimensions[4] << ", "
-           << dimensions[5] << "), " << bytes << ", " << cuda_stream << ">>>(";
-    for (size_t i = 0; i + 1 < launch_begin; ++i) {
-      if (i != 0) stream << ", ";
-      stream << arguments[i];
+    stream << config << ".stream = " << cuda_stream << ";\n";
+    int max_attrs = use_cluster + use_preferred_cluster + 
use_programmatic_dependent_launch +
+                    use_cooperative_launch;
+    if (max_attrs > 0) {
+      std::string attrs = name_supply_->FreshName("cuda_attrs");
+      PrintIndent();
+      stream << "cudaLaunchAttribute " << attrs << "[" << max_attrs << "] = 
{};\n";
+      PrintIndent();
+      stream << config << ".attrs = " << attrs << ";\n";
+      auto set_dimension = [&](const std::string& id, const std::string& 
field, int rank) {
+        PrintIndent();
+        stream << attrs << "[" << config << ".numAttrs].id = " << id << ";\n";
+        for (int j = 0; j < 3; ++j) {
+          PrintIndent();
+          stream << attrs << "[" << config << ".numAttrs].val." << field << 
"." << "xyz"[j] << " = "
+                 << dimensions[rank * 3 + j] << ";\n";
+        }
+        PrintIndent();
+        stream << "++" << config << ".numAttrs;\n";
+      };
+      if (use_cluster) {
+        set_dimension("cudaLaunchAttributeClusterDimension", "clusterDim", 2);
+      }
+      if (use_preferred_cluster) {
+        // Match the CUDA runtime module: a unit preferred cluster adds no 
attribute.
+        PrintIndent();
+        stream << "if (" << dimensions[9] << " != 1 || " << dimensions[10] << 
" != 1 || "
+               << dimensions[11] << " != 1) {\n";
+        int scope = BeginScope();
+        set_dimension("cudaLaunchAttributePreferredClusterDimension", 
"preferredClusterDim", 3);
+        EndScope(scope);
+        PrintIndent();
+        stream << "}\n";
+      }
+      auto set_flag = [&](const std::string& id, const std::string& field) {
+        PrintIndent();
+        stream << attrs << "[" << config << ".numAttrs].id = " << id << ";\n";
+        PrintIndent();
+        stream << attrs << "[" << config << ".numAttrs++].val." << field << " 
= 1;\n";
+      };
+      if (use_programmatic_dependent_launch) {
+        set_flag("cudaLaunchAttributeProgrammaticStreamSerialization",
+                 "programmaticStreamSerializationAllowed");
+      }
+      if (use_cooperative_launch) {
+        set_flag("cudaLaunchAttributeCooperative", "cooperative");
+      }
     }
-    stream << ");\n";
-    CheckError("cudaGetLastError()", true);
+    std::string launch = launch_ + "(::" + std::string(symbol) + ", &" + 
config;
+    for (size_t i = 0; i + 1 < launch_begin; ++i) launch += ", " + 
arguments[i];
+    CheckError(launch + ")", true);
     os << "0";
   }
 
@@ -375,6 +469,7 @@ class CodeGenCUDAHost : public CodeGenCHost {
   }
 
   std::string check_error_;
+  std::string launch_;
   ffi::Array<ffi::String> function_names_;
 };
 
diff --git a/tests/python/codegen/test_target_codegen_cuda.py 
b/tests/python/codegen/test_target_codegen_cuda.py
index 6c3aa00edd..ff8bd00876 100644
--- a/tests/python/codegen/test_target_codegen_cuda.py
+++ b/tests/python/codegen/test_target_codegen_cuda.py
@@ -74,7 +74,7 @@ def test_cuda_host_bundle(tmp_path):
     target = tvm.target.Target("cuda", host="cuda_host")
     built = tvm.compile(add_one, target=target).mod
     source = export_cuda_host(built)
-    assert source.index("__global__") < source.index("<<<")
+    assert source.index("__global__") < source.index("cudaLaunchKernelEx")
     library = tvm_ffi.cpp.build_inline(
         name="cuda_host_add_one",
         cuda_sources=source,
@@ -95,6 +95,142 @@ def test_cuda_host_bundle(tmp_path):
     tvm.testing.run_with_gpu_lock(run_and_check)
 
 
[email protected]
[email protected](not env.has_cuda(), reason="need cuda")
+def test_cuda_host_bundle_bf16_cluster(tmp_path):
+    """bfloat16 pointers and cluster launch attributes in a CUDA-host 
bundle."""
+    from shutil import which
+
+    import tvm_ffi.cpp
+
+    from tvm.backend.cuda import export_cuda_host
+
+    ml_dtypes = pytest.importorskip("ml_dtypes")
+    if which("nvcc") is None:
+        pytest.skip("CUDA-host compilation requires NVCC")
+    if int(tvm.cuda(0).compute_version.split(".")[0]) < 9:
+        pytest.skip("thread block clusters require SM90 or newer")
+
+    @T.prim_func
+    def main(A_ptr: T.handle, B_ptr: T.handle, R_ptr: T.handle):
+        A = T.match_buffer(A_ptr, (128,), "bfloat16")
+        B = T.match_buffer(B_ptr, (128,), "bfloat16")
+        R = T.match_buffer(R_ptr, (4,), "int32")
+        T.device_entry()
+        for cx in T.thread_binding(2, thread="clusterCtaIdx.x"):
+            for bx in T.thread_binding(4, thread="blockIdx.x"):
+                for tx in T.thread_binding(32, thread="threadIdx.x"):
+                    B[bx * 32 + tx] = A[bx * 32 + tx] + T.bfloat16(1)
+                    if tx == 0:
+                        R[bx] = cx
+
+    arch = env.cuda_arch()
+    target = tvm.target.Target({"kind": "cuda", "arch": arch}, 
host="cuda_host")
+    with target:
+        built = tvm.compile(tvm.IRModule({"main": main}), target=target, 
tir_pipeline="tirx").mod
+    source = export_cuda_host(built)
+    assert "cudaLaunchAttributeClusterDimension" in source
+    library = tvm_ffi.cpp.build_inline(
+        name="cuda_host_bf16_cluster",
+        cuda_sources=source,
+        extra_cuda_cflags=[f"-arch={arch}"],
+        build_directory=str(tmp_path),
+        backend="cuda",
+    )
+    loaded = tvm_ffi.load_module(library)
+
+    def run_and_check():
+        dev = tvm.cuda(0)
+        values = np.arange(128).astype(ml_dtypes.bfloat16)
+        a = tvm.runtime.tensor(values, dev)
+        b = tvm.runtime.empty((128,), "bfloat16", dev)
+        r = tvm.runtime.empty((4,), "int32", dev)
+        loaded["main"](a, b, r)
+        tvm.testing.assert_allclose(b.numpy().astype("float32"), 
np.arange(128) + 1)
+        # Each CTA reports its rank within a two-CTA cluster.
+        np.testing.assert_array_equal(r.numpy(), [0, 1, 0, 1])
+
+    tvm.testing.run_with_gpu_lock(run_and_check)
+
+
[email protected]
[email protected](not env.has_cuda(), reason="need cuda")
+def test_cuda_host_bundle_programmatic_dependent_launch(tmp_path):
+    """A programmatic-dependent-launch flag becomes a launch attribute."""
+    from shutil import which
+
+    import tvm_ffi.cpp
+
+    from tvm.backend.cuda import export_cuda_host
+
+    if which("nvcc") is None:
+        pytest.skip("CUDA-host compilation requires NVCC")
+    if int(tvm.cuda(0).compute_version.split(".")[0]) < 9:
+        pytest.skip("programmatic dependent launch requires SM90 or newer")
+
+    @T.prim_func(s_tir=True)
+    def add_one(A: T.Buffer((32,), "float32"), B: T.Buffer((32,), "float32")):
+        T.func_attr({"tirx.kernel_launch_params": 
["tirx.use_programtic_dependent_launch"]})
+        for tx in T.thread_binding(32, "threadIdx.x"):
+            B[tx] = A[tx] + T.float32(1)
+
+    target = tvm.target.Target("cuda", host="cuda_host")
+    source = export_cuda_host(tvm.compile(add_one, target=target).mod)
+    assert "cudaLaunchAttributeProgrammaticStreamSerialization" in source
+    library = tvm_ffi.cpp.build_inline(
+        name="cuda_host_programmatic_dependent_launch",
+        cuda_sources=source,
+        extra_cuda_cflags=[f"-arch={target.arch}"],
+        build_directory=str(tmp_path),
+        backend="cuda",
+    )
+    loaded = tvm_ffi.load_module(library)
+
+    def run_and_check():
+        dev = tvm.cuda(0)
+        values = np.arange(32, dtype="float32")
+        a = tvm.runtime.tensor(values, dev)
+        b = tvm.runtime.empty((32,), "float32", dev)
+        loaded["add_one"](a, b)
+        tvm.testing.assert_allclose(b.numpy(), values + 1)
+
+    tvm.testing.run_with_gpu_lock(run_and_check)
+
+
[email protected]
[email protected](not env.has_cuda(), reason="need cuda")
+def test_cuda_host_bundle_tensor_map_parameter(tmp_path, monkeypatch):
+    """A tensor-map parameter handle compiles in the C++ host wrapper."""
+    from shutil import which
+
+    import tvm_ffi.cpp
+
+    from tvm.backend.cuda import export_cuda_host
+
+    if which("nvcc") is None:
+        pytest.skip("CUDA-host compilation requires NVCC")
+
+    @T.prim_func
+    def main(A_map: T.TensorMap()):
+        T.device_entry()
+        tx = T.thread_id([32])
+        if tx == 0:
+            T.evaluate(T.address_of(A_map))
+
+    target = tvm.target.Target({"kind": "cuda", "arch": "sm_90a"}, 
host="cuda_host")
+    with target:
+        built = tvm.compile(tvm.IRModule({"main": main}), target=target, 
tir_pipeline="tirx").mod
+    source = export_cuda_host(built)
+    assert "(CUtensorMap*)" in source
+    monkeypatch.setenv("TVM_FFI_CUDA_ARCH_LIST", "9.0a")
+    tvm_ffi.cpp.build_inline(
+        name="cuda_host_tensor_map_parameter",
+        cuda_sources=source,
+        build_directory=str(tmp_path),
+        backend="cuda",
+    )
+
+
 @pytest.mark.gpu
 @pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
 def test_cuda_vectorize_add():

Reply via email to