"""Tests for CuTeDSL host codegen integration.""" import pytest import torch 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.backend.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 repeated_dynamic_shape_program(): """Create a program where one dynamic symbol appears in multiple params.""" N = T.dynamic("N") @T.prim_func def main( optional: T.Tensor((N,), T.float32), source: T.Tensor((N,), T.float32), out: T.Tensor((N,), T.float32), ): T.evaluate(0) return main def optional_stride_symbol_program(): """Create a program with an optional strided tensor stride symbol.""" N = T.dynamic("N") stride = T.dynamic("stride") @T.prim_func def main( source: T.Tensor((N,), T.float32), optional: T.StridedTensor[(N,), (stride,), T.float32], out: T.Tensor((N,), T.float32), ): T.evaluate(0) return main def optional_shape_symbol_program(): """Create a program with shape symbols that only belong to optional inputs.""" N = T.dynamic("N") M = T.dynamic("M") @T.prim_func def main( source: T.Tensor((N,), T.float32), optional_a: T.Tensor((M,), T.float32), optional_b: T.Tensor((M,), T.float32), out: T.Tensor((N,), T.float32), ): T.evaluate(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_adapter_resolves_dynamic_symbol_from_live_tensor_candidate(): """Dynamic shape resolution should skip optional None tensor params.""" _require_cutedsl() from tilelang.jit.adapter.cutedsl.adapter import CuTeDSLKernelAdapter program = repeated_dynamic_shape_program() adapter = CuTeDSLKernelAdapter.__new__(CuTeDSLKernelAdapter) adapter.ir_module = tilelang.tvm.IRModule({program.attrs["global_symbol"]: program}) adapter.dynamic_symbolic_map, adapter.dynamic_symbolic_order = adapter._process_dynamic_symbolic() assert len(adapter.dynamic_symbolic_order) == 1 dynamic_symbol = adapter.dynamic_symbolic_order[0] assert adapter._resolve_dynamic_symbolic_value(dynamic_symbol, [None, torch.empty(7), torch.empty(7)]) == 7 with pytest.raises(TypeError, match="no live tensor source"): adapter._resolve_dynamic_symbolic_value(dynamic_symbol, [None, None, None]) def test_cutedsl_adapter_allows_optional_stride_symbol_without_live_tensor(): """Dynamic stride-only symbols may be absent for optional tensor params.""" _require_cutedsl() from tilelang.jit.adapter.cutedsl.adapter import CuTeDSLKernelAdapter program = optional_stride_symbol_program() adapter = CuTeDSLKernelAdapter.__new__(CuTeDSLKernelAdapter) adapter.ir_module = tilelang.tvm.IRModule({program.attrs["global_symbol"]: program}) adapter.dynamic_symbolic_map, adapter.dynamic_symbolic_order = adapter._process_dynamic_symbolic() dynamic_symbols = {sym.name: sym for sym in adapter.dynamic_symbolic_order} param_values = [torch.empty(7), None, torch.empty(7)] assert adapter._resolve_dynamic_symbolic_value(dynamic_symbols["N"], param_values) == 7 assert adapter._resolve_dynamic_symbolic_value(dynamic_symbols["stride"], param_values) == 0 def test_cutedsl_adapter_allows_optional_abi_shape_symbol_without_live_tensor(): """Dynamic shape symbols may be absent when only needed as dead ABI args.""" _require_cutedsl() from tilelang.jit.adapter.cutedsl.adapter import CuTeDSLKernelAdapter program = optional_shape_symbol_program() adapter = CuTeDSLKernelAdapter.__new__(CuTeDSLKernelAdapter) adapter.ir_module = tilelang.tvm.IRModule({program.attrs["global_symbol"]: program}) adapter.dynamic_symbolic_map, adapter.dynamic_symbolic_order = adapter._process_dynamic_symbolic() dynamic_symbols = {sym.name: sym for sym in adapter.dynamic_symbolic_order} param_values = [torch.empty(7), None, None, torch.empty(7)] assert adapter._resolve_dynamic_symbolic_value(dynamic_symbols["N"], param_values) == 7 with pytest.raises(TypeError, match="no live tensor source"): adapter._resolve_dynamic_symbolic_value(dynamic_symbols["M"], param_values) assert adapter._resolve_dynamic_symbolic_value(dynamic_symbols["M"], param_values, require_live_shape=False) == 0 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()