Files
tilelang/testing/python/debug/test_pass_visualizer.py
T
Shuyi LinandClaude 16af6f805e [Feature][Tool] Add pass_visualizer: structure-tree pass browser (#2449)
* [Feature] Add pass_visualizer: structure-tree pass browser

Add an interactive, pass-by-pass IR structure-tree visualizer under
tilelang/utils/pass_visualizer. It complements the existing text-level
pass_diff tool by rendering the SBlock structure tree and expanding tile
ops by field name, with per-class operator highlighting (tile op / sync /
lowered hardware intrinsic) across the CUDA lowering prologue.

Includes a CLI entry point, an example gemm_relu kernel, a pytest suite,
and a docs section contrasting it with pass_diff.

Co-Authored-By: Claude <noreply@anthropic.com>

* Move pass_visualizer to tilelang/tools and fix prologue + review issues

- Move package from tilelang/utils to tilelang/tools per review
- Add missing MaterializeKernelLaunch pass (fixes CUDA CI modulo-by-zero)
- Escape "</" in embedded JSON to prevent <script> injection in HTML report
- Use explicit utf-8 encoding for source/output file I/O
- Guard _fmt_shape against symbolic shapes and validate module spec

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-06 15:39:54 +08:00

165 lines
5.2 KiB
Python

# type: ignore
"""Tests for the pass_visualizer debugging tool.
Covers:
- Core helpers: build_module, build_pass_stages, inspect_structure
- Structure-tree capture and per-pass diffing (build_pass_data via viewer)
- HTML / text emission (emit_html, emit_txt)
These run the CUDA lowering prologue on a small kernel. An explicit cuda target
is passed so the pipeline does not depend on auto target detection.
"""
import tilelang
import tilelang.testing
import tilelang.language as T
from tilelang import tvm
from tilelang.tools.pass_visualizer import (
build_module,
build_pass_stages,
inspect_structure,
)
from tilelang.tools.pass_visualizer.viewer import (
_capture_tree,
build_pass_data,
emit_html,
emit_txt,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _gemm_relu_kernel():
"""A small fused GEMM + bias + ReLU @tilelang.jit kernel."""
@tilelang.jit(out_idx=[-1])
def gemm_relu(M, N, K, block_M, block_N, block_K, dtype="float16", accum_dtype="float32"):
@T.prim_func
def main(
A: T.Tensor((M, K), dtype),
B: T.Tensor((K, N), dtype),
bias: T.Tensor((N,), dtype),
C: T.Tensor((M, N), dtype),
):
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
A_shared = T.alloc_shared((block_M, block_K), dtype)
B_shared = T.alloc_shared((block_K, block_N), dtype)
C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
T.clear(C_local)
for k in T.Pipelined(T.ceildiv(K, block_K), num_stages=3):
T.copy(A[by * block_M, k * block_K], A_shared)
T.copy(B[k * block_K, bx * block_N], B_shared)
T.gemm(A_shared, B_shared, C_local)
for i, j in T.Parallel(block_M, block_N):
C_local[i, j] = T.max(C_local[i, j] + bias[bx * block_N + j], 0)
T.copy(C_local, C[by * block_M, bx * block_N])
return main
return gemm_relu
def _build_small_module():
"""Build an IRModule + resolved cuda target for the small kernel."""
kernel = _gemm_relu_kernel()
func = kernel.get_tir(M=128, N=128, K=128, block_M=64, block_N=64, block_K=32)
cuda_target = tvm.target.Target("cuda", host="llvm")
return build_module(func, target=cuda_target)
# ---------------------------------------------------------------------------
# Core helpers
# ---------------------------------------------------------------------------
@tilelang.testing.requires_cuda
def test_build_pass_stages_nonempty():
"""build_pass_stages returns an ordered list of (name, transform) pairs."""
_mod, target = _build_small_module()
stages = build_pass_stages(target)
assert len(stages) > 0
names = [name for name, _ in stages]
# The prologue must run LayoutInference and LowerTileOp.
assert "LayoutInference" in names
assert "LowerTileOp" in names
for name, transform in stages:
assert isinstance(name, str)
assert callable(transform)
@tilelang.testing.requires_cuda
def test_inspect_structure_renders_tree(capsys):
"""inspect_structure prints the SBlock tree with tile ops expanded by field."""
mod, _target = _build_small_module()
inspect_structure(mod)
out = capsys.readouterr().out
assert "PrimFunc" in out
assert "SBlock" in out
# gemm is expanded by field name, not printed as one positional line.
assert "T.gemm" in out
assert "a_region" in out
@tilelang.testing.requires_cuda
def test_capture_tree_returns_lines():
"""_capture_tree turns inspect_structure output into a list of text lines."""
mod, _target = _build_small_module()
lines = _capture_tree(mod)
assert isinstance(lines, list)
assert len(lines) > 0
assert any("PrimFunc" in ln for ln in lines)
# ---------------------------------------------------------------------------
# build_pass_data + emission (file-driven)
# ---------------------------------------------------------------------------
@tilelang.testing.requires_cuda
def test_build_pass_data_and_emit(tmp_path):
"""End-to-end: run the example kernel through the pipeline and emit HTML/txt."""
import os
kernel_path = os.path.join(
os.path.dirname(tilelang.tools.pass_visualizer.__file__),
"examples",
"gemm_relu.py",
)
with open(kernel_path) as f:
source = f.read()
kwargs = {"M": 128, "N": 128, "K": 128, "block_M": 64, "block_N": 64, "block_K": 32}
name, stages = build_pass_data(kernel_path, None, "auto", kwargs, source)
assert name == "gemm_relu"
# source + (input) + at least one pass.
assert len(stages) >= 3
for st in stages:
assert "name" in st and "flag" in st and "rows" in st
flags = {st["flag"] for st in stages}
assert "source" in flags
assert "input" in flags
html = emit_html(name, stages)
assert "Pass browser" in html
assert "gemm_relu" in html
txt = emit_txt(name, stages)
assert "kernel: gemm_relu" in txt
assert "T.gemm" in txt
if __name__ == "__main__":
tilelang.testing.main()