Skip to content

[Feature] add lower-trace support for debugging#2470

Open
erhsh wants to merge 1 commit into
tile-ai:mainfrom
erhsh:main_lower-trace.v3.merge
Open

[Feature] add lower-trace support for debugging#2470
erhsh wants to merge 1 commit into
tile-ai:mainfrom
erhsh:main_lower-trace.v3.merge

Conversation

@erhsh

@erhsh erhsh commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

IR Lower Trace — Feature Overview

Corresponding commit: 66b1a7ad [feat] add lower-trace support for debugging

1. One-line Summary

A zero-intrusion compilation debugging tool: once enabled, it automatically captures the IR before and after every pass (and the final codegen) in the compilation pipeline, renders a per-pass diff in the terminal and as a self-contained HTML report, and supports an "edit the generated codegen source → recompile with edits injected" workflow for verification.

2. Why It's Needed

TileLang programs are lowered through a sequence of compiler passes, each of which may rewrite the IR. When the final result is unexpected, it is hard to tell which pass changed the IR incorrectly, which backend stage a pass belongs to, or what the final generated C/CUDA/HIP source looks like — let alone quickly patch the generated kernel for experimentation without touching the TileLang source. This tool closes those observability gaps: off → zero overhead; on → the whole pipeline is visible.

3. Implementation

Located under tilelang/tools/lower_trace/, the tool consists of three parts: hook installation & record collection (core), unified diff computation (diff), and self-contained HTML rendering (html).

  • Three transparent hooks (all monkey-patched, fully restorable): intercept Pass.__call__ to capture before/after IR for each pass; intercept PassPipeline.lower (new arch) / phase functions (legacy arch, auto-discovered via AST scan + bytecode inspection) to tag each pass with its pipeline stage; intercept codegen FFIs to capture the final TIR → source step.
  • Runtime records: passes are appended at runtime rather than pre-registered, so conditionally skipped passes leave no phantom slots.
  • Crash-safe: the HTML report is flushed incrementally after every pass (with a section cache, O(n) total cost), so partial results survive even a killed process.
  • Multi-run accumulation: repeated compilations in the same process are tagged with run2_/run3_ prefixes and merged into a single report for side-by-side comparison.
  • Codegen edit-recompile: three cooperating files (baseline / working copy / latest output) are compared each run to decide PATCHED / REGENERATED / SYNCED / CONFLICT, so user edits are never silently overwritten (requires a source-compiling backend: nvrtc / cython / cutedsl).
  • Compatibility: works with both new and legacy TVM FFI, both inspect_source/get_source; path components are sanitized against directory escape; disk writes are best-effort (failures warn, never abort compilation).

4. How to Use

Environment variable (simplest):

TL_LOWER_TRACE=1 python3 my_script.py            # HTML report
TL_LOWER_TRACE=terminal python3 my_script.py     # terminal diff only
TL_LOWER_TRACE=both python3 my_script.py         # terminal + HTML

TL_LOWER_TRACE_DIR sets the artifact root (default ./tmp/lower_trace_dir); the report is at <dir>/<script>/report.html.

Programmatic API:

from tilelang.tools import lower_trace as lt

# Trace the entire pipeline (equivalent to the env var)
lt.enable(mode="both")
kernel = tilelang.compile(func)

# Or a one-shot diff over a fixed pass chain (no global hook installed)
lt.lower_trace(func, [pass_a, pass_b], mode="both", html_path="diff.html")

enable() is idempotent (optional mode/trace_dir/codegen_output); reset() clears records but keeps the hook (per-kernel reports in one process); disable() restores all hooks.

edit-recompile flow: run once with tracing on → edit the on-disk codegen.cpp → rerun to compile the edited source; if a TileLang source change regenerates codegen that conflicts with your edits, the working copy is auto-backed-up to .bak for recovery.

HTML report features: sidebar with per-pass navigation + status dots (changed / no-op / failed / codegen) + line stats, stage filtering, side-by-side diff (word-level highlighting, collapsible context), j/k keyboard navigation, dark/light theme, and failed-pass error boxes showing the exception alongside the pre-crash IR.

5. Impact on the Existing Framework (non-intrusive, easy to merge)

This tool is added in a purely additive, non-intrusive way — no existing framework behavior is changed, and merge risk is low:

  • Zero overhead when off: when TL_LOWER_TRACE is unset, the loading logic in tilelang/init.py is skipped entirely — no import, no patching, no runtime cost; tilelang/tools/init.py lazily loads the submodule via __getattr__, leaving the normal startup path untouched.
  • Observation-only when on, semantics unchanged: every hook is a transparent wrapper that captures before/after IR and forwards the original call — pass behavior and return values are not altered. The only exception is source-only codegen FFIs, which return a proxy object to support edit-recompile; binary FFIs conservatively return the real module.
  • Fully restorable: disable() restores Pass.__call__, PassPipeline.lower, and codegen FFIs to their originals and clears all state — no trace left.
  • Mostly new files: changes are concentrated in the new tilelang/tools/lower_trace/ module plus tests and docs; the only edits to existing source are an env-gated loading block and a lazy-load entry in __init__.py — no compilation-core logic is touched.
  • Tolerant persistence: all disk writes are best-effort; failures only warn and never abort compilation.

In short, the feature provides full-pipeline observability without touching the framework's compilation core, is off by default with zero production impact, and is suitable for direct merge.


Appendix: Artifact directory layout

<TL_LOWER_TRACE_DIR>/
└── <script_name>/
    ├── report.html                       # symlink → latest run's report
    ├── codegen.cpp(.original/.latest)    # edit-recompile trio
    └── .run_records/run_<ts>_<pid>/
        ├── report.html
        ├── <phase>/00_<pass>_before.tir / _after.tir
        └── codegen/.._before.tir / .._after.cpp

Summary

  • Added a new lower_trace debugging workflow for TileLang lowering, with hook-based IR capture before/after passes, per-pass diffs, terminal output, and self-contained HTML reporting.
  • Added support for codegen tracing and edit/recompile verification, including baseline/working/latest source tracking and conflict handling for user-edited generated files.
  • Enabled import-time activation via TL_LOWER_TRACE and lazy module loading through tilelang.tools.
  • Expanded docs to replace the older Pass Diff workflow with IR Lower Trace.
  • Added comprehensive tests covering env/API behavior, HTML output, multi-run tracing, codegen patching, failure handling, and import-time activation.

Testing

  • Added testing/python/debug/test_lower_trace.py with broad regression coverage for the new tracing flow.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the TileLang project.

Please remember to run pre-commit run --all-files in the root directory of the project to ensure your changes are properly linted and formatted. This will help ensure your contribution passes the format check.

We appreciate you taking this step! Our team will review your contribution, and we look forward to your awesome work! 🚀

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new tilelang/tools/lower_trace package implementing a zero-intrusion IR debugging tool that monkey-patches TVM/TileLang lowering to capture per-pass IR diffs and codegen source, producing terminal diffs and/or interactive HTML reports. Activated via TL_LOWER_TRACE environment variable at import time or programmatically via enable()/disable()/reset(). The existing "Pass Diff" tool is marked superseded.

Changes

IR Lower Trace Feature

Layer / File(s) Summary
LowerRecord dataclass and diff utilities
tilelang/tools/lower_trace/core.py, tilelang/tools/lower_trace/diff.py
Defines LowerRecord with per-pass metadata fields and global tracing state; adds ANSI unified diff, character-level inline diff, whitespace opcode merging, side-by-side HTML diff table generation, and change counters.
HTML report renderer
tilelang/tools/lower_trace/html.py
Implements render_pass_section and generate_html with ~1800 lines of inline CSS/JS for a self-contained interactive report: theming, badge filtering, sidebar, copy-to-clipboard, and F7 manual alignment mode.
Core monkey-patching engine
tilelang/tools/lower_trace/core.py
Mode parsing, directory management, global pass interception via _traced_pass_call, legacy/new-arch phase and pipeline tracing, codegen FFI wrapping (_wrap_codegen_ffi) with three-way baseline/working/latest comparison and conflict handling, and full enable()/disable()/reset() lifecycle.
One-shot lower_trace() API and module wiring
tilelang/tools/lower_trace/__init__.py, tilelang/tools/__init__.py, tilelang/__init__.py
Public lower_trace() function with pass normalization, application loop, terminal/HTML output; lazy lower_trace attribute on tilelang.tools; import-time activation from TL_LOWER_TRACE env var.
Test suite
testing/python/debug/test_lower_trace.py
756-line test module covering env-mode resolution, pass-chain API, HTML contents/theming, multi-run accumulation, diff regression, phantom-record checks, terminal-mode atexit simulation, failure-mode HTML flush, codegen proxy/conflict/sync, phase management, and subprocess import-time activation.
Tutorial documentation
docs/tutorials/debug_tools_for_tilelang.md
Adds the IR Lower Trace section describing TL_LOWER_TRACE, output directory layout, one-shot and global APIs, codegen capture/edit-recompile workflow, and HTML report features; marks Pass Diff as superseded.

Sequence Diagram

sequenceDiagram
    participant User
    participant tilelang.__init__
    participant lower_trace.enable
    participant tvm.Pass.__call__
    participant _wrap_codegen_ffi
    participant generate_html

    User->>tilelang.__init__: import tilelang (TL_LOWER_TRACE set)
    tilelang.__init__->>lower_trace.enable: enable(mode, trace_dir)
    lower_trace.enable->>tvm.Pass.__call__: monkey-patch → _traced_pass_call
    lower_trace.enable->>target.build.*: monkey-patch → _wrap_codegen_ffi

    User->>tvm.Pass.__call__: pass(mod)
    tvm.Pass.__call__->>_traced_pass_call: snapshot before IR
    _traced_pass_call->>tvm.Pass.__call__: call original pass
    _traced_pass_call->>_traced_pass_call: snapshot after IR, diff, save LowerRecord

    User->>target.build.*: build(mod, target)
    target.build.*->>_wrap_codegen_ffi: capture TIR, invoke original build
    _wrap_codegen_ffi->>_wrap_codegen_ffi: three-way baseline/working/latest comparison
    _wrap_codegen_ffi->>_traced_pass_call: append STATUS_CODEGEN record

    User->>generate_html: atexit / finally block
    generate_html->>generate_html: render all LowerRecords → report.html
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • tile-ai/tilelang#2375: Introduced the earlier pass_diff hook using per-tvm.ir.transform.Pass snapshot/diff interception — the same pattern this PR extends and explicitly supersedes with the new IR Lower Trace tool.

Suggested reviewers

  • cherichy
  • lucifer1004

🐇 A rabbit hops through passes one by one,
Snapping IR diffs before the changes run.
With monkey-patches set and HTML aglow,
Each codegen twist is caught in the trace's flow.
No phantom records, no diff left unseen —
The Lower Trace tool keeps the pipeline clean! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: adding lower-trace debugging support.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@testing/python/debug/test_lower_trace.py`:
- Around line 9-20: The top-level TL_LOWER_TRACE cleanup in the test module
removes the caller’s environment settings for the rest of the pytest process;
update the import guard around the initial tilelang import so it temporarily
clears TL_LOWER_TRACE and TL_LOWER_TRACE_DIR, then restores the previous values
immediately after import. Use the existing import block and the _isolate_env
fixture in test_lower_trace.py as the anchor points, and keep the rest of the
test behavior unchanged.

In `@tilelang/tools/lower_trace/__init__.py`:
- Around line 108-123: The terminal-mode trace handling in the pass loop around
p(mod) re-raises too early, so the failing step and captured pre-pass IR are
never printed when an exception occurs. Update the exception path in the loop
that builds results and calls p(mod) to emit the terminal header or an error
stanza from the except block before re-raising, ensuring mode="terminal" still
shows which pass failed and the available IR context.
- Around line 168-190: The HTML report generation in the `finally` block can
overwrite the original lowering exception if `generate_html` fails, so preserve
the first error from the lowering flow and treat report writing as best-effort.
Update the `finally` logic around `results`, `generate_html`, and the
`LowerRecord` creation so any exception from HTML writing is caught and logged
without replacing the original failure, ensuring the real pass error remains the
one that propagates.

In `@tilelang/tools/lower_trace/core.py`:
- Around line 313-320: Honor terminal-only mode by preventing implicit
filesystem writes when tracing is set to terminal. Update
`_get_codegen_output_path()` in `core.py` so it only falls back to
`<script_dir>/codegen.cpp` for HTML/both persistence modes, while still
returning any explicit override; then adjust `_save_raw_files()` and the related
run-directory/file creation path to skip creating output directories/files
unless persistence is enabled. Keep explicit `codegen_output` support working
even in terminal mode, and use the existing trace-mode helpers around
`_is_trace_enabled()` / `_ensure_script_dir()` to separate terminal-only from
file-writing behavior.

In `@tilelang/tools/lower_trace/html.py`:
- Around line 1014-1026: The expand/collapse handlers in expandAll and
collapseCtx assume a table always exists inside the current pass-section, but
_make_diff_html() can render only a newline-only p while rec.changed is true, so
the toolbar may invoke these functions with no table present. Add a null check
before querying or using table (and skip the row updates when absent), or
prevent rendering the expand/collapse toolbar when no table is produced, so the
handlers fail safely in the affected diff rendering paths.
- Line 1365: The DOM ID generation in html.py is using rec.phase directly in
sec-* identifiers, which can break markup and selectors if the phase contains
quotes or CSS-special characters. Update the ID generation and any related
selector usage in the affected rendering helpers (including the record/detail
sections and the later lookup code) to derive a stable sanitized DOM-safe ID
from rec.phase, while keeping the original phase only for display text. Make
sure the same safe-ID helper is used consistently wherever these IDs are created
or referenced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46ec0d80-f612-4e1a-bf6e-5edeecd33645

📥 Commits

Reviewing files that changed from the base of the PR and between ed00dfc and 74c7f56.

⛔ Files ignored due to path filters (1)
  • docs/_static/img/lower_trace_html.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • docs/tutorials/debug_tools_for_tilelang.md
  • testing/python/debug/test_lower_trace.py
  • tilelang/__init__.py
  • tilelang/tools/__init__.py
  • tilelang/tools/lower_trace/__init__.py
  • tilelang/tools/lower_trace/core.py
  • tilelang/tools/lower_trace/diff.py
  • tilelang/tools/lower_trace/html.py

Comment thread testing/python/debug/test_lower_trace.py
Comment thread tilelang/tools/lower_trace/__init__.py
Comment thread tilelang/tools/lower_trace/__init__.py
Comment thread tilelang/tools/lower_trace/core.py
Comment thread tilelang/tools/lower_trace/html.py
Comment thread tilelang/tools/lower_trace/html.py
@erhsh

erhsh commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@LeiWang1999 please help review & merge, thx

@erhsh erhsh changed the title [feat] add lower-trace support for debugging [Feature] add lower-trace support for debugging Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant