import importlib.metadata import os import platform import shutil import statistics import subprocess import sys import tilelang.testing import tilelang.language as T import pytest def kernels_with_pdl_trigger(N, block_size=256, dtype=T.float32): """Create a TileLang kernel that triggers dependent PDL launches.""" @T.prim_func def main( A: T.Tensor((N,), dtype), B: T.Tensor((N,), dtype), ): """Trigger dependent PDL launches after writing the output tensor.""" 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 T.pdl_trigger() return main def kernels_with_pdl_sync(N, block_size=256, dtype=T.float32): """Create a TileLang kernel that waits for PDL dependencies.""" @T.prim_func def main( A: T.Tensor((N,), dtype), B: T.Tensor((N,), dtype), ): """Wait for PDL dependencies before writing the output tensor.""" with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx2,): T.pdl_sync() for i in T.Parallel(block_size): idx = bx2 * block_size + i if idx < N: B[idx] = A[idx] * 2.0 return main def kernels_with_pdl_pipeline(N, block_size=256, dtype=T.float32): """Create a two-kernel TileLang pipeline using PDL trigger and sync.""" @T.prim_func def main( A: T.Tensor((N,), dtype), B: T.Tensor((N,), dtype), C: T.Tensor((N,), dtype), ): """Launch a producer kernel and a PDL-dependent consumer 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 T.pdl_trigger() with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx2,): T.pdl_sync() for i in T.Parallel(block_size): idx = bx2 * block_size + i if idx < N: C[idx] = B[idx] * 2.0 return main def kernels_without_pdl_overlap_window(N, block_size=256, work_iters=8192, dtype=T.float32): """Create a strict same-stream two-kernel baseline for the PDL overlap benchmark.""" @T.prim_func def main( A: T.Tensor((N,), dtype), B: T.Tensor((N,), dtype), C: T.Tensor((N,), dtype), Tail: T.Tensor((N,), dtype), Pre: T.Tensor((N,), dtype), ): """Run producer tail work before the consumer can start.""" 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 B[idx] = A[idx] + 1.0 for i in T.Parallel(block_size): idx = bx * block_size + i x = T.alloc_var(T.float32, init=A[idx]) for _ in T.serial(work_iters): x = x * 1.000001 + 0.000001 Tail[idx] = x with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx2,): for i in T.Parallel(block_size): idx = bx2 * block_size + i y = T.alloc_var(T.float32, init=A[idx]) for _ in T.serial(work_iters): y = y * 1.000002 + 0.000002 Pre[idx] = y for i in T.Parallel(block_size): idx = bx2 * block_size + i C[idx] = B[idx] * 2.0 return main def kernels_with_pdl_overlap_window(N, block_size=256, work_iters=8192, dtype=T.float32): """Create a two-kernel PDL benchmark with real pre-sync overlap work.""" @T.prim_func def main( A: T.Tensor((N,), dtype), B: T.Tensor((N,), dtype), C: T.Tensor((N,), dtype), Tail: T.Tensor((N,), dtype), Pre: T.Tensor((N,), dtype), ): """Trigger the consumer early so its pre-sync work can overlap producer tail work.""" 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 B[idx] = A[idx] + 1.0 T.pdl_trigger() for i in T.Parallel(block_size): idx = bx * block_size + i x = T.alloc_var(T.float32, init=A[idx]) for _ in T.serial(work_iters): x = x * 1.000001 + 0.000001 Tail[idx] = x with T.Kernel(T.ceildiv(N, block_size), threads=block_size) as (bx2,): for i in T.Parallel(block_size): idx = bx2 * block_size + i y = T.alloc_var(T.float32, init=A[idx]) for _ in T.serial(work_iters): y = y * 1.000002 + 0.000002 Pre[idx] = y T.pdl_sync() for i in T.Parallel(block_size): idx = bx2 * block_size + i C[idx] = B[idx] * 2.0 return main def _get_sm90_cuda_device(): """Return an SM90+ CUDA device for runtime PDL tests.""" torch = pytest.importorskip("torch") if not torch.cuda.is_available(): pytest.skip("CuTeDSL PDL runtime test requires CUDA") for device_id in range(torch.cuda.device_count()): if torch.cuda.get_device_capability(device_id) >= (9, 0): return torch.device("cuda", device_id) pytest.skip("CuTeDSL PDL runtime test requires an SM90+ CUDA device") def _package_version(package_name: str) -> str: """Return an installed package version or a readable placeholder.""" try: return importlib.metadata.version(package_name) except importlib.metadata.PackageNotFoundError: return "not-installed" def _run_version_command(command: list[str], preferred_substring: str | None = None) -> str: """Return the first non-empty version command line, or a readable failure.""" executable = shutil.which(command[0]) if executable is None: return "not-found" try: result = subprocess.run( [executable, *command[1:]], check=False, capture_output=True, text=True, timeout=10, ) except (OSError, subprocess.SubprocessError) as err: return f"error: {err}" lines = [line.strip() for line in (result.stdout + result.stderr).splitlines() if line.strip()] if preferred_substring is not None: for line in lines: if preferred_substring in line: return line return lines[-1] if lines else f"exit={result.returncode}" def _run_command_summary(command: list[str]) -> str: """Return all non-empty command output lines joined for compact benchmark logs.""" executable = shutil.which(command[0]) if executable is None: return "not-found" try: result = subprocess.run( [executable, *command[1:]], check=False, capture_output=True, text=True, timeout=10, ) except (OSError, subprocess.SubprocessError) as err: return f"error: {err}" lines = [line.strip() for line in (result.stdout + result.stderr).splitlines() if line.strip()] return "; ".join(lines) if lines else f"exit={result.returncode}" def _print_pdl_benchmark_environment(device): """Print exact local environment details for opt-in PDL benchmark runs.""" torch = pytest.importorskip("torch") print("PDL microbenchmark environment:") print(f" python={sys.version.split()[0]}") print(f" platform={platform.platform()}") print(f" tilelang_package={_package_version('tilelang')}") print(f" tilelang_git={_run_version_command(['git', 'rev-parse', '--short', 'HEAD'])}") print(f" torch={torch.__version__}") print(f" torch_cuda={torch.version.cuda}") print(f" cuda_home={os.environ.get('CUDA_HOME') or os.environ.get('CUDA_PATH') or 'unset'}") print(f" nvcc={_run_version_command(['nvcc', '--version'], preferred_substring='release')}") print(f" nvidia_smi={_run_version_command(['nvidia-smi', '--version'], preferred_substring='NVIDIA-SMI version')}") print(f" nvidia_driver_cuda={_run_version_command(['nvidia-smi', '--version'], preferred_substring='CUDA Version')}") print(f" nvidia_gpus={_run_command_summary(['nvidia-smi', '--query-gpu=driver_version,name,compute_cap', '--format=csv,noheader'])}") print(f" nvidia_cutlass_dsl={_package_version('nvidia-cutlass-dsl')}") print(f" nvidia_cutlass_dsl_libs_base={_package_version('nvidia-cutlass-dsl-libs-base')}") print(f" cuda_python={_package_version('cuda-python')}") print(f" cuda_bindings={_package_version('cuda-bindings')}") print(f" benchmark_device={device.index}:{torch.cuda.get_device_name(device)}") print(f" benchmark_device_capability={torch.cuda.get_device_capability(device)}") print(f" TILELANG_DISABLE_CACHE={os.environ.get('TILELANG_DISABLE_CACHE', '0')}") def _benchmark_kernel_ms(kernel, args, tensors_to_clear, warmup, repeats): """Measure a TileLang kernel with CUDA events and return per-run milliseconds.""" torch = pytest.importorskip("torch") start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) samples = [] for run_id in range(warmup + repeats): for tensor in tensors_to_clear: tensor.zero_() torch.cuda.synchronize() start.record() kernel(*args) end.record() end.synchronize() if run_id >= warmup: samples.append(start.elapsed_time(end)) return samples @tilelang.testing.requires_cuda def test_pdl_trigger(): """Verify CUDA codegen emits the PDL trigger intrinsic.""" N = 64 program = kernels_with_pdl_trigger(N) pdl_kernel = tilelang.compile(program, target={"kind": "cuda", "arch": "sm_90"}) code = pdl_kernel.get_kernel_source() assert "cudaTriggerProgrammaticLaunchCompletion" in code @tilelang.testing.requires_cuda def test_pdl_sync(): """Verify CUDA codegen emits the PDL sync intrinsic without restrict qualifiers.""" N = 64 program = kernels_with_pdl_sync(N) pdl_kernel = tilelang.compile(program, target={"kind": "cuda", "arch": "sm_90"}) code = pdl_kernel.get_kernel_source() assert "cudaGridDependencySynchronize" in code assert "__restrict__" not in code def _lower_cutedsl_for_pdl(program): """Lower a PDL program through CuTeDSL and build its host wrapper.""" try: from cutlass.cute import arch as cute_arch from tilelang.jit.adapter.cutedsl.checks import check_cutedsl_available from tilelang.jit.adapter.cutedsl.wrapper import TLCuTeDSLSourceWrapper from tilelang.backend.target import determine_target check_cutedsl_available() except (ImportError, ModuleNotFoundError, RuntimeError) as err: pytest.skip(f"CuTeDSL is not available: {err}") if not (hasattr(cute_arch, "griddepcontrol_launch_dependents") and hasattr(cute_arch, "griddepcontrol_wait")): pytest.skip("CuTeDSL PDL APIs are not available in this nvidia-cutlass-dsl build (introduced in 4.3.4)") # PDL is an SM90/Hopper feature. Use an explicit arch so this codegen test # does not depend on the default CUDA device, which may be sm_80 on mixed-GPU hosts. target = determine_target({"kind": "cutedsl", "arch": "sm_90"}) 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 test_cutedsl_pdl_codegen_and_launcher_support(): """Verify CuTeDSL PDL lowering and host launcher generation.""" trigger_artifact, _ = _lower_cutedsl_for_pdl(kernels_with_pdl_trigger(64, block_size=64)) assert "tl.griddepcontrol_launch_dependents()" in trigger_artifact.kernel_source sync_artifact, sync_wrapper = _lower_cutedsl_for_pdl(kernels_with_pdl_sync(64, block_size=64)) assert "tl.griddepcontrol_wait()" in sync_artifact.kernel_source assert "use_pdl=True" in sync_wrapper.host_func assert "--gpu-arch=sm_90" in sync_wrapper.host_func launcher_cpp = sync_wrapper.get_launcher_cpp_code() assert "CU_LAUNCH_ATTRIBUTE_PROGRAMMATIC_STREAM_SERIALIZATION" in launcher_cpp assert "cuLaunchKernelEx" in launcher_cpp import tilelang.contrib.cutedsl as tl assert hasattr(tl, "griddepcontrol_launch_dependents") assert hasattr(tl, "griddepcontrol_wait") @tilelang.testing.requires_cuda def test_cutedsl_pdl_runtime_pipeline(): """Run a CuTeDSL PDL pipeline and verify the produced tensors.""" torch = pytest.importorskip("torch") device = _get_sm90_cuda_device() N = 64 with torch.cuda.device(device): _lower_cutedsl_for_pdl(kernels_with_pdl_pipeline(N, block_size=64)) kernel = tilelang.compile( kernels_with_pdl_pipeline(N, block_size=64), target={"kind": "cutedsl", "arch": "sm_90"}, ) a = torch.randn(N, dtype=torch.float32, device=device) b = torch.empty_like(a) c = torch.empty_like(a) kernel(a, b, c) torch.cuda.synchronize(device) ref_b = a + 1.0 ref_c = ref_b * 2.0 tilelang.testing.torch_assert_close(b, ref_b, atol=1e-5, rtol=1e-5) tilelang.testing.torch_assert_close(c, ref_c, atol=1e-5, rtol=1e-5) @tilelang.testing.requires_cuda @pytest.mark.perf def test_cutedsl_pdl_overlap_microbenchmark(): """Benchmark PDL overlap against a strict same-stream serial launch baseline. This is intentionally opt-in via ``--run-perf``. It validates correctness and prints timings, but does not assert a speedup threshold because kernel overlap depends on occupancy, clocks, driver scheduling, and host load. """ torch = pytest.importorskip("torch") device = _get_sm90_cuda_device() block_size = int(os.environ.get("TILELANG_PDL_BENCH_BLOCK_SIZE", "256")) num_blocks = int(os.environ.get("TILELANG_PDL_BENCH_BLOCKS", "1")) work_iters = int(os.environ.get("TILELANG_PDL_BENCH_WORK_ITERS", "8192")) warmup = int(os.environ.get("TILELANG_PDL_BENCH_WARMUP", "20")) repeats = int(os.environ.get("TILELANG_PDL_BENCH_REPEATS", "80")) N = num_blocks * block_size _print_pdl_benchmark_environment(device) print("PDL microbenchmark config:") print(" target=cutedsl sm_90") print(f" num_blocks={num_blocks}") print(f" block_size={block_size}") print(f" work_iters={work_iters}") print(f" warmup={warmup}") print(f" repeats={repeats}") with torch.cuda.device(device): serial_kernel = tilelang.compile( kernels_without_pdl_overlap_window(N, block_size=block_size, work_iters=work_iters), target={"kind": "cutedsl", "arch": "sm_90"}, ) pdl_kernel = tilelang.compile( kernels_with_pdl_overlap_window(N, block_size=block_size, work_iters=work_iters), target={"kind": "cutedsl", "arch": "sm_90"}, ) a = torch.randn(N, dtype=torch.float32, device=device) serial_outputs = [torch.empty_like(a) for _ in range(4)] pdl_outputs = [torch.empty_like(a) for _ in range(4)] # First calls generate CuTeDSL cubins. Keep compilation out of the timing window. serial_kernel(a, *serial_outputs) pdl_kernel(a, *pdl_outputs) torch.cuda.synchronize(device) serial_samples = _benchmark_kernel_ms( serial_kernel, (a, *serial_outputs), serial_outputs, warmup=warmup, repeats=repeats, ) pdl_samples = _benchmark_kernel_ms( pdl_kernel, (a, *pdl_outputs), pdl_outputs, warmup=warmup, repeats=repeats, ) ref_b = a + 1.0 ref_c = ref_b * 2.0 tilelang.testing.torch_assert_close(serial_outputs[0], ref_b, atol=1e-5, rtol=1e-5) tilelang.testing.torch_assert_close(serial_outputs[1], ref_c, atol=1e-5, rtol=1e-5) tilelang.testing.torch_assert_close(pdl_outputs[0], ref_b, atol=1e-5, rtol=1e-5) tilelang.testing.torch_assert_close(pdl_outputs[1], ref_c, atol=1e-5, rtol=1e-5) serial_mean = statistics.mean(serial_samples) pdl_mean = statistics.mean(pdl_samples) serial_median = statistics.median(serial_samples) pdl_median = statistics.median(pdl_samples) print("PDL microbenchmark results:") print(f" serial_mean_ms={serial_mean:.6f}") print(f" serial_median_ms={serial_median:.6f}") print(f" serial_min_ms={min(serial_samples):.6f}") print(f" pdl_mean_ms={pdl_mean:.6f}") print(f" pdl_median_ms={pdl_median:.6f}") print(f" pdl_min_ms={min(pdl_samples):.6f}") print(f" speedup_mean={serial_mean / pdl_mean:.6f}") print(f" speedup_median={serial_median / pdl_median:.6f}") if __name__ == "__main__": tilelang.testing.main()