Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7d36ce5
[FIX] Keep traces from mutating Autotuner/Heuristics and isolate clie…
mark14wu Oct 3, 2026
0343946
[REFACTOR] Trim the trace fixes to what Triton 3.8 needs
mark14wu Oct 13, 2026
da31e31
[REFACTOR] Drop the jit_fn-None skips and the untraced-JITFunction walk
mark14wu Oct 13, 2026
c03d7bc
[TEST] Pin hook rebinding and positional untracing
mark14wu Oct 13, 2026
45920c6
[COMPAT] Require Python 3.12
mark14wu Oct 13, 2026
ad9517f
[COMPAT] Move the Python 3.12 requirement to #507
mark14wu Oct 14, 2026
e7de857
[COMPAT] Restore main's Python 3.12 requirement
mark14wu Oct 14, 2026
6f95f27
[REFACTOR] Drop the patch_run setup cleanup
mark14wu Oct 14, 2026
7a12e53
[REFACTOR] Warm up only when run is asked to
mark14wu Oct 15, 2026
05edfe6
[FIX] Let a voted warmup compile kernels with traced helpers
mark14wu Oct 15, 2026
c2ed63e
[REFACTOR] Let the profiler compile each launch on its own
mark14wu Oct 15, 2026
5ed35c7
[FIX] Compile kernels whose traced helpers the profiler reaches
mark14wu Oct 15, 2026
9884584
[REFACTOR] Drop Profiler(compile=...); its vote already decides
mark14wu Oct 15, 2026
27dd023
Merge remote-tracking branch 'origin/split/trace-compile' into split/…
mark14wu Oct 15, 2026
ad87204
[FIX] Keep device functions JITFunctions under the CLI
mark14wu Oct 15, 2026
fc73f42
[REFACTOR] Drop the restore_value hook rebinding
mark14wu Oct 15, 2026
3863ae3
[REFACTOR] Keep main's TritonTrace.__call__
mark14wu Oct 15, 2026
56a4290
[TEST] Drop test_a_launch_does_not_warm_up
mark14wu Oct 15, 2026
6e8c7fb
Merge split/trace-fixes into split/trace-compile
mark14wu Oct 15, 2026
adb4d6d
Merge remote-tracking branch 'origin/split/trace-compile' into split/…
mark14wu Oct 15, 2026
f3d0a83
[TEST] Run the CLI trace-decorator test with real JITFunctions
mark14wu Oct 15, 2026
4af77ae
[TEST] Trace the lifecycle tests with the tracer client
mark14wu Oct 15, 2026
982dc22
Merge split/trace-fixes into split/trace-compile
mark14wu Oct 15, 2026
b24bda4
Merge remote-tracking branch 'origin/split/trace-compile' into split/…
mark14wu Oct 15, 2026
260e0dc
[REFACTOR] Keep main's shared Launch
mark14wu Oct 15, 2026
4a3af76
[FIX] Keep main's warmup vote and fix the Heuristics warmup
mark14wu Oct 15, 2026
6c85503
[TEST] Set TRITON_INTERPRET=1 in CI only for the tests that need it
mark14wu Oct 15, 2026
26a04ad
Merge remote-tracking branch 'origin/split/trace-fixes' into split/tr…
mark14wu Oct 15, 2026
5953b7d
[TEST] Drop the interpret-mode workarounds in the wrapper tests
mark14wu Oct 15, 2026
16e6175
Merge branch 'main' into split/trace-unwrap
mark14wu Oct 15, 2026
56d7b90
[TEST] Pin the CLI warmup bypass on CPU
mark14wu Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions tests/end_to_end/test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from pathlib import Path
from string import Template
import shutil
import pytest
import torch
from tilelens.wrapper import (
SANITIZER_COMMAND,
PROFILER_COMMAND,
Expand Down Expand Up @@ -328,3 +330,45 @@ def test_cli_rejects_trace_decorator(tmp_path: Path):
assert (
"CLI wrapper" in proc.stderr
), f"Expected error message about CLI wrapper conflict in stderr, got:\n{proc.stderr}"


@pytest.mark.parametrize("device", ["cpu", "cuda"])
def test_profiler_cli_instruments_device_functions(tmp_path: Path, device):
if device == "cuda" and not torch.cuda.is_available():
pytest.skip("the profiler's compile needs a GPU")
script = tmp_path / "device_functions.py"
script.write_text(
"""import torch
import triton
import triton.language as tl

@triton.jit
def add_one(value):
for _ in range(1):
value += 1
return value

@triton.jit
def kernel(ptr, BLOCK: tl.constexpr):
offsets = tl.arange(0, BLOCK)
tl.store(ptr + offsets, add_one(tl.load(ptr + offsets)))

"""
+ f"kernel[(1,)](torch.zeros(8, device={device!r}), BLOCK=8)\n",
encoding="utf-8",
)
# On CPU nothing can compile, so the buffer-load check is off there.
env = {
**os.environ,
"PROFILER_DISABLE_BUFFER_LOAD_CHECK": "1" if device == "cpu" else "0",
}
proc = subprocess.run(
[PROFILER_COMMAND, str(script)],
capture_output=True,
text=True,
env=env,
timeout=60,
)
assert proc.returncode == 0, proc.stderr
# The loop inside the device function is instrumented.
assert "Total for-loops detected: 1" in proc.stdout
36 changes: 36 additions & 0 deletions tests/unit/test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest.mock import MagicMock, patch

import tilelens
import triton
from tilelens.core.config import config as cfg
from tilelens.core.trace import TraceInterface
from tilelens.wrapper import (
Expand Down Expand Up @@ -163,6 +164,36 @@ def test_create_patched_jit_with_kwargs_and_custom_original_jit():
assert result == "final_kernel"


def test_patched_jit_preserves_device_function_dependencies(_isolate_cli_active):
cfg.cli_active = True
patched_jit = create_patched_jit(profiler_wrapper, triton.jit, frontend="triton")

@patched_jit
def add_one(value):
return value + 1

@patched_jit
def add_two(value):
return add_one(add_one(value))

# Triton's dependency hashing accepts only JITFunction callees.
assert isinstance(add_one, triton.JITFunction)
assert add_two.cache_key


def test_cli_jit_function_warmup_skips_the_trace(_isolate_cli_active):
cfg.cli_active = True

@triton.jit
def kernel(x):
pass

original_run = MagicMock()
kernel.run = original_run
profiler_wrapper(kernel).run(grid=(1,), warmup=True)
original_run.assert_called_once_with(grid=(1,), warmup=True)


# ======== create_patched_autotune Tests ===========


Expand Down Expand Up @@ -243,6 +274,11 @@ def test_trace_decorator_works_when_cli_inactive(_isolate_cli_active):
assert isinstance(result, TraceInterface)


def test_trace_rejects_an_unknown_frontend():
with pytest.raises(ValueError, match="Unknown frontend"):
tilelens.trace(frontend="bogus")(lambda: None)


def test_apply_wrapper_rejects_non_cli_invocation():
"""apply_sanitizer/apply_profiler should raise when called from Python, not CLI."""
with pytest.raises(RuntimeError, match="must be used as a CLI tool"):
Expand Down
19 changes: 17 additions & 2 deletions tilelens/core/frontend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from contextlib import nullcontext
from dataclasses import dataclass
from importlib import import_module
from typing import Any
from typing import Any, TYPE_CHECKING

from tilelens.core.data import Op

if TYPE_CHECKING:
from tilelens.core.client import Client


_MISSING = object()

Expand Down Expand Up @@ -115,6 +118,12 @@ def from_namespaces(
def maybe_yield_for_multism(self) -> None:
return None

def wrap_kernel(self, kernel: Any, client: "str | Client") -> Any:
"""Create this frontend's traced callable, including CLI adaptation."""
raise NotImplementedError(
f"Frontend '{self.name}' does not implement kernel wrapping"
)

def run_op_overrider(
self,
op: Callable,
Expand Down Expand Up @@ -206,7 +215,13 @@ def register_frontend(frontend: Frontend) -> Frontend:

def get_frontend(name: str) -> Frontend:
if name not in FRONTEND_REGISTRY:
import_module(f"tilelens.core.frontend.{name}")
module_name = f"tilelens.core.frontend.{name}"
try:
import_module(module_name)
except ModuleNotFoundError as exc:
if exc.name != module_name:
raise
raise ValueError(f"Unknown frontend: {name}") from exc
try:
return FRONTEND_REGISTRY[name]
except KeyError as exc:
Expand Down
5 changes: 5 additions & 0 deletions tilelens/core/frontend/gluon.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,11 @@ def __init__(self):
namespaces=definition.namespaces,
)

def wrap_kernel(self, kernel, client):
from ..trace import GluonTrace

return GluonTrace(kernel, client)

@staticmethod
def symbolic_ops_for_op_type(op_type: type[Op]) -> tuple[str, ...]:
return TritonFrontend.symbolic_ops_for_op_type(op_type)
Expand Down
5 changes: 5 additions & 0 deletions tilelens/core/frontend/nki.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ def __init__(self):
namespaces=definition.namespaces,
)

def wrap_kernel(self, kernel, client):
from ..trace import NKITrace

return NKITrace(kernel, client, beta2=False)

def patch_lang(self, fn, client_manager: Any = None) -> _LangPatchScope:
from tilelens.core.simulation.nki import nki_patch_lang

Expand Down
5 changes: 5 additions & 0 deletions tilelens/core/frontend/nki_beta2.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ def __init__(self):
namespaces=definition.namespaces,
)

def wrap_kernel(self, kernel, client):
from ..trace import NKITrace

return NKITrace(kernel, client, beta2=True)

def patch_lang(self, fn, client_manager: Any = None) -> _LangPatchScope:
from tilelens.core.simulation.nki_beta2 import nki_patch_lang

Expand Down
27 changes: 27 additions & 0 deletions tilelens/core/frontend/triton.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,34 @@ def _grid_executor_call(self, grid_executor, *args_dev, **kwargs):
finally:
self._current_client_manager = previous_client_manager

def wrap_kernel(self, kernel, client):
from ..trace import TritonTrace

traced = TritonTrace(kernel, client)
if not cfg.cli_active or not isinstance(kernel, JITFunction):
return traced

# Device helpers must remain JITFunctions for dependency hashing and
# code generation. Instrument their launch path instead of replacing
# the callable that another kernel references.
original_run = kernel.run

def run(*args, **kwargs):
# JITFunction.warmup re-enters run(warmup=True) for the real
# compile; tracing it would recurse.
if kwargs.get("warmup", False):
return original_run(*args, **kwargs)
return traced.run(*args, **kwargs)

kernel.run = run
kernel._tilelens_trace = traced
return kernel

def _jit_function_call(self, jit_function, *args, **kwargs):
traced = getattr(jit_function, "_tilelens_trace", None)
if traced is not None:
return traced(*args, **kwargs)

if (
self._current_client_manager is not None
and not self._jit_function_uses_core_module(jit_function)
Expand Down
16 changes: 6 additions & 10 deletions tilelens/core/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..clients.race_detector.race_detector import NullRaceDetector
from .client import ClientManager, Client
from .data import Launch
from .frontend.base import get_frontend
import types


Expand Down Expand Up @@ -409,7 +410,10 @@ def _is_race_detector_client(selected: str | Client) -> bool:
return isinstance(selected, NullRaceDetector)

def decorator(kernel) -> TritonTrace | NKITrace | GluonTrace | Any:
if cfg.cli_active and isinstance(kernel, TraceInterface):
if cfg.cli_active and (
isinstance(kernel, TraceInterface)
or isinstance(getattr(kernel, "_tilelens_trace", None), TraceInterface)
):
raise RuntimeError(
"@tilelens.trace() decorator cannot be used together with "
"CLI wrapper (e.g., tile-sanitizer / tile-profiler). "
Expand All @@ -435,15 +439,7 @@ def decorator(kernel) -> TritonTrace | NKITrace | GluonTrace | Any:

trace_source(kernel)

# First-time wrapping
if frontend in ("nki", "nki_beta2"):
return NKITrace(kernel, client, beta2=("beta2" in frontend))
elif frontend == "gluon":
return GluonTrace(kernel, client)
elif frontend == "triton":
return TritonTrace(kernel, client)
else:
raise ValueError(f"Unknown frontend: {frontend}")
return get_frontend(frontend).wrap_kernel(kernel, client)

return decorator

Expand Down
Loading