mirror of
https://github.com/tile-ai/tilelang.git
synced 2026-10-24 07:04:00 +08:00
* [Target] Add TileLang target registry * extend * fix gitignore * [Backend] Register target and execution backend resolution * [Backend] Remove JIT execution backend shim * [Backend] Move target helpers under backend * [Backend] Use registration order for target detection * [Backend] Use registration order for execution backend * [Backend] Split target helpers by backend * [Backend] Remove supported target registry * remove lazy detector * fix debug print
446 lines
17 KiB
Python
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={"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()
|