This is an automated email from the ASF dual-hosted git repository.
spectrometerHBH 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 f0ea2d43b8 [TIRx][CUDA] Preserve explicit single-CTA cluster launches
(#20180)
f0ea2d43b8 is described below
commit f0ea2d43b8d6b2b6b14bcbdf725fc0165eb7c541
Author: Bohan Hou <[email protected]>
AuthorDate: Tue Aug 25 16:57:37 2026 -0400
[TIRx][CUDA] Preserve explicit single-CTA cluster launches (#20180)
## Motivation
Calling `K.cta_id_in_cluster(...)` is an explicit request for CUDA
cluster-launch semantics, including when every cluster extent is one.
Lowering previously removed extent-one `clusterCtaIdx.*` bindings, and
the CUDA runtime inferred cluster presence only from dimensions greater
than one. As a result, an explicit `(1, 1, 1)` cluster launch became
indistinguishable from an ordinary CTA launch.
## Changes
- Preserve `clusterCtaIdx.*` launch bindings whenever cluster CTA scope
is present, including extent-one bindings.
- Derive a `use_cluster_launch` flag from regular cluster launch-tag
presence in `LaunchParamConfig`.
- Emit `CU_LAUNCH_ATTRIBUTE_CLUSTER_DIMENSION` whenever that flag is
set, including `(1, 1, 1)`.
- Keep the missing-`clusterCtaIdx` fallback to constant zero for
compatibility with pre-existing IR.
- Add lowering, `SplitHostDevice`, and C++ launch-parameter coverage for
ordinary, singleton, multi-CTA, and preferred-cluster cases.
This does not change serialized module metadata. Preferred-cluster tags
alone do not request a regular cluster launch.
## Launch behavior
NCU 2026.1 measurements on NVIDIA B200:
| Probe | Cluster dimensions | Cluster Size | Uses Blocks as Clusters |
|---|---:|---:|---:|
| Ordinary CTA | `(0, 0, 0)` | 0 | 0 |
| Explicit singleton | `(1, 1, 1)` | 1 | 0 |
| Explicit multi-CTA | `(2, 1, 1)` | 2 | 0 |
## Validation
- TVM build completed successfully.
- LowerTIRx cluster/preferred-cluster tests: 6 passed.
- SplitHostDevice launch tests: 6 passed.
- `LaunchParamConfig` C++ test: 1 passed.
- Relevant CUDA cluster codegen tests: 2 passed.
- Downstream affected correctness matrix: 278/278 passed without skips:
- amax: 59
- fast top-k: 34
- dense SwiGLU: 75
- interleaved quant: 58
- grouped dGLU: 52
- Staged-file pre-commit checks passed.
Downstream B200 benchmarks used Proton timing, five independent samples
per implementation, one-second cooldowns, reference implementations
enabled, interference monitoring, and no sample splicing:
| Matrix | Passing rows | Minimum `mean(reference) / mean(tirx)` |
|---|---:|---:|
| Targeted affected-family gate | 15/15 | 0.996504 |
| Full amax matrix | 88/88 | 0.997440 |
| Full fast top-k matrix | 34/34 | 1.006541 |
The maximum patched-versus-main TIRx slowdown in the targeted comparison
was 0.911%, below the 1% investigation threshold. Six externally
interfered amax attempts were recorded and replaced only by complete
five-round row reruns.
A full TIRx suite run was also performed. The strict kernel registry
passed. The broad parallel run encountered unrelated
environment/reference failures from cross-device DLPack state, aggregate
GPU memory pressure, a missing optional `cutlass.experimental` package,
and reference PTX incompatibility. An isolated core rerun produced 2631
passed, 73 skipped, 3 xpassed, and one pre-existing stale packed-dtype
error-message assertion; no affected cluster correctness configuration
failed.
---
src/backend/cuda/runtime/cuda_module.cc | 2 +-
src/runtime/thread_storage_scope.h | 8 ++
src/tirx/ir/exec_scope.cc | 7 +-
src/tirx/transform/tile_primitive_dispatch.cc | 2 +-
tests/cpp/runtime/thread_storage_scope_test.cc | 42 +++++++++
.../test_tir_transform_split_host_device.py | 29 ++++++
.../tirx/transform/test_transform_lower_tirx.py | 103 +++++++++++++++++++++
7 files changed, 187 insertions(+), 6 deletions(-)
diff --git a/src/backend/cuda/runtime/cuda_module.cc
b/src/backend/cuda/runtime/cuda_module.cc
index 0d210ac0da..604a7de33c 100644
--- a/src/backend/cuda/runtime/cuda_module.cc
+++ b/src/backend/cuda/runtime/cuda_module.cc
@@ -246,7 +246,7 @@ class CUDAWrappedFunc {
unsigned int num_attrs = 0;
// 1) Cluster
- if (wl.cluster_dim(0) != 1 || wl.cluster_dim(1) != 1 || wl.cluster_dim(2)
!= 1) {
+ if (launch_param_config_.use_cluster_launch()) {
CUlaunchAttribute attr{};
attr.id = CU_LAUNCH_ATTRIBUTE_CLUSTER_DIMENSION;
attr.value.clusterDim.x = wl.cluster_dim(0);
diff --git a/src/runtime/thread_storage_scope.h
b/src/runtime/thread_storage_scope.h
index c4c3b50cfb..0443fc68a0 100644
--- a/src/runtime/thread_storage_scope.h
+++ b/src/runtime/thread_storage_scope.h
@@ -284,6 +284,7 @@ class LaunchParamConfig {
public:
void Init(size_t base, const ffi::Array<ffi::String>& launch_param_tags) {
base_ = base;
+ use_cluster_launch_ = false;
std::vector<bool> filled(12, false);
for (size_t i = 0; i < launch_param_tags.size(); ++i) {
std::string tag(launch_param_tags[i]);
@@ -297,6 +298,9 @@ class LaunchParamConfig {
use_cooperative_launch_ = true;
} else {
ThreadScope ts = ThreadScope::Create(tag);
+ if (ts.IsClusterCtaIdx()) {
+ use_cluster_launch_ = true;
+ }
arg_index_map_.push_back(ts.rank * 3 + ts.dim_index);
filled[ts.rank * 3 + ts.dim_index] = true;
}
@@ -333,6 +337,8 @@ class LaunchParamConfig {
bool use_cooperative_launch() const { return use_cooperative_launch_; }
+ bool use_cluster_launch() const { return use_cluster_launch_; }
+
private:
/*! \brief base axis */
size_t base_;
@@ -346,6 +352,8 @@ class LaunchParamConfig {
bool use_programmatic_dependent_launch_{false};
/*! \brief Whether or not use cooperative launch. */
bool use_cooperative_launch_{false};
+ /*! \brief Whether the kernel declares a cluster-to-CTA scope. */
+ bool use_cluster_launch_{false};
};
} // namespace runtime
diff --git a/src/tirx/ir/exec_scope.cc b/src/tirx/ir/exec_scope.cc
index b3a5b8d4ba..47a2198fdc 100644
--- a/src/tirx/ir/exec_scope.cc
+++ b/src/tirx/ir/exec_scope.cc
@@ -381,10 +381,9 @@ ffi::Array<PrimExpr> ResolveCuda(ScopeBinding binding,
case ScopeBinding::kKernelCta:
return Trivial3DResolve(params, "blockIdx.", out_dim);
case ScopeBinding::kClusterCta:
- // A launch whose cluster is a single CTA binds no clusterCtaIdx var; the
- // coordinate is then the constant 0, which is what GetThread returns
for a
- // missing tag. blockIdx and threadIdx are always bound, so they keep the
- // strict lookup.
+ // Keep the missing-tag fallback for compatibility with pre-existing IR.
A missing
+ // clusterCtaIdx coordinate resolves to the constant zero; blockIdx and
threadIdx are
+ // always bound, so they keep the strict lookup.
return Trivial3DResolve(params, "clusterCtaIdx.", out_dim,
/*allow_missing=*/true);
case ScopeBinding::kCtaThread:
return Trivial3DResolve(params, "threadIdx.", out_dim);
diff --git a/src/tirx/transform/tile_primitive_dispatch.cc
b/src/tirx/transform/tile_primitive_dispatch.cc
index 940c0d5004..c36b7d9f8d 100644
--- a/src/tirx/transform/tile_primitive_dispatch.cc
+++ b/src/tirx/transform/tile_primitive_dispatch.cc
@@ -776,7 +776,7 @@ class TilePrimitiveDispatcher : public StmtExprMutator {
}
};
auto cluster_cta_it = id_set.find(ScopeBinding::kClusterCta);
- if (cluster_cta_it == id_set.end() ||
is_one((*cluster_cta_it).second.fused_extent())) {
+ if (cluster_cta_it == id_set.end()) {
// no cluster
add_launch_param(ScopeBinding::kKernelCta, "blockIdx.");
} else {
diff --git a/tests/cpp/runtime/thread_storage_scope_test.cc
b/tests/cpp/runtime/thread_storage_scope_test.cc
new file mode 100644
index 0000000000..50579cf286
--- /dev/null
+++ b/tests/cpp/runtime/thread_storage_scope_test.cc
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "../../../src/runtime/thread_storage_scope.h"
+
+#include <gtest/gtest.h>
+
+namespace tvm {
+namespace runtime {
+namespace {
+
+TEST(LaunchParamConfigTest, DerivesClusterLaunchFromTagPresence) {
+ LaunchParamConfig config;
+ config.Init(0, {"blockIdx.x", "threadIdx.x"});
+ EXPECT_FALSE(config.use_cluster_launch());
+
+ config.Init(0, {"blockIdx.x", "clusterCtaIdx.x", "threadIdx.x"});
+ EXPECT_TRUE(config.use_cluster_launch());
+
+ config.Init(0, {"blockIdx.x", "preferredClusterCtaIdx.x", "threadIdx.x"});
+ EXPECT_FALSE(config.use_cluster_launch());
+}
+
+} // namespace
+} // namespace runtime
+} // namespace tvm
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 5379b2e3b5..d04f2338a1 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,35 @@ def test_cuda_launch_preserves_flag_metadata():
assert int(launch.args[-1]) == 16
+def test_cuda_launch_preserves_singleton_cluster_dimensions():
+ @I.ir_module
+ class Before:
+ @T.prim_func(s_tir=True)
+ def main(A: T.Buffer(1, "float32")):
+ T.func_attr({"target": T.target("cuda", host="llvm")})
+ with T.attr(T.target("cuda"), "target", 0):
+ T.launch_thread("blockIdx.x", 4)
+ T.launch_thread("clusterCtaIdx.x", 1)
+ T.launch_thread("clusterCtaIdx.y", 1)
+ T.launch_thread("clusterCtaIdx.z", 1)
+ T.launch_thread("threadIdx.x", 32)
+ A[0] = 0.0
+
+ after = tvm.tirx.transform.SplitHostDevice()(Before)
+ kernel = after["main_kernel"]
+ assert list(kernel.attrs["tirx.kernel_launch_params"]) == [
+ "blockIdx.x",
+ "clusterCtaIdx.x",
+ "clusterCtaIdx.y",
+ "clusterCtaIdx.z",
+ "threadIdx.x",
+ ]
+
+ launch = after["main"].body.value
+ assert isinstance(launch, tvm.ir.Call)
+ assert [int(arg) for arg in launch.args[-5:]] == [4, 1, 1, 1, 32]
+
+
def test_device_scope_region_extracted_as_device_kernel():
"""A bare device_scope is annotated and extracted as a device kernel."""
diff --git a/tests/python/tirx/transform/test_transform_lower_tirx.py
b/tests/python/tirx/transform/test_transform_lower_tirx.py
index 86f6fb0b29..9f9c254853 100644
--- a/tests/python/tirx/transform/test_transform_lower_tirx.py
+++ b/tests/python/tirx/transform/test_transform_lower_tirx.py
@@ -48,6 +48,17 @@ def _int_triple(side, axis):
return tuple(int(x) for x in side[axis])
+def _launch_thread_extents(func):
+ extents = {}
+
+ def collect(node):
+ if isinstance(node, tvm.tirx.AttrStmt) and node.attr_key ==
"thread_extent":
+ extents[str(node.node.thread_tag)] = int(node.value)
+
+ tvm.tirx.stmt_functor.post_order_visit(func.body, collect)
+ return extents
+
+
L_LANE = T.TileLayout(T.S[32 : 1 @ laneid])
@@ -414,6 +425,98 @@ def test_lower_scope_id():
compare(before3, after3, LowerTIRx)
+def test_lower_ordinary_cta_has_no_cluster_launch_tags():
+ @T.prim_func(private=True)
+ def before() -> None:
+ T.device_entry()
+ T.cta_id([1])
+ T.thread_id([32])
+
+ with tvm.target.Target("cuda"):
+ after = LowerTIRx()(tvm.IRModule({"main": before}))["main"]
+
+ launch_extents = _launch_thread_extents(after)
+ assert launch_extents == {"blockIdx.x": 1, "threadIdx.x": 32}
+
+
+def test_lower_explicit_singleton_cluster_launch_tags_survive_when_unused():
+ @T.prim_func(private=True)
+ def cluster_2d() -> None:
+ T.device_entry()
+ unused_cbx, unused_cby = T.cta_id_in_cluster([1, 1])
+ unused_bx, unused_by = T.cta_id([1, 1])
+ T.thread_id([32])
+
+ @T.prim_func(private=True)
+ def cluster_3d() -> None:
+ T.device_entry()
+ unused_cbx, unused_cby, unused_cbz = T.cta_id_in_cluster([1, 1, 1])
+ unused_bx, unused_by, unused_bz = T.cta_id([1, 1, 1])
+ T.thread_id([32])
+
+ with tvm.target.Target("cuda"):
+ after = LowerTIRx()(tvm.IRModule({"cluster_2d": cluster_2d,
"cluster_3d": cluster_3d}))
+
+ assert _launch_thread_extents(after["cluster_2d"]) == {
+ "blockIdx.x": 1,
+ "blockIdx.y": 1,
+ "clusterCtaIdx.x": 1,
+ "clusterCtaIdx.y": 1,
+ "threadIdx.x": 32,
+ }
+ assert _launch_thread_extents(after["cluster_3d"]) == {
+ "blockIdx.x": 1,
+ "blockIdx.y": 1,
+ "blockIdx.z": 1,
+ "clusterCtaIdx.x": 1,
+ "clusterCtaIdx.y": 1,
+ "clusterCtaIdx.z": 1,
+ "threadIdx.x": 32,
+ }
+
+
+def test_lower_multi_cta_cluster_launch_tags_remain_unchanged():
+ @T.prim_func(private=True)
+ def before() -> None:
+ T.device_entry()
+ unused_cbx, unused_cby = T.cta_id_in_cluster([2, 1])
+ unused_bx, unused_by = T.cta_id([2, 1])
+ T.thread_id([32])
+
+ with tvm.target.Target("cuda"):
+ after = LowerTIRx()(tvm.IRModule({"main": before}))["main"]
+
+ assert _launch_thread_extents(after) == {
+ "blockIdx.x": 2,
+ "blockIdx.y": 1,
+ "clusterCtaIdx.x": 2,
+ "clusterCtaIdx.y": 1,
+ "threadIdx.x": 32,
+ }
+
+
+def test_lower_singleton_cluster_preserves_preferred_cluster_tags():
+ @T.prim_func(private=True)
+ def before() -> None:
+ T.device_entry()
+ unused_cbx, unused_cby = T.cta_id_in_cluster([1, 1], preferred=[2, 2])
+ unused_bx, unused_by = T.cta_id([1, 1])
+ T.thread_id([32])
+
+ with tvm.target.Target("cuda"):
+ after = LowerTIRx()(tvm.IRModule({"main": before}))["main"]
+
+ assert _launch_thread_extents(after) == {
+ "blockIdx.x": 1,
+ "blockIdx.y": 1,
+ "clusterCtaIdx.x": 1,
+ "clusterCtaIdx.y": 1,
+ "preferredClusterCtaIdx.x": 2,
+ "preferredClusterCtaIdx.y": 2,
+ "threadIdx.x": 32,
+ }
+
+
def test_lower_scope_id2():
@T.inline
def func(warp_id, tx):