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 ea0cfa320f [TIRx][CUDA] Preserve kernel launch calls and add CUDA host
source bundling (#20395)
ea0cfa320f is described below
commit ea0cfa320fcdea0567e2f4c19d363625c66630d1
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Sep 20 22:10:32 2026 -0400
[TIRx][CUDA] Preserve kernel launch calls and add CUDA host source bundling
(#20395)
Preserve kernel launch metadata through `tirx.call_ffi_kernel`. Ordinary
host targets lower these calls through packed functions, while
`cuda_host` emits direct CUDA launches with explicit diagnostics for
unsupported launch modes.
Add `tvm.backend.cuda.export_cuda_host(mod)` to concatenate retained
CUDA device source before its host wrappers into one translation unit.
Existing CUDA device code generation stays unchanged; source generation,
compilation and loading remain separate steps.
CUDA-host wrappers use tvm-ffi and CUDA libraries for streams, scoped
device selection, tensor-map encoding and error handling. Tensor-map
descriptors use aligned host storage and the device's by-value launch
ABI. Unsupported TVM runtime services are diagnosed during code
generation.
Tensor-map initialization uses `tirx.tensormap_encode_tiled` to retain
the final descriptor dtype and fixed options in call attributes, with
pointers and dimensions as runtime operands. CUDA-host codegen emits
typed arrays and a direct driver call; ordinary hosts keep the existing
packed encoder ABI.
---
include/tvm/tirx/attrs.h | 73 ++++
include/tvm/tirx/builtin.h | 18 +
python/tvm/backend/cuda/__init__.py | 3 +
python/tvm/backend/cuda/host.py | 78 ++++
.../backend/cuda/tile_primitive/copy_async/tma.py | 17 +-
python/tvm/tirx/__init__.py | 1 +
python/tvm/tirx/build.py | 2 +-
python/tvm/tirx/op.py | 84 +++++
python/tvm/tirx/script/builder/ir.py | 4 +
src/backend/cuda/codegen/codegen_cuda_host.cc | 401 +++++++++++++++++++++
src/backend/cuda/codegen/target_kind.cc | 2 +
src/tirx/op/builtin.cc | 32 ++
src/tirx/script/printer/expr.cc | 27 ++
src/tirx/transform/lower_tirx_dedup_tensormap.cc | 50 ++-
src/tirx/transform/lower_tvm_builtin.cc | 35 +-
src/tirx/transform/split_host_device.cc | 6 +-
tests/python/codegen/test_target_codegen_cuda.py | 41 +++
.../test_tir_transform_lower_tvm_builtin.py | 10 +-
.../test_tir_transform_split_host_device.py | 23 +-
.../tile_primitive/cuda/copy_async/test_tma.py | 32 +-
20 files changed, 868 insertions(+), 71 deletions(-)
diff --git a/include/tvm/tirx/attrs.h b/include/tvm/tirx/attrs.h
new file mode 100644
index 0000000000..1d173b3561
--- /dev/null
+++ b/include/tvm/tirx/attrs.h
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+#ifndef TVM_TIRX_ATTRS_H_
+#define TVM_TIRX_ATTRS_H_
+
+#include <tvm/ffi/container/array.h>
+#include <tvm/ir/attrs.h>
+
+namespace tvm {
+namespace tirx {
+
+/*! \brief Launch metadata for call_ffi_kernel. */
+struct CallFFIKernelAttr : public AttrsNode {
+ /*!
+ * \brief Ordered launch tags describing the suffix of the call arguments.
+ *
+ * The first call argument is the kernel symbol, followed by kernel operands
+ * and launch values. Flag-only tags consume no value; dynamic shared-memory
+ * bytes, when present, are last. Runtime expressions remain in Call.args.
+ */
+ ffi::Array<ffi::String> launch_params;
+
+ static void RegisterReflection() {
+ ffi::reflection::ObjectDef<CallFFIKernelAttr>().def_ro("launch_params",
+
&CallFFIKernelAttr::launch_params);
+ }
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("tirx.CallFFIKernelAttr",
CallFFIKernelAttr, AttrsNode);
+};
+
+/*! \brief Fixed encoding options for tensormap_encode_tiled. */
+struct TensorMapEncodeTiledAttr : public AttrsNode {
+ DLDataType descriptor_dtype;
+ int64_t rank;
+ int64_t interleave;
+ int64_t swizzle;
+ int64_t l2_promotion;
+ int64_t oob_fill;
+ int64_t force_cu_dtype;
+
+ static void RegisterReflection() {
+ ffi::reflection::ObjectDef<TensorMapEncodeTiledAttr>()
+ .def_ro("descriptor_dtype",
&TensorMapEncodeTiledAttr::descriptor_dtype)
+ .def_ro("rank", &TensorMapEncodeTiledAttr::rank)
+ .def_ro("interleave", &TensorMapEncodeTiledAttr::interleave)
+ .def_ro("swizzle", &TensorMapEncodeTiledAttr::swizzle)
+ .def_ro("l2_promotion", &TensorMapEncodeTiledAttr::l2_promotion)
+ .def_ro("oob_fill", &TensorMapEncodeTiledAttr::oob_fill)
+ .def_ro("force_cu_dtype", &TensorMapEncodeTiledAttr::force_cu_dtype);
+ }
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("tirx.TensorMapEncodeTiledAttr",
TensorMapEncodeTiledAttr,
+ AttrsNode);
+};
+
+} // namespace tirx
+} // namespace tvm
+
+#endif // TVM_TIRX_ATTRS_H_
diff --git a/include/tvm/tirx/builtin.h b/include/tvm/tirx/builtin.h
index 6b8939389c..4f66ab328a 100644
--- a/include/tvm/tirx/builtin.h
+++ b/include/tvm/tirx/builtin.h
@@ -349,6 +349,24 @@ TVM_DLL const Op& tvm_stack_make_array();
*/
TVM_DLL const Op& tvm_call_packed();
+/*!
+ * \brief Launch a kernel using the packed-function argument convention.
+ *
+ * Arguments are the kernel symbol, kernel operands, then launch values.
+ * CallFFIKernelAttr::launch_params describes the launch-value suffix.
+ * Host backends may consume this call directly or lower it to tvm_call_packed.
+ */
+TVM_DLL const Op& call_ffi_kernel();
+
+/*!
+ * \brief Encode a tiled tensor map at invocation time.
+ *
+ * TensorMapEncodeTiledAttr stores the descriptor dtype, rank and fixed
options.
+ * Arguments are descriptor and data pointers, global dimensions (rank), byte
+ * strides (rank - 1), box dimensions (rank), then element strides (rank).
+ */
+TVM_DLL const Op& tensormap_encode_tiled();
+
/*!
* \brief See pesudo code
*
diff --git a/python/tvm/backend/cuda/__init__.py
b/python/tvm/backend/cuda/__init__.py
index 89f0b9b71e..1b334c6f45 100644
--- a/python/tvm/backend/cuda/__init__.py
+++ b/python/tvm/backend/cuda/__init__.py
@@ -23,6 +23,8 @@ from tvm_ffi.libinfo import load_lib_ctypes
from tvm.base import _LOADED_LIBS
+from .host import export_cuda_host
+
_LAZY_SUBMODULES = {
"codegen",
"cpp",
@@ -112,6 +114,7 @@ def __getattr__(name: str):
__all__ = [
"codegen",
"cpp",
+ "export_cuda_host",
"iket",
"lang",
"op",
diff --git a/python/tvm/backend/cuda/host.py b/python/tvm/backend/cuda/host.py
new file mode 100644
index 0000000000..65e31dc16f
--- /dev/null
+++ b/python/tvm/backend/cuda/host.py
@@ -0,0 +1,78 @@
+# 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.
+"""CUDA host and device source assembly."""
+
+from tvm_ffi import Module
+
+
+def export_cuda_host(mod: Module) -> str:
+ """Return one CUDA C++ translation unit from a built CUDA-host module.
+
+ Build ``mod`` with a CUDA target whose host is ``"cuda_host"``. Its CUDA
+ device imports must retain their CUDA C++ source. Device definitions are
+ emitted before the host wrappers that launch them. This function only
+ reads the modules and returns source; compilation, loading and writing
+ files are left to the caller. Sources are concatenated without rewriting
+ declarations or deduplicating helpers, so the imports must be compatible
+ within one translation unit and provide the types needed by NVCC's host
pass.
+ Generated host wrappers use tvm-ffi and CUDA libraries. TVM runtime
workspace
+ allocation and other unsupported runtime services are rejected during
codegen.
+ Tensor-map encoding additionally requires linking the CUDA driver library.
+
+ Parameters
+ ----------
+ mod : tvm.runtime.Module
+ The built CUDA-host source module with its CUDA device imports.
+
+ Returns
+ -------
+ source : str
+ CUDA C++ source suitable for compilation with NVCC and tvm-ffi headers.
+
+ Raises
+ ------
+ ValueError
+ If the host or its imports are incompatible, or CUDA source is absent
+ (for example, after loading a device module saved as a binary).
+ """
+ if not isinstance(mod, Module):
+ raise TypeError("export_cuda_host expects a runtime Module")
+ if mod.kind != "c" or "cu" not in mod.get_write_formats():
+ raise ValueError("Expected a source module built with
host='cuda_host'")
+ host_source = mod.inspect_source()
+ if not host_source:
+ raise ValueError("CUDA-host module has no source")
+
+ sources = []
+ visited = set()
+
+ def collect(device_mod):
+ if device_mod in visited:
+ return
+ visited.add(device_mod)
+ if device_mod.kind != "cuda":
+ raise ValueError(f"Expected a CUDA device import, got
{device_mod.kind!r}")
+ source = device_mod.inspect_source("cuda")
+ if not source:
+ raise ValueError("CUDA device import has no CUDA C++ source to
bundle")
+ for imported in device_mod.imports:
+ collect(imported)
+ sources.append(source)
+
+ for imported in mod.imports:
+ collect(imported)
+ return "\n\n".join([*sources, host_source])
diff --git a/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
b/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
index 1f75023fb2..679681d4e5 100644
--- a/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
+++ b/python/tvm/backend/cuda/tile_primitive/copy_async/tma.py
@@ -2048,21 +2048,20 @@ def _get_or_encode_descriptor(spec: TensorMapSpec,
sctx: DispatchContext):
@T.prim_func(check_well_formed=False)
def create_tensor_map():
T.Bind(T.tvm_stack_alloca("tensormap", 1), var=tensor_map)
- T.call_packed(
- "runtime.cuTensorMapEncodeTiled",
+ T.tensormap_encode_tiled(
tensor_map,
- spec.descriptor_dtype,
- spec.rank,
spec.base,
*spec.global_dims,
*spec.global_strides,
*spec.box_dims,
*spec.element_strides,
- spec.interleave,
- spec.swizzle,
- spec.l2_promotion,
- spec.oob_fill,
- *([spec.force_cu_dtype] if spec.force_cu_dtype >= 0 else []),
+ descriptor_dtype=spec.descriptor_dtype,
+ rank=spec.rank,
+ interleave=spec.interleave,
+ swizzle=spec.swizzle,
+ l2_promotion=spec.l2_promotion,
+ oob_fill=spec.oob_fill,
+ force_cu_dtype=spec.force_cu_dtype,
)
T.tvm_kernel_replace_point()
# fmt: on
diff --git a/python/tvm/tirx/__init__.py b/python/tvm/tirx/__init__.py
index 500e938755..d0877c142a 100644
--- a/python/tvm/tirx/__init__.py
+++ b/python/tvm/tirx/__init__.py
@@ -61,6 +61,7 @@ from .function import PrimFunc, IndexMap
from .op import call_packed_lowered, call_cpacked_lowered,
register_intrin_lowering
from .op import call_packed, call_cpacked, call_intrin, call_pure_extern,
call_extern
+from .op import CallFFIKernelAttr, call_ffi_kernel, TensorMapEncodeTiledAttr,
tensormap_encode_tiled
from .op import call_llvm_intrin, call_llvm_pure_intrin, all, any, min_value,
max_value, trace
from .op import tvm_stack_alloca, tvm_stack_make_shape, tvm_stack_make_array
from .op import tvm_tuple, handle_add_byte_offset, tvm_struct_get,
tvm_struct_set
diff --git a/python/tvm/tirx/build.py b/python/tvm/tirx/build.py
index f15650a3ba..214da90ede 100644
--- a/python/tvm/tirx/build.py
+++ b/python/tvm/tirx/build.py
@@ -102,7 +102,7 @@ def split_host_device_mods(mod: IRModule) ->
tuple[IRModule, dict[Target, IRModu
def is_host_func(f):
target = f.attrs.get("target", tvm.target.Target("llvm"))
- return target.kind.name in ["llvm", "c"]
+ return target.kind.name in ["llvm", "c", "cuda_host"]
host_mod = tvm.tirx.transform.Filter(is_host_func)(mod)
device_mod = tvm.tirx.transform.Filter(lambda f: not is_host_func(f))(mod)
diff --git a/python/tvm/tirx/op.py b/python/tvm/tirx/op.py
index 291ec8720d..2369f9d5b5 100644
--- a/python/tvm/tirx/op.py
+++ b/python/tvm/tirx/op.py
@@ -255,6 +255,90 @@ def call_packed(*args, span=None):
return Call(Op.get("tirx.tvm_call_packed"), call_args, span=span,
ret_ty="int32")
+@tvm_ffi.register_object("tirx.CallFFIKernelAttr")
+class CallFFIKernelAttr(tvm.ir.Attrs):
+ """Ordered launch tags for an explicit FFI kernel call."""
+
+ launch_params: list[str]
+
+ def __init__(self, launch_params):
+ self.__init_handle_by_constructor__(_ffi_api.CallFFIKernelAttr,
launch_params)
+
+
+def call_ffi_kernel(*args, launch_params, ret_ty="int32", span=None):
+ """Call a kernel with its symbol, kernel operands, then launch values.
+
+ ``launch_params`` contains ordered tags for the launch-value suffix.
+ Flag-only tags consume no argument, and dynamic shared-memory bytes are
+ last when present. Host codegen may launch directly; other hosts use the
+ existing packed-function calling convention.
+ """
+ return Call(
+ "tirx.call_ffi_kernel",
+ args,
+ attrs=CallFFIKernelAttr(launch_params),
+ ret_ty=ret_ty,
+ span=span,
+ )
+
+
+@tvm_ffi.register_object("tirx.TensorMapEncodeTiledAttr")
+class TensorMapEncodeTiledAttr(tvm.ir.Attrs):
+ """Descriptor dtype and fixed options for tiled tensor-map encoding."""
+
+ def __init__(
+ self,
+ descriptor_dtype,
+ rank,
+ interleave=0,
+ swizzle=0,
+ l2_promotion=0,
+ oob_fill=0,
+ force_cu_dtype=-1,
+ ):
+ self.__init_handle_by_constructor__(
+ _ffi_api.TensorMapEncodeTiledAttr,
+ descriptor_dtype,
+ rank,
+ interleave,
+ swizzle,
+ l2_promotion,
+ oob_fill,
+ force_cu_dtype,
+ )
+
+
+def tensormap_encode_tiled(
+ *args,
+ descriptor_dtype,
+ rank,
+ interleave=0,
+ swizzle=0,
+ l2_promotion=0,
+ oob_fill=0,
+ force_cu_dtype=-1,
+ span=None,
+):
+ """Encode a tiled tensor map using runtime pointers and shape operands.
+
+ Arguments are the descriptor and data pointers, global dimensions (rank),
+ byte strides (rank - 1), box dimensions (rank), and element strides (rank).
+ The dtype describes the final descriptor units, including any promotion.
+ CUDA-host codegen encodes directly; other hosts use the runtime packed
call.
+ """
+ if not 1 <= rank <= 5 or len(args) != 4 * rank + 1:
+ raise ValueError("tensormap_encode_tiled requires rank 1..5 and 4 *
rank + 1 operands")
+ return Call(
+ "tirx.tensormap_encode_tiled",
+ args,
+ attrs=TensorMapEncodeTiledAttr(
+ descriptor_dtype, rank, interleave, swizzle, l2_promotion,
oob_fill, force_cu_dtype
+ ),
+ ret_ty="int32",
+ span=span,
+ )
+
+
def call_cpacked(*args, span=None):
"""Build expression by call an external packed function.
diff --git a/python/tvm/tirx/script/builder/ir.py
b/python/tvm/tirx/script/builder/ir.py
index 874e981a27..4364d5102d 100644
--- a/python/tvm/tirx/script/builder/ir.py
+++ b/python/tvm/tirx/script/builder/ir.py
@@ -3202,6 +3202,8 @@ tvm_stack_alloca = _op_wrapper(_tir_op.tvm_stack_alloca)
tvm_stack_make_shape = _op_wrapper(_tir_op.tvm_stack_make_shape)
tvm_stack_make_array = _op_wrapper(_tir_op.tvm_stack_make_array)
call_packed = _op_wrapper(_tir_op.call_packed)
+call_ffi_kernel = _op_wrapper(_tir_op.call_ffi_kernel)
+tensormap_encode_tiled = _op_wrapper(_tir_op.tensormap_encode_tiled)
call_cpacked = _op_wrapper(_tir_op.call_cpacked)
call_packed_lowered = _op_wrapper(_tir_op.call_packed_lowered)
call_cpacked_lowered = _op_wrapper(_tir_op.call_cpacked_lowered)
@@ -3520,6 +3522,8 @@ __all__ = [
"tvm_stack_make_shape",
"tvm_stack_make_array",
"call_packed",
+ "call_ffi_kernel",
+ "tensormap_encode_tiled",
"call_cpacked",
"call_packed_lowered",
"call_cpacked_lowered",
diff --git a/src/backend/cuda/codegen/codegen_cuda_host.cc
b/src/backend/cuda/codegen/codegen_cuda_host.cc
new file mode 100644
index 0000000000..5fb4aee7e5
--- /dev/null
+++ b/src/backend/cuda/codegen/codegen_cuda_host.cc
@@ -0,0 +1,401 @@
+/*
+ * 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.
+ */
+
+/*! \file codegen_cuda_host.cc
+ * \brief C host wrappers with direct CUDA kernel launches.
+ */
+#include <tvm/ffi/reflection/registry.h>
+#include <tvm/tirx/attrs.h>
+#include <tvm/tirx/type.h>
+
+#include <algorithm>
+#include <array>
+#include <limits>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "../../../runtime/metadata.h"
+#include "../../../target/source/codegen_c_host.h"
+
+namespace tvm {
+namespace codegen {
+
+class CodeGenCUDAHost : public CodeGenCHost {
+ public:
+ void Init(Target target) {
+ CodeGenCHost::Init(false, true, true, target->str(), {});
+ // Retain C-host wrapper initialization, with an FFI/CUDA-only prelude.
+ decl_stream.str("");
+ decl_stream.clear();
+ decl_stream << "#include <tvm/ffi/c_api.h>\n"
+ << "#include <tvm/ffi/extra/c_env_api.h>\n"
+ << "#include <tvm/ffi/extra/cuda/device_guard.h>\n"
+ << "#include <tvm/ffi/function.h>\n"
+ << "#include <cuda.h>\n#include <cuda_runtime.h>\n#include
<math.h>\n"
+ << "#define TVM_DLL TVM_FFI_DLL_EXPORT\n";
+ InitGlobalContext();
+ check_error_ = name_supply_->FreshName("tvm_cuda_host_check");
+ decl_stream << "static int " << check_error_
+ << "(cudaError_t error, bool allow_unloading = false) {\n"
+ << " if (error == cudaSuccess || "
+ << "(allow_unloading && error == cudaErrorCudartUnloading))
return 0;\n"
+ << " const char* parts[] = {\"CUDA launch: \",
cudaGetErrorString(error)};\n"
+ << " TVMFFIErrorSetRaisedFromCStrParts(\"CUDAError\", parts,
2);\n"
+ << " cudaGetLastError();\n"
+ << " return -1;\n}\n";
+ }
+
+ using CodeGenCHost::PrintType;
+
+ void PrintType(const Type& type, std::ostream& os) override {
+ if (type.as<tirx::TensorMapTypeNode>()) {
+ os << "CUtensorMap";
+ } else {
+ CodeGenCHost::PrintType(type, os);
+ }
+ }
+
+ void AddFunction(const GlobalVar& gvar, const PrimFunc& func) override {
+ // A guard destructor may report a CUDA error on any return path. Keep it
+ // inside the FFI exception boundary along with device selection and setup.
+ InitFuncState(func);
+ PrintFunctionSignature(GetFunctionName(gvar), func, stream);
+ stream << " {\n TVM_FFI_SAFE_CALL_BEGIN();\n try {\n";
+ int scope = BeginScope();
+ PrintStmt(func->body);
+ EndScope(scope);
+ // CUDADeviceGuard reports errors by throwing. Clear the reported CUDA
+ // last-error state so a subsequent successful launch is not blamed for it.
+ stream << " } catch (...) {\n cudaGetLastError();\n throw;\n }\n"
+ << " TVM_FFI_SAFE_CALL_END();\n}\n\n";
+ if (auto symbol = func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol)) {
+ function_names_.push_back(symbol.value());
+ if (func->HasNonzeroAttr(tirx::attr::kIsEntryFunc) &&
!has_tvm_ffi_main_func_) {
+ function_names_.push_back(ffi::symbol::tvm_ffi_main);
+ PrintFuncPrefix(stream);
+ PrintType(func->ret_type, stream);
+ stream << " " << ffi::symbol::tvm_ffi_main
+ << "(void* self, void* args, int num_args, void* result) {\n"
+ << " return " << symbol.value() << "(self, args, num_args,
result);\n}\n";
+ }
+ }
+ }
+
+ ffi::Array<ffi::String> GetFunctionNames() { return function_names_; }
+
+ 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") {
+ auto count = op->args[1].as_or_throw<IntImm>()->value.as<int64_t>();
+ TVM_FFI_CHECK(count.has_value() && *count > 0, ValueError)
+ << "cuda_host requires a positive constant tensor-map allocation
count";
+ std::string name = name_supply_->FreshName("cuda_tensormap");
+ PrintIndent();
+ stream << "alignas(64) CUtensorMap " << name << "[" << *count << "];\n";
+ os << name;
+ return;
+ }
+ if (op->op.same_as(tirx::builtin::tvm_call_packed_lowered())) {
+ const auto& name = op->args[0].as_or_throw<StringImm>()->value;
+ if (name == "runtime.cuTensorMapEncodeTiled" || name ==
"runtime.cuTensorMapInit") {
+ TVM_FFI_THROW(ValueError)
+ << "cuda_host requires tensormap_encode_tiled instead of a packed
tensor-map encoder";
+ }
+ if (name == "__tvm_set_device") {
+ std::string args =
+ "((TVMFFIAny*)" + PrintExpr(op->args[1]) + " + " +
PrintExpr(op->args[2]) + ")";
+ std::string guard = name_supply_->FreshName("cuda_device_guard");
+ PrintIndent();
+ stream << "if (" << args << "[0].v_int64 != kDLCUDA) {\n"
+ << " TVMFFIErrorSetRaisedFromCStr(\"ValueError\", "
+ << "\"cuda_host requires a CUDA device\");\n return -1;\n}\n";
+ PrintIndent();
+ stream << "tvm::ffi::CUDADeviceGuard " << guard << "(" << args <<
"[1].v_int64);\n";
+ os << "0";
+ return;
+ }
+ std::string function_name = name;
+ TVM_FFI_CHECK(function_name.rfind("runtime.", 0) != 0 &&
+ function_name.rfind("device_api.", 0) != 0 &&
+ function_name.rfind("__tvm_", 0) != 0,
+ ValueError)
+ << "cuda_host does not provide TVM runtime service: " << name;
+ }
+ if (auto call_op = op->op.as<Op>()) {
+ if (op_attr_global_symbol_.count(call_op.value())) {
+ const auto& symbol = op_attr_global_symbol_[call_op.value()];
+ TVM_FFI_CHECK(std::string(symbol).rfind("TVMBackend", 0) != 0,
ValueError)
+ << "cuda_host does not provide TVM runtime operation: " << symbol;
+ }
+ }
+ if (op->op.same_as(tirx::builtin::call_extern()) ||
+ op->op.same_as(tirx::builtin::call_pure_extern())) {
+ const auto& symbol = op->args[0].as_or_throw<StringImm>()->value;
+ TVM_FFI_CHECK(std::string(symbol).rfind("TVMBackend", 0) != 0,
ValueError)
+ << "cuda_host does not provide TVM runtime operation: " << symbol;
+ }
+ if (op->op.same_as(tirx::builtin::tensormap_encode_tiled())) {
+ PrintTensorMapEncode(op);
+ os << "0";
+ return;
+ }
+ if (!op->op.same_as(tirx::builtin::call_ffi_kernel())) {
+ CodeGenCHost::Dispatch_(op, os);
+ return;
+ }
+ const auto* attr = op->attrs.as<tirx::CallFFIKernelAttr>();
+ TVM_FFI_CHECK(attr, ValueError) << "cuda_host kernel calls require
CallFFIKernelAttr";
+ TVM_FFI_CHECK(!op->args.empty() && op->args[0].as<StringImmNode>(),
ValueError)
+ << "cuda_host kernel calls require a string kernel symbol";
+ const auto& symbol = op->args[0].as<StringImmNode>()->value;
+
+ std::array<int, 6> axes;
+ axes.fill(-1);
+ int shared_memory = -1;
+ size_t num_launch_values = 0;
+ std::unordered_set<std::string> seen;
+ for (size_t i = 0; i < attr->launch_params.size(); ++i) {
+ std::string tag = attr->launch_params[i];
+ TVM_FFI_CHECK(seen.insert(tag).second, ValueError)
+ << "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) {
+ 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 {
+ 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;
+ }
+ TVM_FFI_CHECK_GE(axis, 0, ValueError)
+ << "cuda_host does not support launch parameter: " << tag;
+ axes[axis] = static_cast<int>(num_launch_values++);
+ }
+ }
+ TVM_FFI_CHECK_GE(op->args.size(), num_launch_values + 1, ValueError)
+ << "cuda_host kernel call is missing launch arguments";
+ size_t launch_begin = op->args.size() - num_launch_values;
+ for (size_t i = launch_begin; i < op->args.size(); ++i) {
+ auto type = op->args[i]->ty.as<PrimType>();
+ TVM_FFI_CHECK(
+ type.has_value() && type.value().IsScalar() &&
type.value().MatchesCode(kDLInt, kDLUInt),
+ ValueError)
+ << "cuda_host launch arguments must be scalar integers";
+ }
+
+ // Evaluate arguments once in their original order, including launch
values.
+ // Buffer expressions retain their pointer type for the direct kernel call.
+ std::vector<std::string> arguments;
+ for (size_t i = 1; i < op->args.size(); ++i) {
+ std::string value = PrintExpr(op->args[i]);
+ if (i < launch_begin) {
+ if (auto* ptr = op->args[i]->ty.as<PointerTypeNode>()) {
+ if (ptr->element_type.as<tirx::TensorMapTypeNode>()) {
+ // The device ABI takes a grid-constant descriptor by value, while
+ // the host IR carries a pointer to its aligned stack storage.
+ value = "*(" + value + ")";
+ }
+ }
+ }
+ std::string name = name_supply_->FreshName("cuda_arg");
+ PrintIndent();
+ stream << "auto " << name << " = " << value << ";\n";
+ arguments.push_back(name);
+ }
+ auto launch_arg = [&](int index) { return arguments[launch_begin - 1 +
index]; };
+ std::array<std::string, 6> dimensions;
+ for (size_t i = 0; i < axes.size(); ++i) {
+ if (axes[i] < 0) {
+ dimensions[i] = "1";
+ } else {
+ dimensions[i] = name_supply_->FreshName("cuda_dim");
+ PrintIndent();
+ stream << "size_t " << dimensions[i] << " = static_cast<size_t>(" <<
launch_arg(axes[i])
+ << ");\n";
+ // Match LaunchParamConfig's handling of empty dynamic dimensions.
+ PrintIndent();
+ stream << "if (" << dimensions[i] << " == 0) " << dimensions[i] << " =
1;\n";
+ }
+ }
+ std::string device = name_supply_->FreshName("cuda_device");
+ std::string cuda_stream = name_supply_->FreshName("cuda_stream");
+ PrintIndent();
+ stream << "int " << device << ";\n";
+ CheckError("cudaGetDevice(&" + device + ")");
+ PrintIndent();
+ stream << "cudaStream_t " << cuda_stream
+ << " = static_cast<cudaStream_t>(TVMFFIEnvGetStream(kDLCUDA, " <<
device << "));\n";
+ std::string bytes = shared_memory < 0 ? "0" : launch_arg(shared_memory);
+ if (shared_memory >= 0) {
+ PrintIndent();
+ stream << "if (" << bytes << " >= 49152) {\n";
+ int scope = BeginScope();
+ CheckError("cudaFuncSetAttribute(::" + std::string(symbol) +
+ ", cudaFuncAttributeMaxDynamicSharedMemorySize, " + bytes +
")");
+ EndScope(scope);
+ PrintIndent();
+ stream << "}\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 << ");\n";
+ CheckError("cudaGetLastError()", true);
+ os << "0";
+ }
+
+ private:
+ void CheckError(const std::string& call, bool allow_unloading = false) {
+ PrintIndent();
+ stream << "if (::" << check_error_ << "(" << call;
+ if (allow_unloading) stream << ", true";
+ stream << ") != 0) return -1;\n";
+ }
+ void PrintTensorMapEncode(const CallNode* op) {
+ const auto* attr = op->attrs.as<tirx::TensorMapEncodeTiledAttr>();
+ TVM_FFI_CHECK(attr && attr->rank >= 1 && attr->rank <= 5 &&
+ op->args.size() == static_cast<size_t>(4 * attr->rank +
1),
+ ValueError)
+ << "Invalid tensormap_encode_tiled attributes or operands";
+ static const std::unordered_map<std::string, std::string> dtype_names{
+ {"int8", "UINT8"}, {"uint8", "UINT8"},
+ {"uint16", "UINT16"}, {"uint32", "UINT32"},
+ {"uint64", "UINT64"}, {"int32", "INT32"},
+ {"int64", "INT64"}, {"float16", "FLOAT16"},
+ {"float32", "FLOAT32"}, {"float64", "FLOAT64"},
+ {"bfloat16", "BFLOAT16"}, {"float8_e4m3fn", "UINT8"},
+ {"float8_e5m2", "UINT8"}, {"float4_e2m1fn", "16U4_ALIGN16B"}};
+ auto dtype =
dtype_names.find(ffi::DLDataTypeToString(attr->descriptor_dtype));
+ TVM_FFI_CHECK(dtype != dtype_names.end(), ValueError)
+ << "Unsupported cuda_host tensor-map descriptor dtype: "
+ << ffi::DLDataTypeToString(attr->descriptor_dtype);
+ std::string cuda_dtype = "CU_TENSOR_MAP_DATA_TYPE_" + dtype->second;
+ if (attr->force_cu_dtype != -1) {
+ TVM_FFI_CHECK(attr->force_cu_dtype == 11 && attr->descriptor_dtype.code
== kDLFloat &&
+ attr->descriptor_dtype.bits == 32 &&
attr->descriptor_dtype.lanes == 1,
+ ValueError)
+ << "cuda_host only supports a TFLOAT32 tensor-map dtype override";
+ cuda_dtype = "CU_TENSOR_MAP_DATA_TYPE_TFLOAT32";
+ }
+ for (int64_t value : {attr->interleave, attr->swizzle, attr->l2_promotion,
attr->oob_fill}) {
+ TVM_FFI_CHECK(value >= 0 && value <= std::numeric_limits<int>::max(),
ValueError)
+ << "cuda_host tensor-map options must fit a nonnegative CUDA enum";
+ }
+ // Preserve operand evaluation order and reject narrowing that could turn
an
+ // invalid dynamic dimension or stride into a valid but different CUDA
input.
+ std::vector<std::string> values;
+ for (size_t i = 0; i < op->args.size(); ++i) {
+ std::string value = PrintExpr(op->args[i]);
+ std::string name = name_supply_->FreshName("tensormap_arg");
+ PrintIndent();
+ stream << "auto " << name << " = " << value << ";\n";
+ values.push_back(name);
+ }
+ for (size_t i = 2; i < op->args.size(); ++i) {
+ const std::string& name = values[i];
+ auto type = op->args[i]->ty.as<PrimType>();
+ TVM_FFI_CHECK(type && type.value().IsScalar() && type.value().bits() <=
64 &&
+ type.value().MatchesCode(kDLInt, kDLUInt),
+ ValueError)
+ << "cuda_host tensor-map dimensions and strides must be scalar
integers";
+ if (type.value().MatchesCode(kDLInt)) {
+ PrintIndent();
+ stream << "TVM_FFI_CHECK(" << name << " >= 0, ValueError) "
+ << "<< \"Negative tensor-map dimension or stride\";\n";
+ }
+ if (i >= static_cast<size_t>(2 * attr->rank + 1)) {
+ PrintIndent();
+ stream << "TVM_FFI_CHECK(static_cast<uint64_t>(" << name
+ << ") <= 4294967295ULL, ValueError) "
+ << "<< \"Tensor-map dimension or stride exceeds uint32\";\n";
+ }
+ }
+ size_t index = 2;
+ auto array = [&](const char* type, int64_t count) {
+ std::string name = name_supply_->FreshName("tensormap_values");
+ PrintIndent();
+ stream << type << " " << name << "[" << std::max<int64_t>(count, 1) <<
"] = {";
+ for (int64_t i = 0; i < count; ++i) {
+ if (i) stream << ", ";
+ stream << "static_cast<" << type << ">(" << values[index++] << ")";
+ }
+ stream << "};\n";
+ return name;
+ };
+ std::string shape = array("cuuint64_t", attr->rank);
+ std::string strides = array("cuuint64_t", attr->rank - 1);
+ std::string box = array("cuuint32_t", attr->rank);
+ std::string element_strides = array("cuuint32_t", attr->rank);
+ std::string error = name_supply_->FreshName("tensormap_error");
+ PrintIndent();
+ stream << "CUresult " << error << " =
cuTensorMapEncodeTiled(static_cast<CUtensorMap*>("
+ << values[0] << "), " << cuda_dtype << ", " << attr->rank << ", "
<< values[1] << ", "
+ << shape << ", " << strides << ", " << box << ", " <<
element_strides
+ << ", static_cast<CUtensorMapInterleave>(" << attr->interleave
+ << "), static_cast<CUtensorMapSwizzle>(" << attr->swizzle
+ << "), static_cast<CUtensorMapL2promotion>(" << attr->l2_promotion
+ << "), static_cast<CUtensorMapFloatOOBfill>(" << attr->oob_fill <<
"));\n";
+ PrintIndent();
+ stream << "if (" << error << " != CUDA_SUCCESS) {\n"
+ << " const char* message = \"cuTensorMapEncodeTiled failed\";\n"
+ << " cuGetErrorString(" << error << ", &message);\n"
+ << " TVMFFIErrorSetRaisedFromCStr(\"CUDAError\", message);\n"
+ << " return -1;\n}\n";
+ }
+
+ std::string check_error_;
+ ffi::Array<ffi::String> function_names_;
+};
+
+ffi::Module BuildCUDAHost(IRModule mod, Target target) {
+ CodeGenCUDAHost cg;
+ cg.Init(target);
+ std::vector<std::pair<GlobalVar, PrimFunc>> functions;
+ for (auto [gvar, base_func] : mod->functions) {
+ functions.emplace_back(gvar, base_func.as_or_throw<PrimFunc>());
+ }
+ std::sort(functions.begin(), functions.end(), [](const auto& lhs, const
auto& rhs) {
+ return lhs.first->name_hint < rhs.first->name_hint;
+ });
+ for (const auto& [gvar, func] : functions) cg.DeclareFunction(gvar, func);
+ for (const auto& [gvar, func] : functions) cg.AddFunction(gvar, func);
+ return CSourceModuleCreate(cg.Finish(), "cu", cg.GetFunctionNames());
+}
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+ ffi::reflection::GlobalDef().def("target.build.cuda_host", BuildCUDAHost);
+}
+
+} // namespace codegen
+} // namespace tvm
diff --git a/src/backend/cuda/codegen/target_kind.cc
b/src/backend/cuda/codegen/target_kind.cc
index 3624983cef..b63f1f6e96 100644
--- a/src/backend/cuda/codegen/target_kind.cc
+++ b/src/backend/cuda/codegen/target_kind.cc
@@ -114,6 +114,8 @@ ffi::Map<ffi::String, ffi::Any>
UpdateNVPTXAttrs(ffi::Map<ffi::String, ffi::Any>
void RegisterTargetKinds() {
namespace refl = tvm::ffi::reflection;
+ TVM_REGISTER_TARGET_KIND("cuda_host", kDLCPU).set_default_keys({"cpu"});
+
TVM_REGISTER_TARGET_KIND("cuda", kDLCUDA)
.add_attr_option<ffi::String>("mcpu")
.add_attr_option<ffi::String>("arch")
diff --git a/src/tirx/op/builtin.cc b/src/tirx/op/builtin.cc
index 84db3c9749..f2856276aa 100644
--- a/src/tirx/op/builtin.cc
+++ b/src/tirx/op/builtin.cc
@@ -24,6 +24,7 @@
*/
#include <tvm/ffi/function.h>
#include <tvm/ir/prim/builtin.h>
+#include <tvm/tirx/attrs.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/op.h>
#include <tvm/tirx/op_attr_types.h>
@@ -32,6 +33,31 @@ namespace tvm {
namespace tirx {
namespace builtin {
+TVM_FFI_STATIC_INIT_BLOCK() {
+ TensorMapEncodeTiledAttr::RegisterReflection();
+ ffi::reflection::GlobalDef().def(
+ "tirx.TensorMapEncodeTiledAttr",
+ [](DLDataType descriptor_dtype, int64_t rank, int64_t interleave,
int64_t swizzle,
+ int64_t l2_promotion, int64_t oob_fill, int64_t force_cu_dtype) {
+ auto attrs = ffi::make_object<TensorMapEncodeTiledAttr>();
+ attrs->descriptor_dtype = descriptor_dtype;
+ attrs->rank = rank;
+ attrs->interleave = interleave;
+ attrs->swizzle = swizzle;
+ attrs->l2_promotion = l2_promotion;
+ attrs->oob_fill = oob_fill;
+ attrs->force_cu_dtype = force_cu_dtype;
+ return Attrs(attrs);
+ });
+ CallFFIKernelAttr::RegisterReflection();
+ ffi::reflection::GlobalDef().def("tirx.CallFFIKernelAttr",
+ [](ffi::Array<ffi::String> launch_params) {
+ auto attrs =
ffi::make_object<CallFFIKernelAttr>();
+ attrs->launch_params =
std::move(launch_params);
+ return Attrs(attrs);
+ });
+}
+
// Script metadata extends the canonical primitive operators registered by IR.
TVM_FFI_STATIC_INIT_BLOCK() {
#define PRIM_SCRIPT_BUILTIN(OpName) \
@@ -207,6 +233,12 @@ TIR_DEFINE_BUILTIN_FUNC(tvm_call_packed)
.set_attr<TCallEffectKind>("TCallEffectKind",
static_cast<int64_t>(CallEffectKind::kOpaque))
.set_attr<TScriptPrinterName>("TScriptPrinterName",
ffi::String("call_packed"), /*plevel=*/20);
+TIR_DEFINE_BUILTIN_FUNC(tensormap_encode_tiled)
+ .set_attr<TCallEffectKind>("TCallEffectKind",
static_cast<int64_t>(CallEffectKind::kOpaque));
+
+TIR_DEFINE_BUILTIN_FUNC(call_ffi_kernel)
+ .set_attr<TCallEffectKind>("TCallEffectKind",
static_cast<int64_t>(CallEffectKind::kOpaque));
+
TIR_DEFINE_BUILTIN_FUNC(tvm_call_cpacked)
.set_attr<TCallEffectKind>("TCallEffectKind",
static_cast<int64_t>(CallEffectKind::kOpaque))
.set_attr<TScriptPrinterName>("TScriptPrinterName",
ffi::String("call_cpacked"), /*plevel=*/20);
diff --git a/src/tirx/script/printer/expr.cc b/src/tirx/script/printer/expr.cc
index e3a77b19ab..f694b844ed 100644
--- a/src/tirx/script/printer/expr.cc
+++ b/src/tirx/script/printer/expr.cc
@@ -18,6 +18,7 @@
*/
#include <tvm/ir/prim/builtin.h>
#include <tvm/te/operation.h>
+#include <tvm/tirx/attrs.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/type.h>
@@ -377,6 +378,32 @@ Doc PrintTIRCall(Call call, AccessPath call_p, IRDocsifier
d) {
for (int i = 0; i < n_args; ++i) {
call_args.push_back(d->AsDoc<ExprDoc>(call->args[i],
call_p->Attr("args")->ArrayItem(i)));
}
+ if (call->op.same_as(tirx::builtin::tensormap_encode_tiled())) {
+ const auto* attrs = call->attrs.as<tirx::TensorMapEncodeTiledAttr>();
+ TVM_FFI_ICHECK(attrs);
+ auto attr_p = call_p->Attr("attrs");
+ return TIR(d, "tensormap_encode_tiled")
+ ->Call(call_args,
+ {"descriptor_dtype", "rank", "interleave", "swizzle",
"l2_promotion", "oob_fill",
+ "force_cu_dtype"},
+
{LiteralDoc::Str(ffi::DLDataTypeToString(attrs->descriptor_dtype),
+ attr_p->Attr("descriptor_dtype")),
+ LiteralDoc::Int(attrs->rank, attr_p->Attr("rank")),
+ LiteralDoc::Int(attrs->interleave,
attr_p->Attr("interleave")),
+ LiteralDoc::Int(attrs->swizzle, attr_p->Attr("swizzle")),
+ LiteralDoc::Int(attrs->l2_promotion,
attr_p->Attr("l2_promotion")),
+ LiteralDoc::Int(attrs->oob_fill, attr_p->Attr("oob_fill")),
+ LiteralDoc::Int(attrs->force_cu_dtype,
attr_p->Attr("force_cu_dtype"))});
+ }
+ if (call->op.same_as(tirx::builtin::call_ffi_kernel())) {
+ const auto* attrs = call->attrs.as<tirx::CallFFIKernelAttr>();
+ TVM_FFI_ICHECK(attrs);
+ return TIR(d, "call_ffi_kernel")
+ ->Call(call_args, {"launch_params", "ret_ty"},
+ {d->AsDoc<ExprDoc>(attrs->launch_params,
+
call_p->Attr("attrs")->Attr("launch_params")),
+ get_call_return_type_doc()});
+ }
ExprDoc op_doc = call->op.as<Op>()
? LiteralDoc::Str(call->op.as<Op>().value()->name,
call_p->Attr("op"))
: d->AsDoc<ExprDoc>(call->op, call_p->Attr("op"));
diff --git a/src/tirx/transform/lower_tirx_dedup_tensormap.cc
b/src/tirx/transform/lower_tirx_dedup_tensormap.cc
index 23f9a1cbab..5a9c5c8111 100644
--- a/src/tirx/transform/lower_tirx_dedup_tensormap.cc
+++ b/src/tirx/transform/lower_tirx_dedup_tensormap.cc
@@ -53,10 +53,12 @@ inline bool IsTensorMapAlloca(const BindNode* bind) {
return false;
}
-// Is an Evaluate of tvm_call_packed("runtime.cuTensorMapEncodeTiled", ...)?
+// Recognize typed encoding and legacy manually authored packed encoding.
inline const CallNode* AsCuTensorMapEncode(const EvaluateNode* eval) {
const CallNode* call = eval->value.as<CallNode>();
- if (!call || !call->op.same_as(builtin::tvm_call_packed())) return nullptr;
+ if (!call) return nullptr;
+ if (call->op.same_as(builtin::tensormap_encode_tiled())) return call;
+ if (!call->op.same_as(builtin::tvm_call_packed())) return nullptr;
if (call->args.empty()) return nullptr;
if (const auto* s = call->args[0].as<StringImmNode>()) {
if (s->value == "runtime.cuTensorMapEncodeTiled") return call;
@@ -64,23 +66,17 @@ inline const CallNode* AsCuTensorMapEncode(const
EvaluateNode* eval) {
return nullptr;
}
-// Extract the tensormap var and the key (arguments after the tensormap var)
-inline std::pair<ffi::Optional<Var>, ffi::Array<Expr>> ExtractEncodeKey(const
CallNode* call) {
- TVM_FFI_ICHECK(call->op.same_as(builtin::tvm_call_packed()));
- // args[0] is function name, args[1] is tensormap handle, rest are parameters
- if (call->args.size() < 2) return {ffi::Optional<Var>(), ffi::Array<Expr>()};
- ffi::Optional<Var> tensormap;
- if (auto v = call->args[1].as<Var>()) {
- tensormap = v.value();
- } else {
- tensormap = ffi::Optional<Var>();
+// Exclude only the output pointer; retain op, attributes and all input
operands
+// so descriptor dtype, forced dtype and encoding modes participate in
equality.
+inline std::pair<ffi::Optional<Var>, Call> ExtractEncodeKey(const CallNode*
call) {
+ size_t output_index = call->op.same_as(builtin::tensormap_encode_tiled()) ?
0 : 1;
+ TVM_FFI_ICHECK_GT(call->args.size(), output_index);
+ ffi::Optional<Var> tensormap = call->args[output_index].as<Var>();
+ ffi::Array<Expr> args;
+ for (size_t i = 0; i < call->args.size(); ++i) {
+ if (i != output_index) args.push_back(call->args[i]);
}
- ffi::Array<Expr> key;
- key.reserve(call->args.size() - 2);
- for (size_t i = 2; i < call->args.size(); ++i) {
- key.push_back(call->args[i]);
- }
- return {tensormap, key};
+ return {tensormap, Call(call->ty, call->op, args, call->attrs)};
}
} // namespace
@@ -88,14 +84,12 @@ inline std::pair<ffi::Optional<Var>, ffi::Array<Expr>>
ExtractEncodeKey(const Ca
// First pass: Analyze encode calls and decide canonical tensormap
per-parameter set
class CuTensorMapDedupAnalyzer : public StmtExprVisitor {
public:
- CuTensorMapDedupAnalyzer() {
- canonical_list_.emplace_back(std::vector<std::pair<ffi::Array<Expr>,
Var>>());
- }
+ CuTensorMapDedupAnalyzer() {
canonical_list_.emplace_back(std::vector<std::pair<Call, Var>>()); }
ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->min));
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->extent));
- canonical_list_.emplace_back(std::vector<std::pair<ffi::Array<Expr>,
Var>>());
+ canonical_list_.emplace_back(std::vector<std::pair<Call, Var>>());
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->body));
canonical_list_.pop_back();
return std::nullopt;
@@ -103,7 +97,7 @@ class CuTensorMapDedupAnalyzer : public StmtExprVisitor {
ffi::Optional<VisitInterrupt> Visit_(const WhileNode* op) final {
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->condition));
- canonical_list_.emplace_back(std::vector<std::pair<ffi::Array<Expr>,
Var>>());
+ canonical_list_.emplace_back(std::vector<std::pair<Call, Var>>());
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->body));
canonical_list_.pop_back();
return std::nullopt;
@@ -111,11 +105,11 @@ class CuTensorMapDedupAnalyzer : public StmtExprVisitor {
ffi::Optional<VisitInterrupt> Visit_(const IfThenElseNode* op) final {
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->condition));
- canonical_list_.emplace_back(std::vector<std::pair<ffi::Array<Expr>,
Var>>());
+ canonical_list_.emplace_back(std::vector<std::pair<Call, Var>>());
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->then_case));
canonical_list_.pop_back();
if (op->else_case) {
- canonical_list_.emplace_back(std::vector<std::pair<ffi::Array<Expr>,
Var>>());
+ canonical_list_.emplace_back(std::vector<std::pair<Call, Var>>());
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit(op->else_case.value()));
canonical_list_.pop_back();
}
@@ -153,7 +147,7 @@ class CuTensorMapDedupAnalyzer : public StmtExprVisitor {
}
private:
- std::vector<std::vector<std::pair<ffi::Array<Expr>, Var>>> canonical_list_;
+ std::vector<std::vector<std::pair<Call, Var>>> canonical_list_;
std::unordered_map<Var, Var, ffi::ObjectPtrHash, ffi::ObjectPtrEqual>
tensormap_var_remap_;
};
@@ -165,7 +159,7 @@ class CuTensorMapDedupRewriter : public StmtExprMutator {
CuTensorMapDedupRewriter(
std::unordered_map<Var, Var, ffi::ObjectPtrHash, ffi::ObjectPtrEqual>
var_remap) {
for (const auto& [source, target] : var_remap) VarRemapSet(source, target);
- emitted_keys_.emplace_back(std::vector<ffi::Array<Expr>>());
+ emitted_keys_.emplace_back(std::vector<Call>());
}
private:
@@ -297,7 +291,7 @@ class CuTensorMapDedupRewriter : public StmtExprMutator {
}
// Track which parameter keys have already emitted an encode call
- std::vector<std::vector<ffi::Array<Expr>>> emitted_keys_;
+ std::vector<std::vector<Call>> emitted_keys_;
};
namespace transform {
diff --git a/src/tirx/transform/lower_tvm_builtin.cc
b/src/tirx/transform/lower_tvm_builtin.cc
index cd3a88da88..9f14fe4d95 100644
--- a/src/tirx/transform/lower_tvm_builtin.cc
+++ b/src/tirx/transform/lower_tvm_builtin.cc
@@ -29,6 +29,7 @@
#include <tvm/ir/prim/expr.h>
#include <tvm/ir/scope_stack.h>
#include <tvm/runtime/logging.h>
+#include <tvm/tirx/attrs.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/stmt_functor.h>
#include <tvm/tirx/transform.h>
@@ -57,17 +58,21 @@ class BuiltinLower : public StmtExprMutator {
using StmtExprMutator::Mutate_;
static PrimFunc Build(PrimFunc func) {
ffi::Optional<PrimExpr> device_type = std::nullopt;
+ bool preserve_ffi_kernel = false;
if (auto target = func->GetAttr<Target>(tvm::attr::kTarget)) {
device_type = IntImm::Int32(target.value()->kind->default_device_type);
+ auto host = target.value()->GetHost().value_or(target.value());
+ preserve_ffi_kernel = host->kind->name == "cuda_host";
}
- auto mutator = ffi::make_object<BuiltinLower>(device_type);
+ auto mutator = ffi::make_object<BuiltinLower>(device_type,
preserve_ffi_kernel);
func.CopyOnWrite()->body = mutator->VisitBodyAndRealizeAlloca(func->body);
return func;
}
- explicit BuiltinLower(ffi::Optional<PrimExpr> device_type = std::nullopt)
- : device_type_(device_type) {}
+ explicit BuiltinLower(ffi::Optional<PrimExpr> device_type = std::nullopt,
+ bool preserve_ffi_kernel = false)
+ : device_type_(device_type), preserve_ffi_kernel_(preserve_ffi_kernel) {}
// NOTE: Right now, we make the following scoping requirement
// for memory allocated by the following primitives
@@ -405,7 +410,27 @@ class BuiltinLower : public StmtExprMutator {
}
UnchangedOr<Expr> Mutate_(const CallNode* op, InplaceMode inplace_mode)
final {
- if (op->op.same_as(builtin::tvm_call_packed())) {
+ if (op->op.same_as(builtin::tensormap_encode_tiled()) &&
!preserve_ffi_kernel_) {
+ const auto* attr = op->attrs.as<TensorMapEncodeTiledAttr>();
+ TVM_FFI_CHECK(attr && attr->rank >= 1 && attr->rank <= 5 &&
+ op->args.size() == static_cast<size_t>(4 * attr->rank
+ 1),
+ ValueError)
+ << "Invalid tensormap_encode_tiled attributes or operands";
+ ffi::Array<Expr> args{StringImm("runtime.cuTensorMapEncodeTiled"),
op->args[0],
+
StringImm(ffi::DLDataTypeToString(attr->descriptor_dtype)),
+ IntImm(PrimType::Int(32), attr->rank),
op->args[1]};
+ for (size_t i = 2; i < op->args.size(); ++i) args.push_back(op->args[i]);
+ for (int64_t value : {attr->interleave, attr->swizzle,
attr->l2_promotion, attr->oob_fill}) {
+ args.push_back(IntImm(PrimType::Int(32), value));
+ }
+ if (attr->force_cu_dtype != -1) {
+ args.push_back(IntImm(PrimType::Int(32), attr->force_cu_dtype));
+ }
+ Call packed(op->ty, builtin::tvm_call_packed(), args);
+ return MakeCallPackedGeneric(packed.get(), 0,
builtin::tvm_call_packed_lowered(), false);
+ }
+ if (op->op.same_as(builtin::tvm_call_packed()) ||
+ (op->op.same_as(builtin::call_ffi_kernel()) && !preserve_ffi_kernel_))
{
return MakeCallPackedGeneric(op, 0, builtin::tvm_call_packed_lowered(),
/* use_last_value_as_traced_value*/ false);
} else if (op->op.same_as(builtin::tvm_call_cpacked())) {
@@ -797,6 +822,8 @@ class BuiltinLower : public StmtExprMutator {
ffi::Optional<PrimExpr> device_type_{std::nullopt};
ffi::Optional<PrimExpr> device_id_{std::nullopt};
+ // CUDA host codegen consumes explicit launches after ordinary builtin
lowering.
+ bool preserve_ffi_kernel_{false};
bool is_precheck_{false};
// Record all stack frames.
diff --git a/src/tirx/transform/split_host_device.cc
b/src/tirx/transform/split_host_device.cc
index b700b963c0..a2ab6f41a1 100644
--- a/src/tirx/transform/split_host_device.cc
+++ b/src/tirx/transform/split_host_device.cc
@@ -31,6 +31,7 @@
#include <tvm/ir/unique_name_supply.h>
#include <tvm/target/target.h>
#include <tvm/tirx/analysis.h>
+#include <tvm/tirx/attrs.h>
#include <tvm/tirx/builtin.h>
#include <tvm/tirx/op.h>
#include <tvm/tirx/stmt_functor.h>
@@ -859,7 +860,10 @@ class DeviceKernelMutator : public StmtExprMutator {
PrimType node_ty = IsVoidType(node->ty) ? PrimType::Void() :
node->ty.as_or_throw<PrimType>();
PrimType ret_ty = node_ty.IsVoid() ? PrimType::Int(32) : node_ty;
- return Call(ret_ty, builtin::tvm_call_packed(),
call_args).as_or_throw<PrimExpr>();
+ auto attrs = ffi::make_object<CallFFIKernelAttr>();
+ attrs->launch_params = dev_info.launch_params;
+ return Call(ret_ty, builtin::call_ffi_kernel(), call_args, Attrs(attrs))
+ .as_or_throw<PrimExpr>();
}
ffi::Optional<Target> current_target_;
diff --git a/tests/python/codegen/test_target_codegen_cuda.py
b/tests/python/codegen/test_target_codegen_cuda.py
index d0459f3e95..6c3aa00edd 100644
--- a/tests/python/codegen/test_target_codegen_cuda.py
+++ b/tests/python/codegen/test_target_codegen_cuda.py
@@ -54,6 +54,47 @@ def setup_cuda_compile_mode(request):
tvm.register_global_func("tvm_callback_cuda_compile", orig_func,
override=True)
[email protected]
[email protected](not env.has_cuda(), reason="need cuda")
+def test_cuda_host_bundle(tmp_path):
+ 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(s_tir=True)
+ def add_one(A: T.Buffer((32,), "float32"), B: T.Buffer((32,), "float32")):
+ for tx in T.thread_binding(32, "threadIdx.x"):
+ B[tx] = A[tx] + T.float32(1)
+
+ 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("<<<")
+ library = tvm_ffi.cpp.build_inline(
+ name="cuda_host_add_one",
+ 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)
+
+
@pytest.mark.gpu
@pytest.mark.skipif(not env.has_cuda(), reason="need cuda")
def test_cuda_vectorize_add():
diff --git
a/tests/python/tirx-transform/test_tir_transform_lower_tvm_builtin.py
b/tests/python/tirx-transform/test_tir_transform_lower_tvm_builtin.py
index bacbe70d42..c1b4ef510c 100644
--- a/tests/python/tirx-transform/test_tir_transform_lower_tvm_builtin.py
+++ b/tests/python/tirx-transform/test_tir_transform_lower_tvm_builtin.py
@@ -110,13 +110,19 @@ def test_lower_call_packed():
@pytest.mark.skipif(not env.has_llvm(), reason="need llvm")
-def test_lower_call_packed_raw_string():
[email protected]("call", [tvm.tirx.call_packed,
tvm.tirx.call_ffi_kernel])
+def test_lower_call_packed_raw_string(call):
+ def invoke(*args):
+ if call is tvm.tirx.call_ffi_kernel:
+ return call(*args, launch_params=[])
+ return call(*args)
+
@I.ir_module
class Before:
@T.prim_func(s_tir=True)
def main():
T.func_attr({"target": tvm.target.Target("llvm")})
- T.call_packed("testing.echo", "payload")
+ T.evaluate(invoke("testing.echo", "payload"))
@I.ir_module
class Expected:
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 ed5ce0f9a3..4cbe5cba48 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
@@ -70,7 +70,7 @@ def test_split_host_device():
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm",
"opt-level": 0})})
- T.call_packed("main_kernel", n)
+ T.call_ffi_kernel("main_kernel", n, launch_params=[])
@T.prim_func(s_tir=True)
def main_kernel(n: T.int32):
@@ -164,7 +164,7 @@ def test_split_host_device_without_func_host_attribute():
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("llvm")})
- T.call_packed("main_kernel", n)
+ T.call_ffi_kernel("main_kernel", n, launch_params=[])
@T.prim_func(s_tir=True)
def main_kernel(n: T.int32):
@@ -229,7 +229,7 @@ def test_split_host_device_name_collision():
@T.prim_func(s_tir=True)
def main(n: T.int32):
T.func_attr({"target": T.target("cuda", host={"kind": "llvm",
"opt-level": 0})})
- T.call_packed("main_kernel_1", n)
+ T.call_ffi_kernel("main_kernel_1", n, launch_params=[])
@T.prim_func(s_tir=True)
def main_kernel_1(n: T.int32):
@@ -384,7 +384,7 @@ def test_thread_extent_region_extracted_as_device_kernel():
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
T.func_attr({"target": T.target("cuda", host="llvm")})
- T.call_packed("main_kernel", A.data, 16)
+ T.call_ffi_kernel("main_kernel", A.data, 16,
launch_params=["threadIdx.x"])
@T.prim_func(s_tir=True)
def main_kernel(A_data: T.handle("float32")):
@@ -433,9 +433,14 @@ def test_cuda_launch_preserves_flag_metadata():
launch = after["main"].body.value
assert isinstance(launch, tvm.ir.Call)
+ assert launch.op == tvm.ir.Op.get("tirx.call_ffi_kernel")
+ assert isinstance(launch.attrs, tvm.tirx.CallFFIKernelAttr)
+ assert list(launch.attrs.launch_params) ==
list(kernel.attrs["tirx.kernel_launch_params"])
# Programmatic launch is flag-only and therefore adds no packed operand.
assert len(launch.args) == 3
assert int(launch.args[-1]) == 16
+ tvm.ir.assert_structural_equal(after,
tvm.ir.load_json(tvm.ir.save_json(after)))
+ tvm.ir.assert_structural_equal(after,
tvm.script.from_source(after.script()))
def test_cuda_required_block_size_coexists_with_launch_bounds():
@@ -515,7 +520,7 @@ def test_device_scope_region_extracted_as_device_kernel():
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("cuda", host="llvm")})
- T.call_packed("main_kernel", A.data)
+ T.call_ffi_kernel("main_kernel", A.data, launch_params=[])
@T.prim_func(s_tir=True)
def main_kernel(A_data: T.handle("float32")):
@@ -558,7 +563,7 @@ def test_lower_device_kernel_launch():
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
- T.call_packed("kernel", A.data)
+ T.call_ffi_kernel("kernel", A.data, launch_params=[])
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
@@ -599,7 +604,7 @@ def test_externally_visible_kernel_launch():
@T.prim_func(s_tir=True)
def main(A: T.Buffer(1, "float32")):
T.func_attr({"target": T.target("llvm")})
- T.call_packed("kernel_by_another_name", A.data)
+ T.call_ffi_kernel("kernel_by_another_name", A.data,
launch_params=[])
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
@@ -646,7 +651,7 @@ def test_collect_launch_parameter():
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32")):
T.func_attr({"target": T.target("llvm")})
- T.call_packed("kernel", A.data, 16)
+ T.call_ffi_kernel("kernel", A.data, 16,
launch_params=["threadIdx.x"])
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32")):
@@ -729,7 +734,7 @@ def test_bind_before_thread_extent():
@T.prim_func(s_tir=True)
def main(A: T.Buffer(16, "float32"), n: T.int32):
T.func_attr({"target": T.target("llvm")})
- T.call_packed("kernel", A.data, n, n + 1)
+ T.call_ffi_kernel("kernel", A.data, n, n + 1,
launch_params=["threadIdx.x"])
@T.prim_func(s_tir=True)
def kernel(A_data: T.handle("float32"), n: T.int32):
diff --git
a/tests/python/tirx/operator/tile_primitive/cuda/copy_async/test_tma.py
b/tests/python/tirx/operator/tile_primitive/cuda/copy_async/test_tma.py
index f2fef0a861..cde0816723 100644
--- a/tests/python/tirx/operator/tile_primitive/cuda/copy_async/test_tma.py
+++ b/tests/python/tirx/operator/tile_primitive/cuda/copy_async/test_tma.py
@@ -26,7 +26,7 @@ import tvm_ffi
import tvm
import tvm.testing
-from tvm.ir import PointerType, PrimType, Range, StringImm
+from tvm.ir import PointerType, PrimType, Range
from tvm.script import tirx as T
from tvm.script.tirx import tile as Tx
from tvm.sym import Analyzer
@@ -123,12 +123,7 @@ class _EncodeCollector:
self.calls = []
def _visit_call(self, op):
- if (
- isinstance(op.op, tvm.ir.Op)
- and op.op.name == "tirx.tvm_call_packed"
- and isinstance(op.args[0], StringImm)
- and op.args[0].value == "runtime.cuTensorMapEncodeTiled"
- ):
+ if isinstance(op.op, tvm.ir.Op) and op.op.name ==
"tirx.tensormap_encode_tiled":
self.calls.append(op)
def visit_stmt(self, stmt):
@@ -271,8 +266,8 @@ def _collect_encodes(stmts):
def _encode_signature(call):
- rank = int(call.args[3])
- cursor = 5
+ rank = call.attrs.rank
+ cursor = 2
dims = tuple(call.args[cursor : cursor + rank])
cursor += rank
strides = tuple(call.args[cursor : cursor + rank - 1])
@@ -281,13 +276,17 @@ def _encode_signature(call):
cursor += rank
element_strides = tuple(call.args[cursor : cursor + rank])
cursor += rank
- enums = tuple(call.args[cursor : cursor + 4])
- cursor += 4
- forced_dtype = call.args[cursor] if cursor < len(call.args) else None
+ enums = (
+ call.attrs.interleave,
+ call.attrs.swizzle,
+ call.attrs.l2_promotion,
+ call.attrs.oob_fill,
+ )
+ forced_dtype = call.attrs.force_cu_dtype if call.attrs.force_cu_dtype >= 0
else None
return {
- "dtype": call.args[2].value,
+ "dtype": str(call.attrs.descriptor_dtype),
"rank": rank,
- "base": call.args[4],
+ "base": call.args[1],
"dims": dims,
"strides": strides,
"boxes": boxes,
@@ -1226,7 +1225,7 @@ def
test_auto_maximum_prefix_and_mixed_radix_issue_pointer():
assert _count_tma(impl).total == 512
-def test_copy_tma_host_init_dtype_is_string():
+def test_copy_tma_host_init_dtype_is_attribute():
"""The host-init encode call must carry the dtype as a StringImm, not a
packed enum -- ``_encode_signature`` reads ``args[2].value`` as a str."""
_, host_init_stmts, _ = _lower_direct(
@@ -1238,8 +1237,7 @@ def test_copy_tma_host_init_dtype_is_string():
dtype="float16",
)
encode_call = _collect_encodes(host_init_stmts)[0]
- assert isinstance(encode_call.args[2], StringImm)
- assert encode_call.args[2].value == "float16"
+ assert str(encode_call.attrs.descriptor_dtype) == "float16"
@pytest.mark.parametrize(