Files
tilelang/testing/python/language/test_tilelang_language_pdl.py
T
b939fa0137 [TIR][IR] Update to use tirx (#2216)
* [Feature] Implement Hoist Loop-Invariant If Pass

- Added a new pass to hoist loop-invariant if statements out of loops, improving optimization opportunities.
- Introduced classes for collecting and checking conditions, as well as for rewriting statements.
- Integrated the new pass into the optimization pipeline and provided a corresponding API for usage.

* Add tilelang semantics guide to programming guides (#1740)

* Add CallNodeChecker to Loop Unswitching Pass and integrate tilelang cache disabling in benchmark script

- Introduced CallNodeChecker class to identify CallNode expressions in loop conditions, enhancing loop-invariant checks.
- Updated IsLoopInvariant function to reject conditions containing CallNodes, preventing potential side effects.
- Added tilelang.disable_cache() in benchmark_mha_sink_fwd.py to optimize performance during benchmarking.

* lint fix

* lint fix

* Enhance Loop Unswitching Pass with Let Binding Support

- Added support for Let-bound variables in the WrittenBufferReadChecker to improve buffer read checks.
- Introduced UsesLoopVarThroughLetBindings function to check if conditions depend on loop variables through Let bindings.
- Updated IsLoopInvariant function to account for Let bindings when determining loop invariance.
- Enhanced HoistableIfFinder to track Let bindings for variables bound to BufferLoad expressions.
- Added debug print statements in the OptimizeForTarget function to visualize the module state before and after loop unswitching.

* fix

* lint fix

* Enhance Loop Unswitching with CallNode Exclusion Check

- Added CallCheckerExcludingIf class to ensure function calls outside of hoisted if statements are identified, preventing potential synchronization issues during loop unswitching.
- Updated loop unswitching logic to incorporate the new call checker, enhancing safety and correctness.
- Integrated debug print statements in OptimizeForTarget to visualize module state before and after loop unswitching.
- Disabled tilelang cache in the benchmark script for improved performance.

* Update TVM subproject to latest commit and remove debug print statements in OptimizeForTarget function

* Add support for disabling loop unswitching optimization

- Introduced a new configuration option `tl.disable_loop_unswitching` to allow users to disable the loop unswitching optimization.
- Updated the Loop Unswitching pass to check this configuration and return the original function if the option is enabled.
- Added relevant documentation in the PassConfigKey enumeration for clarity.

* update tvm

* Refactor: Update TileLang to use tirx instead of tir

- Removed references to `tvm.tir` and replaced them with `tvm.tirx` across various files, including examples and backend operations.
- Updated target configuration in documentation to reflect the new usage of target config dictionaries instead of CLI-style strings.
- Cleaned up `pyproject.toml` and `load_tvm.cmake` by removing obsolete paths.
- Enhanced examples for dequantization and GEMM operations to utilize the new `tirx` constructs.
- Adjusted various backend operations to ensure compatibility with the new `tirx` namespace.

* pass fix

* Update submodule 'tvm' to latest commit f24c7b19

* Apply formatting fixes

* Update CMakeLists.txt to set HIDE_PRIVATE_SYMBOLS for TileLang and modify target_host initialization in lower.py for compatibility with TVM's target handling.

* Refactor CMakeLists.txt and kernel_cache.py to update target names and library dependencies for TVM integration. Adjusted output targets and library names for better compatibility across platforms.

* Update repair-wheel-command in pyproject.toml to exclude libz3.so with wildcard for better compatibility with dependency packages.

* Fix macOS Z3 dylib lookup

* Move macOS Z3 install-name fix to install step

* Update TVM submodule for runtime import fix

* Update ROCm backend for TVM module factory changes

* Fix build-tree Z3 runtime lookup

* Fix macOS build-tree Z3 lookup

* Support legacy target option strings

* Normalize CUDA target in transform test

* Qualify Metal stream FFI lookup

* Fix PyPI Z3 packaging links

* Update TVM for Windows DLL lookup

* Register Windows dependency DLL directories

* Bump tvm-ffi to match tirx

* tvm update

* Apply lint fixes

* Enhance reduction operations with support for nan propagation and vectorization. Introduce new functions for preferred vectorized size and update existing reduction logic to handle packed operations for bfloat16 and float16 types. Add nan-aware min and max operations in CUDA and ROCm backends, and update related tests to validate functionality.

* Refactor ReduceLowerer: Remove unnecessary buffer allocation logic for packed operations in reduce.h

* Normalize PyPI Z3 dylib before linking

* Drop legacy target option string support

* Fix type reference for barrier annotations in CUDA copy operations. Updated `BufferLoadNode` to `tirx::BufferLoadNode` in `GetBarrier` and `LowerCluster` methods to ensure compatibility with recent changes in the TIR API.

---------

Co-authored-by: Yu Cheng <yu.cheng@pku.edu.cn>
Co-authored-by: Yichen Yan <oraluben@outlook.com>
2026-05-20 15:38:13 +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={"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.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({"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()