IR Lower TraceΒΆ
IR Lower Trace shows how TileLangβs TIR changes as compiler passes run. It transparently captures the IR before and after every pass in the compilation pipeline β including the final codegen step that produces C/CUDA/HIP source β and renders a human-readable diff report in the terminal and/or a self-contained HTML page. No code changes are required: enabling a single environment variable is enough.
IR Lower Trace is the recommended successor to Pass Diff. Compared with
the older TILELANG_PASS_DIFF tool, it adds:
Phase context β each pass is tagged with its pipeline phase (e.g.
pipeline_c,phase1_...), so you can tell which backend stage a pass belongs to.Codegen capture β the final TIR-to-source lowering is recorded, and the generated C/CUDA/HIP code is dropped to disk for inspection or editing.
Edit-and-recompile workflow β edit the generated codegen source on disk and rerun; your edits are injected back into compilation (with conflict detection).
Per-compilation isolation β each kernel compilation owns its records, pass indices, pipeline scopes, and report lifecycle. Concurrent kernels do not write into one shared in-memory trace.
Raw
.tirdumps β before/after IR for every pass is written to disk, keyed by phase and pass index.Crash-safe incremental HTML β the report is flushed after every pass, so partial results survive even if the process crashes.
Enhanced HTML report β sidebar pass navigation, status dots, phase tabs,
j/kkeyboard navigation,Shift+Eglobal expand,F7manual alignment, dark/light theme.
Observe the Lowering PipelineΒΆ
The simplest way to enable IR Lower Trace is to set the TL_LOWER_TRACE
environment variable before running your script:
# HTML report (default when set to 1/on/true/yes)
TL_LOWER_TRACE=1 python3 my_script.py
# Colored diff printed to the terminal only
TL_LOWER_TRACE=terminal python3 my_script.py
# Both terminal output and HTML report
TL_LOWER_TRACE=both python3 my_script.py
# Disabled (default β zero overhead, no patching)
python3 my_script.py
TileLang reads this setting when tilelang is imported and registers a tool
factory for future compilation sessions. It does not patch TVMβs default
PassContext or global codegen functions. To change the setting after import,
call enable() programmatically.
When HTML output is enabled, a stable symlink <script_dir>/report.html is
maintained and points to the latest runβs report. Open it directly in a browser:
# typical location
open tmp/lower_trace_dir/my_script/report.html
Variable |
Description |
Default |
|---|---|---|
|
Enable tracing. Values: |
off |
|
Base output directory for all trace artifacts |
|
Output Directory StructureΒΆ
A single run produces the following layout under TL_LOWER_TRACE_DIR:
<TL_LOWER_TRACE_DIR>/
βββ <script_name>/ # derived from sys.argv[0], e.g. "my_script"
βββ report.html # symlink β latest run's report
βββ codegen_device.cpp # generated device source (editable, see below)
βββ codegen_device.cpp.original # device baseline for edit/recompile workflow
βββ codegen_device.cpp.latest # latest generated device source
βββ codegen_host.cpp # generated CPU wrapper, when host codegen runs
βββ codegen_host.cpp.original # independent host baseline
βββ codegen_host.cpp.latest # latest generated host source
βββ .run_records/
βββ run_<YYYYMMDD_HHMMSS_ffffff>_<pid>_<unique-id>/
βββ report.html # this run's full report
βββ pipeline_c/ # one subdir per phase (example)
β βββ 00_BindTarget_before.tir
β βββ 00_BindTarget_after.tir
β βββ 01_Simplify_before.tir
β βββ 01_Simplify_after.tir
βββ phase2_optimize/ # another phase (illustrative)
β βββ ...
βββ codegen/
β βββ 42_codegen_device_before.tir
β βββ 42_codegen_device_after.cpp
β βββ 49_codegen_host_before.tir
β βββ 49_codegen_host_after.cpp
βββ unscoped/ # passes outside any pipeline window
βββ ...
Each phase gets its own subdirectory; passes are numbered globally (00_, 01_,
β¦) so ordering is unambiguous. Codegen records write the after artifact as
.cpp (the generated source), while ordinary passes use .tir for both sides.
Both device and host records belong to the codegen phase, with names
codegen_device and codegen_host respectively. Host files and records appear
only when the execution backend invokes host codegen. Each working file has its
own baseline, latest output, and backups; host codegen cannot overwrite the
device working file or its editing state.
The standard backend wrappers explicitly identify each event as device or host
codegen. Generic instrumentation calls that omit role leave it unspecified
(None): these retain the codegen record name and default codegen.cpp working
file. An explicit codegen_output path is used unchanged for such events.
Custom callers should supply a role to distinguish device and host output;
unspecified events share the legacy working file and edit state.
Programmatic APIΒΆ
For fine-grained control, IR Lower Trace exposes two layers of API.
One-shot: lower_trace()ΒΆ
Diff a fixed chain of passes against an IR module without installing any global hook:
from tilelang.tools import lower_trace as lt
from tilelang import tvm
import tilelang.transform as transform
# Diff a single pass
results = lt.lower_trace(func, transform.Simplify(), mode="terminal")
# Diff a named chain, write an HTML report
results = lt.lower_trace(
func,
[
("Annotate", tvm.tirx.transform.AnnotateDeviceRegions()),
("Split", tvm.tirx.transform.SplitHostDevice()),
("ThreadSync", transform.ThreadSync("shared")),
],
mode="both",
html_path="my_diff.html",
)
Parameter |
Description |
Default |
|---|---|---|
|
A |
Required |
|
A single pass, a list of passes, or a list of |
Required |
|
|
|
|
Number of context lines in the unified diff |
|
|
Output path for the HTML report |
|
Returns a list[dict] with one entry per pass step, each containing name,
before_script, after_script, diff_lines, insertions, deletions, and
changed.
Compilation instrumentation: enable() / disable()ΒΆ
To trace the entire compilation pipeline of a real kernel (what the environment variable does, but programmatically):
from tilelang.tools import lower_trace as lt
# Enable tracing for the rest of the process.
lt.enable(mode="both")
# ... run tilelang.compile() / kernel compilation ...
All three parameters of lt.enable() are optional β mode, trace_dir, and
codegen_output fall back to the TL_LOWER_TRACE / TL_LOWER_TRACE_DIR env
vars (or sensible defaults) when omitted.
Parameter |
Description |
Default |
|---|---|---|
|
Force a trace mode: |
|
|
Base output directory. |
|
|
Device working-file path; a host working-file path is derived when host codegen runs. Pass |
|
An explicit codegen_output="foo.cpp" writes device source to foo.cpp and
host source to foo_host.cpp. The host path inserts _host before the final
extension: foo.kernel.cpp becomes foo.kernel_host.cpp, and a path without an
extension, such as foo, becomes foo_host. Each path has its own .original,
.latest, and backup files. Setting codegen_output=None suppresses these
stable working files and their edit state, while codegen records still appear
in the trace and raw snapshots remain available.
enable() is idempotent β calling it multiple times is safe. Its values are
snapshotted when a compilation begins, so changing them cannot mutate a kernel
that is already compiling.
When to use disable()ΒΆ
Function |
When to call |
What it does |
|---|---|---|
|
You want to disable tracing for subsequent compilations within the same process (e.g. a long-running service that only traces the first kernel) |
Unregisters the tool factory. A compilation already in progress keeps its own session and completes normally. |
from tilelang.tools import lower_trace as lt
lt.enable(mode="both")
# First kernel β traced.
kernel1 = tilelang.compile(func_a)
# Second kernel β traced into its own session and report automatically.
kernel2 = tilelang.compile(func_b)
# Optional: disable tracing for any further compilations.
lt.disable()
Note
If lt.disable() is not called, tracing stays active for future compilations.
Each HTML report is finalized when its owning compilation exits, including when
that compilation raises.
HTML Report FeaturesΒΆ
The HTML report is a single self-contained file (no external assets) providing:
Sidebar with per-pass navigation, status dots (β changed / β no-op / β failed / β codegen), and
+/βline-count statistics. Collapsible and drag-resizable.Phase tabs to filter passes by pipeline phase.
Summary bar with clickable filter badges (changed / failed / codegen).
Side-by-side diff with GitHub-style coloring, word-level inline highlighting, and collapsible context (
ββ Expandbuttons reveal hidden equal lines).Keyboard navigation β
j/kto move between passes,Shift+Eto expand/collapse all,F7for Beyond-Compare-style manual alignment.Dark/Light theme toggle persisted via
localStorage.Copy buttons for before/after IR of any pass.
Error boxes β a failed pass shows its exception message alongside the IR before the crash.
IR Lower Trace HTML report.ΒΆ
Codegen Source Capture & Edit-Recompile WorkflowΒΆ
When tracing is enabled, device and host codegen steps (TIR β C/CUDA/HIP/β¦) are intercepted separately. Device source runs on the GPU or NPU and contains the computation, device memory transfers, and synchronization. Host source runs on the CPU and wraps runtime operations such as argument checks, output allocation, and launching the device kernel.
In html/both mode, the generated sources are written to
<script_dir>/codegen_device.cpp and <script_dir>/codegen_host.cpp by default.
For CUDA and Ascend, tvm_ffi invokes both device and host codegen; nvrtc and
cython do not invoke a separate host codegen step at this layer. Their runtime
adapters still provide CPU-side launch logic. Only backends that invoke host
codegen produce codegen_host.cpp.
To support editing the generated code and re-running with your edits applied,
IR Lower Trace maintains three cooperating files for each codegen role.
The device files are shown below; the host files use the same suffixes on
codegen_host.cpp and keep an independent edit state:
File |
Role |
|---|---|
|
Working copy β user-editable. Edits are recompiled when the backend supports the workflow described below. |
|
Baseline β the codegen snapshot the working copy was last synced from. Written only on init or re-sync, never blindly overwritten. |
|
Latest codegen output β the actual output of the most recent run, overwritten every run for diff reference. |
On each run a three-way comparison (baseline / working copy / current codegen output) decides how to proceed:
Situation |
Working copy vs |
|
Action |
Console tag |
|---|---|---|---|---|
No change |
identical |
identical |
Compile with codegen output as-is |
β |
Codegen changed only |
identical |
differs |
Regenerate the working copy and |
|
User edited only |
differs |
identical |
Inject the working copy ( |
|
Both changed, working == latest |
differs |
differs (working matches latest) |
Advance baseline; use working copy |
|
Both changed, working != latest |
differs |
differs (working differs from latest) |
CONFLICT β back up working copy β |
|
First run (no baseline) |
β |
β |
Initialise |
(init) |
Working copy exists without baseline |
β |
β |
Back up the pre-existing working file β |
|
Note on
.bakfiles: The backups created byCONFLICTandINIT-BACKUPserve different purposes.INIT-BACKUPpreserves a pre-existing working file of unknown origin before the trace tool takes it over.CONFLICTpreserves the userβs edits before a codegen change overwrites them. RecoverCONFLICTedits withdiff codegen_device.cpp.original.bak codegen_device.cpp.bakfor the device working copy, or the equivalent command withcodegen_host.cppfor the host working copy. These backup files are created only when needed.
Typical WorkflowΒΆ
Inspect β Run once with
TL_LOWER_TRACE=1. Opencodegen_device.cppto read the generated source.Edit β Modify
codegen_device.cpp(e.g. add aprintf, tweak a loop). Do not touch.original.Rerun β Run again. Because
codegen_device.cppdiffers from.originalbut codegen output is unchanged, youβll seePATCHED from β¦/codegen_device.cppand your edited source is compiled.Iterate β Keep editing and rerunning. Each run re-injects your working copy.
If codegen itself changes (e.g. you modified the TileLang program) β two outcomes:
If your edits happen to match the new codegen output β
SYNCED(baseline advances, your edits preserved).If both your edits and codegen changed and they differ β
CONFLICT. Your working copy is backed up tocodegen_device.cpp.bakand the old baseline tocodegen_device.cpp.original.bak. Recover your edits withdiff codegen_device.cpp.original.bak codegen_device.cpp.bak, then re-apply them against the freshly regeneratedcodegen_device.cpp.
Note
Backend requirements for edit-and-recompile. The edit-and-recompile
workflow requires a source-compiling execution backend β nvrtc, cython, or
cutedsl. These backends use *_without_compile codegen FFIs that produce
source-only modules, then compile the (edited) source string at runtime via
NVRTC / Cython / CuTeDSL.
The default tvm_ffi backend pre-compiles device code to a binary (PTX/hsaco)
from TIR during codegen. When the tvm_ffi backend is active and you edit
codegen_device.cpp, youβll see a NOTE message indicating that your edits are
recorded in the trace for diff viewing but were not recompiled. To use
edit-and-recompile, switch to a source-compiling backend:
# For CUDA targets:
tilelang.compile(..., execution_backend="nvrtc")
# For HIP targets:
tilelang.compile(..., execution_backend="cython")
Capturing host source and preserving host edits does not add host edit-and-recompile support; the existing backend requirements still apply.
How It WorksΒΆ
Instrumentation sessions are owned by complete compiler entry points such as
tilelang.compile(), tilelang.lower(), and grouped compilation. Lower-level
pipeline and codegen components contribute events to the active session but do
not create standalone sessions themselves.
IR Lower Trace uses three cooperating layers:
TVM
PassInstrumentβ before/after callbacks capturestr(mod), compute+/βline counts, and append aLowerRecord. The shared nested callback stack records depth and parent relationships, including passes invoked internally by another TVM pass. Passes outside a pipeline window are tagged with theunscopedphase.Explicit
PassPipeline.lowerscope β contributes a context-local phase to the caller-owned session so passes in a backend pipeline are grouped under labels such aspipeline_c. The pipeline does not create or finalize the compile session. This is a normal pipeline boundary, not a class monkey-patch.Explicit backend codegen hook β TileLangβs device/host codegen registry routes each
target.build.*call through the active compile session. This captures final TIR β source lowering, labels events as device or host, and drives each roleβs three-file edit-recompile workflow without replacing a process-global FFI. When no session is active, the registry invokes the underlying codegen directly.
Pass records are appended at runtime (not pre-registered), so conditional
passes that are skipped at runtime β e.g. LetInline when
should_force_let_inline() is False β simply do not appear, leaving no
phantom/skipped slots. The HTML report is flushed incrementally after every
pass (O(n) total cost), so partial results survive even a crash or SIGKILL.
When one logical compilation uses multiple PassContexts (for example grouped lowering followed by device and host codegen), they obtain fresh callback objects from the same compile session. Their records share one monotonic index inside that session. A different compilation starts from index zero with a new record list.
Note
enable() affects TileLang-managed compilation sessions only; it never mutates
the PassContext that happens to be current at enable time. For a standalone TVM
PassContext, lt.create_pass_instrument() remains available for manual use.
Capturing IR and, in HTML mode, rewriting the report after every pass adds
debugging overhead. Leave the feature disabled for normal builds and benchmarks.
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.
IR Lower Trace 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.
TipsΒΆ
Use
terminalmode for quick checks β the colored diff prints as passes run, so you can see changes in real time.Use
htmlmode for thorough analysis β navigate across many passes, expand hidden context, and copy IR snippets.Combine with
TL_LOWER_TRACE_DIRto direct reports to a specific location, e.g. when running in CI or comparing across runs.The hook captures all passes in the lowering pipeline, including those triggered internally by
tilelang.compile(). This makes it useful for understanding the full compilation flow.If you previously used
TILELANG_PASS_DIFF, switch toTL_LOWER_TRACEβ it is a strict superset and is the tool that receives future improvements.