Files
tilelang/testing/python/jit/test_tilelang_jit_cutedsl_host_codegen.py
T
a84c9b1433 [CuTeDSL] Integrate host codegen call sites (#2221)
[CuTeDSL] Integrate host codegen call sites

Drive CuTeDSL launcher generation from lowered host tvm_call_packed call sites so multi-kernel wrappers preserve host IR order and repeated launches can be represented.

Expose separate device, host, and generated module sources through the CuTeDSL adapter and persist them separately in the kernel/autotune caches.

Normalize bare cutedsl targets through TileLang CUDA arch detection so Hopper runners use sm_90a for CuTeDSL compilation instead of the generic sm_90 Target("cuda") result.

Add CuTeDSL host-codegen integration tests covering multi-call launcher ordering, source accessors, cache restore behavior, and bare target architecture normalization.

Co-authored-by: Jayce <jayce.su@enflame-tech.com>
Co-authored-by: LeiWang1999 <leiwang1999@outlook.com>
2026-05-21 20:28:41 +08:00

256 lines
9.5 KiB
Python

"""Tests for CuTeDSL host codegen integration."""
import pytest
import tilelang
import tilelang.language as T
import tilelang.testing
CUTEDSL_SM90_TARGET = {"kind": "cutedsl", "arch": "sm_90"}
def _require_cutedsl():
"""Skip when the CuTeDSL Python stack is unavailable."""
try:
from tilelang.jit.adapter.cutedsl.checks import check_cutedsl_available
check_cutedsl_available()
except (ImportError, ModuleNotFoundError, RuntimeError, AssertionError) as err:
pytest.skip(f"CuTeDSL is not available: {err}")
def _lower_cutedsl(program):
"""Lower a TileLang program and build its CuTeDSL host wrapper."""
_require_cutedsl()
from tilelang.jit.adapter.cutedsl.wrapper import TLCuTeDSLSourceWrapper
from tilelang.utils.target import determine_target
target = determine_target(CUTEDSL_SM90_TARGET)
with target:
artifact = tilelang.lower(program, target=target)
mod = tilelang.tvm.IRModule({program.attrs["global_symbol"]: program})
wrapper = TLCuTeDSLSourceWrapper(mod, artifact.kernel_source, target, artifact.device_mod, artifact.host_mod)
return artifact, wrapper
def _host_entry_gvar_and_func(wrapper):
"""Return the GlobalVar and PrimFunc selected as the wrapper host entry."""
entry_func = wrapper._host_entry_func()
for gvar, host_func in wrapper.host_mod.functions.items():
if host_func.same_as(entry_func):
return gvar, host_func
raise AssertionError("Unable to locate wrapper host entry function in host_mod")
def single_kernel_program(N, block_size=64, dtype=T.float32):
"""Create a single-kernel CuTeDSL program."""
@T.prim_func
def main(
A: T.Tensor((N,), dtype),
B: T.Tensor((N,), dtype),
):
"""Write one output tensor from one device kernel."""
with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx,):
for i in T.Parallel(block_size):
idx = bx * block_size + i
if idx < N:
B[idx] = A[idx] + 1.0
return main
def two_kernel_program(N, block_size=64, dtype=T.float32):
"""Create a program with two lowered host kernel call sites."""
@T.prim_func
def main(
A: T.Tensor((N,), dtype),
B: T.Tensor((N,), dtype),
C: T.Tensor((N,), dtype),
):
"""Launch two device kernels from one host wrapper."""
with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx,):
for i in T.Parallel(block_size):
idx = bx * block_size + i
if idx < N:
B[idx] = A[idx] + 1.0
with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx2,):
for j in T.Parallel(block_size):
idx = bx2 * block_size + j
if idx < N:
C[idx] = B[idx] * 2.0
return main
@tilelang.testing.requires_cuda
def test_cutedsl_host_wrapper_follows_lowered_host_call_sites():
"""Verify generated CuTeDSL launchers use lowered host call-site order."""
_, wrapper = _lower_cutedsl(two_kernel_program(64))
call_sites = wrapper._collect_host_kernel_call_sites()
call_names = [call_site["function_name"] for call_site in call_sites]
assert len(call_names) == 2
assert call_names == wrapper.function_names
launcher_cpp = wrapper.get_launcher_cpp_code()
first_launch = launcher_cpp.index(f"Launch kernel 0: {call_names[0]}")
second_launch = launcher_cpp.index(f"Launch kernel 1: {call_names[1]}")
assert first_launch < second_launch
host_source = wrapper.host_func
first_cubin_launch = host_source.index(f"{call_names[0]}(")
second_cubin_launch = host_source.index(f"{call_names[1]}(")
assert first_cubin_launch < second_cubin_launch
@tilelang.testing.requires_cuda
def test_cutedsl_host_wrapper_preserves_repeated_same_kernel_call_sites():
"""Verify repeated host launches of the same device kernel remain distinct."""
_, wrapper = _lower_cutedsl(single_kernel_program(64))
original_call_site = wrapper._collect_host_kernel_call_sites()[0]
function_name = original_call_site["function_name"]
repeated_body = tilelang.tvm.tirx.SeqStmt(
[
tilelang.tvm.tirx.Evaluate(tilelang.tvm.tirx.call_packed(function_name, *original_call_site["function_params"])),
tilelang.tvm.tirx.Evaluate(tilelang.tvm.tirx.call_packed(function_name, *original_call_site["function_params"])),
]
)
entry_gvar, entry_func = _host_entry_gvar_and_func(wrapper)
wrapper.host_mod = tilelang.tvm.IRModule({entry_gvar.name_hint: entry_func.with_body(repeated_body)})
call_sites = wrapper._collect_host_kernel_call_sites()
call_names = [call_site["function_name"] for call_site in call_sites]
assert call_names == [function_name, function_name]
assert call_sites[0] is not call_sites[1]
wrapper.update_lib_code(wrapper.source)
launcher_cpp = wrapper.get_launcher_cpp_code()
first_launch = launcher_cpp.index(f"Launch kernel 0: {function_name}")
second_launch = launcher_cpp.index(f"Launch kernel 1: {function_name}")
assert first_launch < second_launch
host_source = wrapper.host_func
kernel_wrapper_source = host_source[host_source.index("def kernel_wrapper") :]
first_cubin_launch = kernel_wrapper_source.index(f"{function_name}(")
second_cubin_launch = kernel_wrapper_source.index(f"{function_name}(", first_cubin_launch + 1)
assert first_cubin_launch < second_cubin_launch
@tilelang.testing.requires_cuda
def test_cutedsl_host_call_sites_ignore_dead_helper_funcs():
"""Verify CuTeDSL launch collection only scans the host entry function."""
_, wrapper = _lower_cutedsl(single_kernel_program(64))
call_sites = wrapper._collect_host_kernel_call_sites()
function_name = call_sites[0]["function_name"]
entry_gvar, entry_func = _host_entry_gvar_and_func(wrapper)
dead_helper = tilelang.tvm.tirx.PrimFunc(
[],
tilelang.tvm.tirx.Evaluate(tilelang.tvm.tirx.call_packed(function_name)),
).with_attr("global_symbol", "dead_helper")
wrapper.host_mod = tilelang.tvm.IRModule(
{
entry_gvar.name_hint: entry_func,
"dead_helper": dead_helper,
}
)
assert wrapper._collect_host_kernel_call_sites() == call_sites
@tilelang.testing.requires_cuda
def test_cutedsl_host_call_sites_require_launch_metadata():
"""Verify missing launch metadata for a real host call site fails fast."""
_, wrapper = _lower_cutedsl(single_kernel_program(64))
function_name = wrapper._collect_host_kernel_call_sites()[0]["function_name"]
for metadata_name in ("block_info", "grid_info", "dynamic_smem_buf"):
original_metadata = getattr(wrapper, metadata_name)
metadata = dict(original_metadata)
metadata.pop(function_name)
try:
setattr(wrapper, metadata_name, metadata)
with pytest.raises(AssertionError, match=f".*{function_name}.*{metadata_name}.*"):
wrapper.update_lib_code(wrapper.source)
finally:
setattr(wrapper, metadata_name, original_metadata)
@tilelang.testing.requires_cuda
def test_cutedsl_adapter_exposes_device_and_host_sources():
"""Verify CuTeDSL adapter exposes device, host, and combined sources."""
_require_cutedsl()
kernel = tilelang.compile(
single_kernel_program(64),
target=CUTEDSL_SM90_TARGET,
execution_backend="cutedsl",
)
device_source = kernel.get_kernel_source(kernel_only=True)
host_source = kernel.get_host_source()
combined_source = kernel.get_kernel_source(kernel_only=False)
assert "@cute.kernel" in device_source
assert "def call(" in host_source
assert "--gpu-arch=sm_90" in host_source
assert "_generate_cubin_if_needed" in host_source
assert device_source in combined_source
assert host_source in combined_source
def test_cutedsl_cache_restore_does_not_fallback_to_host_source(monkeypatch, tmp_path):
"""Verify missing cached generated modules stay distinct from host wrappers."""
from tilelang.jit.adapter.cutedsl.adapter import CuTeDSLKernelAdapter
from tilelang.jit.adapter.cutedsl import adapter as cutedsl_adapter
class FakeLibraryGenerator:
"""Minimal CuTeDSL library generator stub for cache restore tests."""
def __init__(self, target, verbose):
self.target = target
self.verbose = verbose
self.pymodule = object()
def assign_compile_flags(self, compile_flags):
"""Accept compile flags without invoking the real generator."""
self.compile_flags = compile_flags
def load_lib(self, lib_path):
"""Pretend to load a cached Python module from disk."""
self.libpath = lib_path
monkeypatch.setattr(cutedsl_adapter, "CuTeDSLLibraryGenerator", FakeLibraryGenerator)
prim_func = tilelang.tvm.tirx.PrimFunc([], tilelang.tvm.tirx.Evaluate(0)).with_attr("global_symbol", "main")
missing_kernel_py = tmp_path / "missing_kernel.py"
restored = CuTeDSLKernelAdapter.from_database(
params=[],
result_idx=[],
target=CUTEDSL_SM90_TARGET,
func_or_mod=prim_func,
host_kernel_source="# host wrapper",
device_kernel_source="# device kernel",
kernel_lib_path=str(missing_kernel_py),
)
assert restored.get_generated_module_source() is None
assert restored.get_host_source() == "# host wrapper"
assert restored.get_kernel_source(kernel_only=False) == "# device kernel\n\n# host wrapper"
if __name__ == "__main__":
tilelang.testing.main()