* [feat] add lower-trace support for debugging * [fix] address review: rebase, doc migration, lower_trace robustness Address CodeRabbit + SiriusNEO review feedback on #2470: Blocking (SiriusNEO): - Rebase onto tile-ai/main (resolve docs/ + tilelang/__init__.py conflicts). - Move IR Lower Trace doc from docs/tutorials/ to docs/tools/lower_trace.md (alongside pass_diff.md); add index entry and 'superseded' pointer on pass_diff.md; cross-link from the tutorial. CodeRabbit Major: - lower_trace/__init__.py: finally block uses active_exc pattern so a generate_html() failure can no longer mask the real lowering exception. - lower_trace/html.py: sanitize rec.phase into DOM-safe IDs via _safe_id() before embedding in id="..." / selectors (avoids markup/JS breakage and local HTML injection from special characters). CodeRabbit Minor: - test_lower_trace.py: restore caller's TL_LOWER_TRACE* env after the guarded tilelang import instead of permanently clearing it. - lower_trace/__init__.py: terminal mode prints a FAILED stanza (pass name, exception, pre-pass IR) before re-raising so the failing step is visible. - lower_trace/core.py: _get_codegen_output_path() no longer auto-writes codegen.cpp in terminal-only mode (explicit override still honored). - lower_trace/html.py: expandAll()/collapseCtx() null-guard the table lookup so a changed diff with no table fails safe. Cleanup: - Add docstrings to test helpers and tools.__getattr__ (coverage now 94%+). - ruff check + format clean. * [Docs] Add lower trace to tools sidebar * [fix] route lower-trace env vars through tilelang.env Register TL_LOWER_TRACE / TL_LOWER_TRACE_DIR as EnvVar in the central Environment class with get_lower_trace_mode()/get_lower_trace_dir() parsers, mirroring the existing TILELANG_PASS_DIFF convention, instead of reading os.environ directly in the lower_trace module. * [fix] simplify lower_trace lazy-import in tools __getattr__ Drop the redundant globals() cache assignment (import_module already binds the submodule onto the parent package) and fix the docstring: the module is imported lazily for cost, not to defer monkey-patch side effects, which happen in enable(). Note why importlib is required over "from . import" (the latter re-enters __getattr__ and recurses). --------- Co-authored-by: caojian5 <caojian5@huawei.com> Co-authored-by: SiriusNEO <chaofan@deepseek.com>
5.3 KiB
Pass Diff
:::{admonition} Superseded — use IR Lower Trace :class: warning
This tool has been superseded by IR Lower Trace (TL_LOWER_TRACE), documented
in {doc}lower_trace. New users should use IR Lower Trace directly — it provides
phase context, codegen capture, multi-run accumulation, and an enhanced HTML
report. TILELANG_PASS_DIFF is retained only for backward compatibility.
:::
Pass Diff shows how TileLang's TIR changes as compiler passes run. It captures the IR before and after each pass, computes a unified diff, and writes the result to the terminal, an interactive HTML report, or both.
Use the environment-variable workflow to observe the complete lowering pipeline without modifying a program. Use the Python API when you want to apply and inspect a specific pass sequence.
Observe the Lowering Pipeline
Set TILELANG_PASS_DIFF before starting the Python process:
# Print colored diffs while passes run.
TILELANG_PASS_DIFF=terminal python my_script.py
# Write an HTML report.
TILELANG_PASS_DIFF=html python my_script.py
# Print terminal diffs and write an HTML report.
TILELANG_PASS_DIFF=both python my_script.py
TileLang reads this setting when tilelang is imported and installs a process-
wide hook around TVM pass execution. Setting the variable after importing
TileLang does not enable the hook for that process.
| Variable | Accepted values | Default |
|---|---|---|
TILELANG_PASS_DIFF |
terminal, html, both; 1, true, yes, and on also select terminal output |
0 (disabled) |
TILELANG_PASS_DIFF_OUTPUT |
Directory for pipeline HTML reports | tmp/pass_diff_output |
False-like values (0, false, no, off, or an empty string) disable the
hook. An unrecognized nonempty value falls back to terminal output.
HTML reports use a timestamped filename such as
pass_diff_20260710_153000.html. The report is refreshed after each pass so a
partial result is available even during a long compilation, then finalized at
process exit.
:width: 600
:alt: Pass Diff HTML report showing TIR before and after a compiler pass
:align: center
Pass Diff HTML report.
The HTML viewer provides side-by-side before and after source, collapsible pass sections, expandable unchanged context, copy controls, light and dark themes, and aggregate insertion and deletion counts.
:::{note}
The pipeline hook wraps tvm.ir.transform.Pass.__call__ for the entire process,
so it observes TVM passes outside the immediate tilelang.compile() call as
well. Capturing IR and, in HTML mode, rewriting the report after every pass adds
debugging overhead. Leave the feature disabled for normal builds and benchmarks.
:::
Compare Selected Passes
The pass_diff Python API accepts a PrimFunc or IRModule and applies the
provided passes in order:
import tilelang
from tilelang import tvm
from tilelang.utils.pass_diff import pass_diff
steps = pass_diff(
func,
[
("AnnotateDeviceRegions", tvm.tirx.transform.AnnotateDeviceRegions()),
("SplitHostDevice", tvm.tirx.transform.SplitHostDevice()),
("ThreadSync", tilelang.transform.ThreadSync("shared")),
],
mode="both",
context=5,
html_path="tmp/selected_passes.html",
)
A single unnamed pass is also accepted:
steps = pass_diff(func, tilelang.transform.ThreadSync("shared"))
| Parameter | Description | Default |
|---|---|---|
func_or_mod |
Starting PrimFunc or IRModule; a PrimFunc is wrapped as main |
Required |
passes |
One pass, a list of passes, or a list of (name, pass) pairs |
Required |
mode |
terminal, html, or both |
terminal |
context |
Unchanged context lines included around each diff hunk | 3 |
html_path |
Output file used by html and both modes |
pass_diff_report.html |
The return value contains one dictionary per pass with these fields:
| Field | Meaning |
|---|---|
name |
Explicit step name, or a name derived from the pass type |
before_script |
Complete TIR script before the pass |
after_script |
Complete TIR script after the pass |
diff_lines |
Unified diff as a list of strings |
insertions |
Number of added lines |
deletions |
Number of removed lines |
changed |
Whether the pass changed the rendered TIR |
For example, a test can assert that a transformation introduced an expected intrinsic:
steps = pass_diff(func, tilelang.transform.ThreadSync("shared"))
assert steps[0]["changed"]
assert "tvm_storage_sync" in steps[0]["after_script"]
Do not enable the process-wide TILELANG_PASS_DIFF hook when using the
programmatic API unless you intentionally want both layers of reporting. The
API invokes each pass normally, so the global hook would capture those same
invocations again.
Choosing an Output Mode
- Use
terminalfor a small number of passes and immediate feedback. - Use
htmlwhen a lowering pipeline produces many changes or you need to compare complete before and after scripts. - Use
bothwhen iterating interactively but retaining a shareable report. - Increase
contextonly for the programmatic API; the process-wide hook uses three context lines.
Pass Diff compares the textual form returned by IRModule.script(). A textual
change is useful evidence about a pass, but it does not by itself establish a
semantic or performance change.