Files
tilelang/testing/python/language/test_tilelang_language_pdl.py
T
Jayce SuandJayce 2451f8d176 [CuTeDSL] Add PDL codegen and launcher support (#2220)
* [CuTeDSL] Support CUTLASS DSL 4.5 setup

Update the CUDA test requirements to the 4.5.0 CuTeDSL wheel stack and keep the runtime compatibility check accepting older supported CuTeDSL versions. Also accept the relocated Constexpr export while retaining the fallback for older wheels.

The 4.5.0 pin is a CI/test environment choice. CuTeDSL PDL APIs were introduced upstream in 4.3.4, so the pin should not be read as the minimum PDL version.

* [CuTeDSL] Wire PDL lowering and launch support

TileLang PDL intrinsics were already available for the CUDA backend, but the CuTeDSL backend did not lower them to cute.arch.griddepcontrol_* or enable PDL on generated kernel launches.

Lower T.pdl_trigger() and T.pdl_sync() to the CuTeDSL griddepcontrol APIs, re-export those APIs from tilelang.contrib.cutedsl when available, and pass use_pdl / programmatic stream serialization in Python and C++ launchers. Add CuTeDSL tests that cover device lowering and launcher generation.

---------

Co-authored-by: Jayce <jayce.su@enflame-tech.com>
2026-05-19 17:15:55 +08:00

446 lines
17 KiB
Python

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="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="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.utils.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("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="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 -arch=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="cutedsl -arch=sm_90",
)
pdl_kernel = tilelang.compile(
kernels_with_pdl_overlap_window(N, block_size=block_size, work_iters=work_iters),
target="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()