Repository navigation
[FIX] Keep traces from mutating Autotuner/Heuristics or failing their warmup - #494
Conversation
Performance Benchmark
Iterations: 1 warmup + 20 measured |
d675311 to
dcd8610
Compare
…nt hooks
TritonTrace built its interpreted runner by assigning to the user's
Autotuner/Heuristics (runner.fn, _do_bench) and its warmup runner by
deep-copying the result. On Triton 3.8 this crashes three ways on main:
- a @triton.heuristics kernel: "Heuristics.run() got multiple values for
keyword argument 'warmup'" (the launch's warmup=False reached
KernelInterface.warmup, which adds warmup=True);
- @triton.autotune over @triton.heuristics: "k() missing 1 required
positional argument" (the Heuristics layer was replaced by the
interpreted function, so its constexpr never reached the kernel);
- a client that votes for a real compile (the profiler) on a kernel that
calls a traced helper, as under the CLI wrappers: the compile got the
TritonTrace. Called by name, it was rejected by the dependency walk
that keys the compile ("Unsupported function referenced:
<TritonTrace ...>"); passed as a constexpr argument, it was called by
the code generator, which ran the interpreter and left
triton.language patched.
Every Autotuner/Heuristics layer is now shallow-copied over the
interpreted function or the JITFunction. An Autotuner copy gets its own
config cache, never uses Triton's on-disk autotune cache, has Triton's
reset_to_zero/restore_value hooks rebound to it, and (interpreted) uses
the dummy benchmarker even if the user's autotuner cached a real one. A
Heuristics copy warms up through fn.warmup, so the clients' warmup vote
applies to it. During a voted real compile, the traced helpers the
kernel's code reaches by name are bound back to their JITFunctions, and
traced arguments and parameter defaults are passed as JITFunctions. A
TritonTrace called outside any interpreter (neither a traced launch's
nor Triton's own under TRITON_INTERPRET=1) raises a TypeError instead of
running the interpreter; an untraced kernel run by Triton's interpreter
still calls it as before.
Client hooks:
- every traced launch gets its own Launch; two launches used to append
the same object to `launches`;
- patch_warmup asks every client before compiling (main stopped at the
first True vote, yet every client got post_warmup_callback). On exit a
scope takes its own gate out wherever it sits, so overlapping scopes
(e.g. on two host threads) may close in any order, and jit_fn ends
with exactly the warmup it had (main left a bound method in its
instance dict);
- patch_run undoes partial patching when registering client hooks fails:
the sanitizer plus the race detector on one kernel refuse with two
loop overriders and used to leave the interpreter's ops patched;
- ClientManager.finalize finalizes every client even if one raises, then
re-raises the first failure.
Keep the fixes and drop the code they did not need: - patch_warmup keeps main's shape and takes one real_compile hook from the trace; _WarmupGate (out-of-order scopes), RealArgs, the compile_context/real_args parameters, _without_warmup and _voted_warmup are gone; - one _rebuild_runner: Autotuner copies get their own cache, cache_results=False (check_disk_cache crashed on the interpreted chain) and do_bench set directly; Triton's restore hooks are rebound only when they close over the original; no nested-trace look-through; - a traced function called outside a traced launch does what the untraced function does (Triton's interpreter, or JITFunction's refusal) instead of raising a custom TypeError; - parameter-default unwrapping is dropped; _is_triton_internal is gone (it hid traced helpers in triton.* modules), and so are the global walker's defensive guards; - finalize and the first-True-vote short circuit are back to main's; collecting finalize errors moves up the stack, where a client can first raise in finalize; - patch_run creates its Launch per launch and sets up inside the try, without the lang_patched flag. test_trace_lifecycle.py keeps 7 tests, each failing on main.
A trace without a JITFunction (decorated while the interpret knob was on) that reaches a real compile fails either way. The skips only changed the error, and for an argument they let the code generator call the trace, which patched triton.language. The walk no longer looks through untraced JITFunctions. The CLI traces every triton.jit, so such a chain needs triton.runtime.jit.jit or an explicit tilelens.trace on a device function; main does not support it either.
The autotune test runs both with Triton's restore_value post_hook and with a user post_hook that closes over one list, so the hook rebinding and its identity check are both pinned. The voted-compile test passes FN by position.
Python 3.12 inlines comprehensions, so a kernel's co_names already holds the names its tuple comprehensions call; _code_names' recursion into nested code objects only served 3.10/3.11. README and AGENTS.md follow, and the cp310/cp311 NKI wheel sources go.
The version files go back to main's. #507 requires Python 3.12, and this PR depends on it.
30f3aa6 to
ad9517f
Compare
| self._populate_loop_hooks(all_loop_callbacks) | ||
| patch_for_loop(frontend_name) | ||
| patch_lang(fn, frontend_name, client_manager=self) | ||
| # Inside the try: a failing setup (e.g. two loop overriders) must |
There was a problem hiding this comment.
Removed. client.py is now unchanged from main.
| restore_value=["out_ptr"], | ||
| # Triton's own restore_value post_hook, or a user's that closes over | ||
| # just `posts` and must stay bound to it. | ||
| post_hook=(lambda nargs, exception: posts.append(exception)) |
There was a problem hiding this comment.
Agreed. Removed the hook rebinding along with this test.
|
Update: #494 no longer fixes this (it fails as on main); #513 fixes it. Minimal reproducer: an ordinary kernel that calls a import torch, triton, triton.language as tl
@triton.jit
def add_one(x):
return x + 1
@triton.jit
def kernel(x_ptr, BLOCK: tl.constexpr):
offs = tl.arange(0, BLOCK)
tl.store(x_ptr + offs, add_one(tl.load(x_ptr + offs)))
x = torch.zeros(8, device="cuda")
kernel[(1,)](x, BLOCK=8)
print("ok")
The CLI traces every |
Its setup only fails when two clients with loop overriders trace one kernel, which is not a typical use.
A launch passes warmup=False, so it no longer compiles first. That is the profiler's need, not every trace's, and with it go the fixes that made the per-launch compile work: traced helpers, constexpr traces and the Heuristics warmup.
Without it, Triton's hooks leave restore_copies on the user's Autotuner, as an untraced run does; the kernel's output is the same.
Only a host-side call of a traced device function reaches it outside a launch, which is not a typical use.
The autotune-over-heuristics test fails the same way when a launch warms up.
The custom client is down to no-op callbacks, which the tracer covers.
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _real_jit(): | ||
| # tests/unit/test_multithreading.py sets TRITON_INTERPRET=1 at import |
There was a problem hiding this comment.
I think the right way is to avoid using TRITON_INTERPRET=1 for certain tests in the CI configuration file
There was a problem hiding this comment.
Done. CI now sets TRITON_INTERPRET=1 only for test_multithreading.py and test_autotune_interpreter_mode, and this fixture is gone.
| frontend = get_frontend(frontend_name) | ||
| namespaces = frontend.namespaces | ||
| # A fresh Launch per launch, so `launches` gets distinct entries. | ||
| self.launch = Launch() |
There was a problem hiding this comment.
I'm not sure if this is correct. I thought the user is responsible for cleaning launches, otherwise we will lose records
There was a problem hiding this comment.
You're right. The Tracer's records accumulate across launches, so a fresh Launch lost the earlier launches' tensors and the visualizer hit a KeyError. Reverted.
| with self.client_manager.patch_warmup(self.jit_fn): | ||
| if self.warmup_runner: | ||
| self.warmup_runner.warmup(*args, **kwargs) | ||
| if kwargs.get("warmup"): |
There was a problem hiding this comment.
We no longer call patch_warmup? How about pre and post warm up callbacks?
There was a problem hiding this comment.
Restored main's run, so every launch goes through patch_warmup and the warmup callbacks again. The remaining fix is the Heuristics warmup, which passed warmup= twice.
The Tracer's records accumulate across launches, so a fresh Launch per launch held earlier launches' records without their tensors, and the visualizer failed with a KeyError.
Every launch calls the warmup again, and the clients' pre_warmup_callback decides whether to compile. Each Heuristics copy fills in its values and calls fn.warmup, instead of KernelInterface.warmup, which passed warmup= a second time.
test_multithreading.py set it at import, which made every later @triton.jit in the session an InterpretedFunction.
#494 was trimmed before it landed; this layer keeps what it builds on: - client.py: main's patch_warmup and patch_run (#494), with op/loop registration for interpreting clients only. _WarmupGate is gone. begin_launch still gives every launch its own Launch, and finalize still finalizes every client when one raises. - trace.py: IR helpers on #494's _rebuild_runner (ir=True adds the conflicting-meta-parameter check); main's TritonTrace.__call__, comparing interpreting clients. The IR capture keeps mapping traced arguments to their JITFunctions (_untraced_call_args); the global unwrapping (_unwrapped_trace_globals) is dropped with #494's warmup path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
On main, a trace:
Autotuner/Heuristics. Now it runs on copies of that chain, which also fixes autotune over heuristics andcache_results.@triton.heuristicskernel in the warmup. Now each Heuristics copy warms up throughfn.warmup.Changes
tilelens/core/trace.py_rebuild_runnerreplaces_interpreter_runnerand_warmup_runner. It builds the trace's Autotuner/Heuristics chain from shallow copies.fnand the benchmarker on the user's objects, so later untraced launches ran in the interpreter.@triton.autotuneover@triton.heuristicsfailed with "missing 1 required positional argument", because the Heuristics layer was replaced by the interpreted function.cache. A shallow copy would otherwise share the user's.cache_results = False. With it on,check_disk_cachefails on the interpreted chain with "'function' object has no attribute 'fn'".do_benchto the dummy benchmarker.do_benchis a cached property, so once the user's Autotuner had benchmarked, setting_do_benchdid nothing. A two-config kernel ran 111 times in the interpreter instead of 3.warmupthat fills in its values and callsfn.warmup, where the clients vote on whether to compile. Before,KernelInterface.warmuppassedwarmup=a second time, so every launch failed with "Heuristics.run() got multiple values for keyword argument 'warmup'".CI and tests
tests/unit/test_multithreading.pyno longer setsTRITON_INTERPRET=1at import, which made every later@triton.jitin the session anInterpretedFunction..github/workflows/python-app.ymlsets it in a separate step for that file and fortest_autotune_interpreter_mode, which covers a trace without aJITFunction.tests/unit/test_trace_lifecycle.py(new):cache, unchanged;tests/end_to_end/test_tracer.pyreads the copy'sdo_benchinstead of_do_bench.