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

jinhongyii 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 0d9c9384b1 [TIRx][CUDA] Support exact required block dimensions 
(#20223)
0d9c9384b1 is described below

commit 0d9c9384b1473e2a2e7d84f077f151bb3b9130bc
Author: Bohan Hou <[email protected]>
AuthorDate: Fri Aug 28 21:42:32 2026 -0400

    [TIRx][CUDA] Support exact required block dimensions (#20223)
    
    This adds an exact CUDA launch contract for TIRx kernels that need PTX
    `.reqntid` rather than advisory launch bounds.
    
    - preserve `tirx.required_block_size` through host/device splitting
    - emit CUDA 13 `__block_size__` with static thread and cluster
    dimensions
    - pass the required-block flag through runtime metadata without adding a
    packed operand
    - launch with `CU_LAUNCH_KERNEL_REQUIRED_BLOCK_DIM` and avoid duplicate
    cluster attributes
    - reject incompatible launch-bound and max-register controls
    
    Validation:
    - `cmake --build build --parallel`
    - `python -m pytest -q
    tests/python/tirx-transform/test_tir_transform_split_host_device.py
    tests/python/tirx/codegen/test_codegen_cuda.py` (225 passed)
    - `pre-commit run --files include/tvm/tirx/function.h
    src/backend/cuda/codegen/codegen_cuda.cc
    src/backend/cuda/runtime/cuda_module.cc src/runtime/metadata.h
    src/runtime/thread_storage_scope.h
    src/tirx/transform/split_host_device.cc
    tests/python/tirx-transform/test_tir_transform_split_host_device.py
    tests/python/tirx/codegen/test_codegen_cuda.py`
    - CUDA 13.2 SM100 runtime launch through a TIRx kernel with generated
    PTX `.reqntid 128,1,1`
---
 include/tvm/tirx/function.h                        | 13 ++++++
 src/backend/cuda/codegen/codegen_cuda.cc           | 20 +++++++++
 src/backend/cuda/runtime/cuda_module.cc            | 50 +++++++++++++++++-----
 src/runtime/metadata.h                             |  2 +
 src/runtime/thread_storage_scope.h                 |  6 +++
 src/tirx/transform/split_host_device.cc            | 34 ++++++++++++++-
 .../test_tir_transform_split_host_device.py        | 30 +++++++++++++
 tests/python/tirx/codegen/test_codegen_cuda.py     | 39 +++++++++++++++++
 8 files changed, 183 insertions(+), 11 deletions(-)

diff --git a/include/tvm/tirx/function.h b/include/tvm/tirx/function.h
index 58912aefe4..523ca3b3d1 100644
--- a/include/tvm/tirx/function.h
+++ b/include/tvm/tirx/function.h
@@ -271,6 +271,7 @@ namespace attr {
  *
  * - tvm::runtime::launch_param::kUseProgramaticDependentLaunch
  * - tvm::runtime::launch_param::kUseCooperativeLaunch
+ * - tvm::runtime::launch_param::kUseRequiredBlockDimension
  *
  *   Flag-only launch attributes.  These tags add no packed operand.
  *
@@ -303,6 +304,18 @@ constexpr const char* kLaunchBoundsMaxBlocksPerCluster =
  */
 constexpr const char* kMaxRegisters = "tirx.max_registers";
 
+/*!
+ * \brief Require CUDA to use the statically-declared block and cluster 
dimensions.
+ *
+ * Emits the CUDA 13 ``__block_size__`` kernel qualifier.  Unlike
+ * ``__launch_bounds__``, this is an exact launch contract: CUDA derives the
+ * PTX ``.reqntid`` directive from the thread extents, and interprets the
+ * launch grid in clusters using the cluster-CTA extents.
+ *
+ * Type: IntImm (must be 1)
+ */
+constexpr const char* kRequiredBlockSize = "tirx.required_block_size";
+
 /*!
  * \brief Whether to set noalias rule on the function arguments.
  *
diff --git a/src/backend/cuda/codegen/codegen_cuda.cc 
b/src/backend/cuda/codegen/codegen_cuda.cc
index 15484391ca..ef5f13c0a5 100644
--- a/src/backend/cuda/codegen/codegen_cuda.cc
+++ b/src/backend/cuda/codegen/codegen_cuda.cc
@@ -244,6 +244,26 @@ void CodeGenCUDA::PrintExtraAttrs(const PrimFunc& f, 
std::ostream& os) {
     cluster_cta_x_is_linear_rank_ = false;
   }
   auto max_registers = f->GetAttr<int64_t>(tirx::attr::kMaxRegisters);
+  auto required_block_size = 
f->GetAttr<int64_t>(tirx::attr::kRequiredBlockSize);
+  if (required_block_size.has_value()) {
+    TVM_FFI_ICHECK_EQ(required_block_size.value(), 1);
+    TVM_FFI_ICHECK(!max_registers.has_value() &&
+                   
!f->GetAttr<int64_t>(tirx::attr::kLaunchBoundsMinBlocksPerSM).has_value() &&
+                   
!f->GetAttr<int64_t>(tirx::attr::kLaunchBoundsMaxBlocksPerCluster).has_value())
+        << tirx::attr::kRequiredBlockSize
+        << " cannot be combined with CUDA launch bounds or maximum registers";
+    const auto* tx = extractor.threadIdx_x_ext.as<IntImmNode>();
+    const auto* ty = extractor.threadIdx_y_ext.as<IntImmNode>();
+    const auto* tz = extractor.threadIdx_z_ext.as<IntImmNode>();
+    const auto* cx = extractor.clusterCtaIdx_x_ext.as<IntImmNode>();
+    const auto* cy = extractor.clusterCtaIdx_y_ext.as<IntImmNode>();
+    const auto* cz = extractor.clusterCtaIdx_z_ext.as<IntImmNode>();
+    TVM_FFI_ICHECK(tx && ty && tz && cx && cy && cz)
+        << tirx::attr::kRequiredBlockSize << " requires static thread and 
cluster dimensions";
+    os << " __block_size__((" << tx->value << ", " << ty->value << ", " << 
tz->value << "), ("
+       << cx->value << ", " << cy->value << ", " << cz->value << "))";
+    return;
+  }
   if (max_registers.has_value()) {
     TVM_FFI_ICHECK_GT(max_registers.value(), 0);
     
TVM_FFI_ICHECK(!f->GetAttr<int64_t>(tirx::attr::kLaunchBoundsMinBlocksPerSM).has_value()
 &&
diff --git a/src/backend/cuda/runtime/cuda_module.cc 
b/src/backend/cuda/runtime/cuda_module.cc
index 3984bfde39..271f62a24b 100644
--- a/src/backend/cuda/runtime/cuda_module.cc
+++ b/src/backend/cuda/runtime/cuda_module.cc
@@ -250,9 +250,11 @@ class CUDAWrappedFunc {
     CUstream strm = static_cast<CUstream>(TVMFFIEnvGetStream(kDLCUDA, 
device_id));
     std::array<CUlaunchAttribute, 4> attrs{};
     unsigned int num_attrs = 0;
+    bool use_required_block_dimension = 
launch_param_config_.use_required_block_dimension();
 
-    // 1) Cluster
-    if (launch_param_config_.use_cluster_launch()) {
+    // 1) Cluster.  __block_size__ fixes this at compile time, and supplying a
+    // runtime cluster attribute for the same kernel is forbidden by CUDA.
+    if (!use_required_block_dimension && 
launch_param_config_.use_cluster_launch()) {
       CUlaunchAttribute attr{};
       attr.id = CU_LAUNCH_ATTRIBUTE_CLUSTER_DIMENSION;
       attr.value.clusterDim.x = wl.cluster_dim(0);
@@ -262,8 +264,17 @@ class CUDAWrappedFunc {
     }
 
     // 1b) Preferred cluster (CUDA 12.8+, 
cudaLaunchAttributePreferredClusterDimension)
-    if (wl.preferred_cluster_dim(0) != 1 || wl.preferred_cluster_dim(1) != 1 ||
-        wl.preferred_cluster_dim(2) != 1) {
+    if (use_required_block_dimension) {
+      for (int i = 0; i < 3; ++i) {
+        TVM_FFI_ICHECK(wl.preferred_cluster_dim(i) == 1 ||
+                       wl.preferred_cluster_dim(i) == wl.cluster_dim(i))
+            << "CUDA required block dimensions cannot be combined with a 
different preferred "
+               "cluster size";
+      }
+    }
+    if (!use_required_block_dimension &&
+        (wl.preferred_cluster_dim(0) != 1 || wl.preferred_cluster_dim(1) != 1 
||
+         wl.preferred_cluster_dim(2) != 1)) {
       CUlaunchAttribute attr{};
       attr.id = CU_LAUNCH_ATTRIBUTE_PREFERRED_CLUSTER_DIMENSION;
       attr.value.clusterDim.x = wl.preferred_cluster_dim(0);
@@ -290,12 +301,31 @@ class CUDAWrappedFunc {
 
     // 4) Launch
     CUlaunchConfig config{};
-    config.gridDimX = wl.grid_dim(0);
-    config.gridDimY = wl.grid_dim(1);
-    config.gridDimZ = wl.grid_dim(2);
-    config.blockDimX = wl.block_dim(0);
-    config.blockDimY = wl.block_dim(1);
-    config.blockDimZ = wl.block_dim(2);
+    if (use_required_block_dimension) {
+#if CUDA_VERSION >= 13000
+      for (int i = 0; i < 3; ++i) {
+        TVM_FFI_ICHECK_EQ(wl.grid_dim(i) % wl.cluster_dim(i), 0U)
+            << "CUDA required block dimension launch needs each logical 
block-grid dimension "
+               "to be divisible by its compile-time cluster dimension";
+      }
+      config.gridDimX = wl.grid_dim(0) / wl.cluster_dim(0);
+      config.gridDimY = wl.grid_dim(1) / wl.cluster_dim(1);
+      config.gridDimZ = wl.grid_dim(2) / wl.cluster_dim(2);
+      config.blockDimX = CU_LAUNCH_KERNEL_REQUIRED_BLOCK_DIM;
+      config.blockDimY = 1;
+      config.blockDimZ = 1;
+#else
+      TVM_FFI_THROW(InternalError)
+          << "CUDA required block dimensions need CUDA Toolkit 13 or newer";
+#endif
+    } else {
+      config.gridDimX = wl.grid_dim(0);
+      config.gridDimY = wl.grid_dim(1);
+      config.gridDimZ = wl.grid_dim(2);
+      config.blockDimX = wl.block_dim(0);
+      config.blockDimY = wl.block_dim(1);
+      config.blockDimZ = wl.block_dim(2);
+    }
     config.sharedMemBytes = wl.dyn_shmem_size;
     config.hStream = strm;
     config.attrs = num_attrs == 0 ? nullptr : attrs.data();
diff --git a/src/runtime/metadata.h b/src/runtime/metadata.h
index 0a8d96261e..ce40f81570 100644
--- a/src/runtime/metadata.h
+++ b/src/runtime/metadata.h
@@ -54,6 +54,8 @@ constexpr const char* kUseDynamicSharedMemoryTag = 
"tirx.use_dyn_shared_memory";
 constexpr const char* kUseProgramaticDependentLaunch = 
"tirx.use_programtic_dependent_launch";
 /*! \brief A tag to specify whether or not use cooperative launch */
 constexpr const char* kUseCooperativeLaunch = "tirx.use_cooperative_launch";
+/*! \brief A tag to launch with CUDA's statically required block dimension */
+constexpr const char* kUseRequiredBlockDimension = 
"tirx.use_required_block_dimension";
 
 }  // namespace launch_param
 
diff --git a/src/runtime/thread_storage_scope.h 
b/src/runtime/thread_storage_scope.h
index 0443fc68a0..92be05a395 100644
--- a/src/runtime/thread_storage_scope.h
+++ b/src/runtime/thread_storage_scope.h
@@ -296,6 +296,8 @@ class LaunchParamConfig {
         use_programmatic_dependent_launch_ = true;
       } else if (tag == launch_param::kUseCooperativeLaunch) {
         use_cooperative_launch_ = true;
+      } else if (tag == launch_param::kUseRequiredBlockDimension) {
+        use_required_block_dimension_ = true;
       } else {
         ThreadScope ts = ThreadScope::Create(tag);
         if (ts.IsClusterCtaIdx()) {
@@ -337,6 +339,8 @@ class LaunchParamConfig {
 
   bool use_cooperative_launch() const { return use_cooperative_launch_; }
 
+  bool use_required_block_dimension() const { return 
use_required_block_dimension_; }
+
   bool use_cluster_launch() const { return use_cluster_launch_; }
 
  private:
@@ -352,6 +356,8 @@ class LaunchParamConfig {
   bool use_programmatic_dependent_launch_{false};
   /*! \brief Whether or not use cooperative launch. */
   bool use_cooperative_launch_{false};
+  /*! \brief Whether CUDA should use the kernel's statically required block 
dimension. */
+  bool use_required_block_dimension_{false};
   /*! \brief Whether the kernel declares a cluster-to-CTA scope. */
   bool use_cluster_launch_{false};
 };
diff --git a/src/tirx/transform/split_host_device.cc 
b/src/tirx/transform/split_host_device.cc
index b46650f864..afb98b1690 100644
--- a/src/tirx/transform/split_host_device.cc
+++ b/src/tirx/transform/split_host_device.cc
@@ -91,6 +91,7 @@ class LaunchBoundsAttrExtractor : public StmtMutator {
     min_blocks_per_sm_.reset();
     max_blocks_per_cluster_.reset();
     max_registers_.reset();
+    required_block_size_.reset();
     Stmt result = operator()(std::move(stmt));
     TVM_FFI_ICHECK(!max_blocks_per_cluster_.has_value() || 
min_blocks_per_sm_.has_value())
         << tirx::attr::kLaunchBoundsMaxBlocksPerCluster << " requires "
@@ -98,12 +99,18 @@ class LaunchBoundsAttrExtractor : public StmtMutator {
     TVM_FFI_ICHECK(!max_registers_.has_value() ||
                    (!min_blocks_per_sm_.has_value() && 
!max_blocks_per_cluster_.has_value()))
         << tirx::attr::kMaxRegisters << " cannot be combined with CUDA launch 
bounds";
+    TVM_FFI_ICHECK(!required_block_size_.has_value() ||
+                   (!min_blocks_per_sm_.has_value() && 
!max_blocks_per_cluster_.has_value() &&
+                    !max_registers_.has_value()))
+        << tirx::attr::kRequiredBlockSize
+        << " cannot be combined with CUDA launch bounds or maximum registers";
     return result;
   }
 
   std::optional<int64_t> min_blocks_per_sm() const { return 
min_blocks_per_sm_; }
   std::optional<int64_t> max_blocks_per_cluster() const { return 
max_blocks_per_cluster_; }
   std::optional<int64_t> max_registers() const { return max_registers_; }
+  std::optional<int64_t> required_block_size() const { return 
required_block_size_; }
 
  private:
   Stmt VisitStmt_(const AttrStmtNode* op) final {
@@ -142,6 +149,18 @@ class LaunchBoundsAttrExtractor : public StmtMutator {
       }
       max_registers_ = max_registers->value;
       return VisitStmt(op->body);
+    } else if (op->attr_key == tirx::attr::kRequiredBlockSize) {
+      const auto* required_block_size = op->value.as<IntImmNode>();
+      TVM_FFI_ICHECK(required_block_size)
+          << tirx::attr::kRequiredBlockSize << " expects an integer value";
+      TVM_FFI_ICHECK_EQ(required_block_size->value, 1)
+          << tirx::attr::kRequiredBlockSize << " must be 1";
+      if (required_block_size_.has_value()) {
+        TVM_FFI_ICHECK_EQ(required_block_size_.value(), 
required_block_size->value)
+            << "Conflicting " << tirx::attr::kRequiredBlockSize << " values";
+      }
+      required_block_size_ = required_block_size->value;
+      return VisitStmt(op->body);
     }
     return StmtMutator::VisitStmt_(op);
   }
@@ -149,6 +168,7 @@ class LaunchBoundsAttrExtractor : public StmtMutator {
   std::optional<int64_t> min_blocks_per_sm_;
   std::optional<int64_t> max_blocks_per_cluster_;
   std::optional<int64_t> max_registers_;
+  std::optional<int64_t> required_block_size_;
 };
 
 class HostDeviceSplitter : public StmtMutator {
@@ -276,6 +296,10 @@ class HostDeviceSplitter : public StmtMutator {
         device_func = WithAttr(std::move(device_func), 
tirx::attr::kMaxRegisters,
                                launch_bounds_attr.max_registers().value());
       }
+      if (launch_bounds_attr.required_block_size().has_value()) {
+        device_func = WithAttr(std::move(device_func), 
tirx::attr::kRequiredBlockSize,
+                               
launch_bounds_attr.required_block_size().value());
+      }
     }
     auto num_inputs = cur_func_->GetAttr<int64_t>(tvm::attr::kNumInputs);
     if (num_inputs.has_value()) {
@@ -363,6 +387,8 @@ class DeviceInfoCollector : public StmtVisitor {
         }
       }
     }
+    collector.use_required_block_dimension_ =
+        func->GetAttr<int64_t>(tirx::attr::kRequiredBlockSize).value_or(0) == 
1;
 
     collector(func->body);
 
@@ -373,6 +399,10 @@ class DeviceInfoCollector : public StmtVisitor {
     if (collector.use_cooperative_launch_) {
       
collector.info_.launch_params.push_back(tvm::runtime::launch_param::kUseCooperativeLaunch);
     }
+    if (collector.use_required_block_dimension_) {
+      collector.info_.launch_params.push_back(
+          tvm::runtime::launch_param::kUseRequiredBlockDimension);
+    }
     // The dynamic shared memory is required to be the last of the kernel
     // launch parameters. An explicit tirx.dyn_smem_bytes declaration wins;
     // otherwise fall back to the size inferred from the allocation extent.
@@ -397,7 +427,8 @@ class DeviceInfoCollector : public StmtVisitor {
 
     for (const ffi::String& param : collector.info_.launch_params) {
       if (param == tvm::runtime::launch_param::kUseProgramaticDependentLaunch 
||
-          param == tvm::runtime::launch_param::kUseCooperativeLaunch) {
+          param == tvm::runtime::launch_param::kUseCooperativeLaunch ||
+          param == tvm::runtime::launch_param::kUseRequiredBlockDimension) {
         continue;
       }
       collector.info_.launch_args.push_back(collector.GetArgument(param));
@@ -517,6 +548,7 @@ class DeviceInfoCollector : public StmtVisitor {
   // Flag-only launch attributes requested by the original PrimFunc.
   bool use_programmatic_dependent_launch_{false};
   bool use_cooperative_launch_{false};
+  bool use_required_block_dimension_{false};
   // Accumulated Bind definitions for inlining into extent/size expressions.
   ffi::Map<Var, PrimExpr> bind_map_;
 };
diff --git 
a/tests/python/tirx-transform/test_tir_transform_split_host_device.py 
b/tests/python/tirx-transform/test_tir_transform_split_host_device.py
index d04f2338a1..ddc11bb1d4 100644
--- a/tests/python/tirx-transform/test_tir_transform_split_host_device.py
+++ b/tests/python/tirx-transform/test_tir_transform_split_host_device.py
@@ -437,6 +437,36 @@ def test_cuda_launch_preserves_flag_metadata():
     assert int(launch.args[-1]) == 16
 
 
+def test_cuda_required_block_size_becomes_flag_only_launch_metadata():
+    @I.ir_module
+    class Before:
+        @T.prim_func(s_tir=True)
+        def main(A: T.Buffer(4, "float32")):
+            T.func_attr({"target": T.target("cuda", host="llvm")})
+            T.attr(T.target("cuda"), "target", 0)
+            T.attr(0, "tirx.required_block_size", 1)
+            bx = T.launch_thread("blockIdx.x", 4)
+            tx = T.launch_thread("threadIdx.x", 128)
+            if tx == 0:
+                A[bx] = 0.0
+
+    after = tvm.tirx.transform.SplitHostDevice()(Before)
+    kernel = after["main_kernel"]
+    assert int(kernel.attrs["tirx.required_block_size"]) == 1
+    assert list(kernel.attrs["tirx.kernel_launch_params"]) == [
+        "blockIdx.x",
+        "threadIdx.x",
+        "tirx.use_required_block_dimension",
+    ]
+
+    launch = after["main"].body.value
+    assert isinstance(launch, tvm.ir.Call)
+    # The required-block flag reaches FunctionInfo metadata but adds no packed 
operand.
+    assert len(launch.args) == 4
+    assert int(launch.args[-2]) == 4
+    assert int(launch.args[-1]) == 128
+
+
 def test_cuda_launch_preserves_singleton_cluster_dimensions():
     @I.ir_module
     class Before:
diff --git a/tests/python/tirx/codegen/test_codegen_cuda.py 
b/tests/python/tirx/codegen/test_codegen_cuda.py
index f9b47645e8..1a79db2b63 100644
--- a/tests/python/tirx/codegen/test_codegen_cuda.py
+++ b/tests/python/tirx/codegen/test_codegen_cuda.py
@@ -270,6 +270,45 @@ def test_tirx_max_registers_rejects_launch_bounds():
         _get_source(main)
 
 
+def test_tirx_required_block_size_emits_cuda_block_size():
+    @T.prim_func
+    def main(A: T.Buffer((8,), "int32")):
+        T.device_entry()
+        T.attr({"tirx.required_block_size": 1})
+        bx, by = T.cta_id([4, 2])
+        _, cy = T.cta_id_in_cluster([1, 2])
+        tx = T.thread_id([128])
+        if tx == 0:
+            A[bx * 2 + by] = cy
+
+    src, _ = _get_source(main)
+    assert 'extern "C" __global__ void __block_size__((128, 1, 1), (1, 2, 1)) 
main_kernel' in src
+    assert "__launch_bounds__" not in src
+    assert "tirx.required_block_size" not in src
+
+
+def test_tirx_required_block_size_rejects_launch_controls():
+    @T.prim_func
+    def main(A: T.Buffer((4,), "int32")):
+        T.device_entry()
+        T.attr(
+            {
+                "tirx.required_block_size": 1,
+                "tirx.launch_bounds_min_blocks_per_sm": 1,
+            }
+        )
+        bx = T.cta_id([4])
+        tx = T.thread_id([128])
+        if tx == 0:
+            A[bx] = A[bx] + 1
+
+    with pytest.raises(
+        tvm.error.InternalError,
+        match="cannot be combined with CUDA launch bounds or maximum 
registers",
+    ):
+        _get_source(main)
+
+
 def test_tirx_cuda_kernel_return_zero_codegen_is_void_early_return():
     @T.prim_func
     def main(A: T.Buffer((4,), "int32")):

Reply via email to