-
Notifications
You must be signed in to change notification settings - Fork 99
[fix] Autotuner: propagate the tuned function's return value #899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -711,5 +711,47 @@ def test_artifact_key_must_name_a_kernel_parameter(): | ||||||||||||||||
| _make_artifact_tuner(key=["mni"]) | |||||||||||||||||
|
|
|||||||||||||||||
|
|
|||||||||||||||||
| # ── return-value propagation ───────────────────────────────────────────── | |||||||||||||||||
| def test_call_returns_tuned_fn_value(monkeypatch): | |||||||||||||||||
| """The tuned function's return value must propagate through __call__ on both | |||||||||||||||||
| the search path and the cache-hit path.""" | |||||||||||||||||
| monkeypatch.setenv("FLYDSL_AUTOTUNE", "1") # force the search path first | |||||||||||||||||
|
|
|||||||||||||||||
| def fn(a, out, BLOCK): | |||||||||||||||||
| return ("result", BLOCK) | |||||||||||||||||
|
|
|||||||||||||||||
| tuner = _make_tuner( | |||||||||||||||||
| fn=fn, | |||||||||||||||||
| configs=[Config(BLOCK=64), Config(BLOCK=128)], | |||||||||||||||||
| do_bench_fn=lambda call, warmup, rep: (call(), 1.0)[1], | |||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This stub returns a constant Reversing the list is enough to flip the result: Two consequences: the test cannot distinguish "returns the best config's value" from "returns the first config's value", and any future change to selection (sorting results, batching the benchmark, a different tie-break) would break it for reasons unrelated to return propagation. Giving the configs distinct times makes the winner unambiguous and order-independent. I checked that this variant returns times = iter([2.0, 1.0]) # the second config is genuinely faster
tuner = _make_tuner(
fn=fn,
configs=[Config(BLOCK=64), Config(BLOCK=128)],
do_bench_fn=lambda call, warmup, rep: (call(), next(times))[1],
)
...
assert searched == ("result", 128) |
|||||||||||||||||
| ) | |||||||||||||||||
| args = (FakeTensor((8,)), FakeTensor((1,))) | |||||||||||||||||
|
|
|||||||||||||||||
| searched = tuner(*args) # search path -> runs the winning config | |||||||||||||||||
| assert searched == ("result", 64) | |||||||||||||||||
|
|
|||||||||||||||||
| monkeypatch.setenv("FLYDSL_AUTOTUNE", "0") | |||||||||||||||||
| hit = tuner(*args) # cache-hit path | |||||||||||||||||
| assert hit == ("result", 64) | |||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This half does not actually exercise the cache-hit path. The file already has the idiom for this: |
|||||||||||||||||
|
|
|||||||||||||||||
|
|
|||||||||||||||||
| def test_call_returns_none_when_fn_returns_none(monkeypatch): | |||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, just so it is not miscounted as regression coverage: this one passes against the unpatched code too, because the buggy Still worth keeping as a guard against an over-correction — someone later "improving" this to hand back |
|||||||||||||||||
| """In-place kernels (write into `out`, return nothing) still return None -- | |||||||||||||||||
| the fix only forwards whatever the tuned fn returns, it doesn't fabricate.""" | |||||||||||||||||
| monkeypatch.setenv("FLYDSL_AUTOTUNE", "1") | |||||||||||||||||
|
|
|||||||||||||||||
| def fn(a, out, BLOCK): | |||||||||||||||||
| out._data[0] = float(BLOCK) # writes in place, returns None | |||||||||||||||||
|
|
|||||||||||||||||
| tuner = _make_tuner( | |||||||||||||||||
| fn=fn, | |||||||||||||||||
| configs=[Config(BLOCK=64)], | |||||||||||||||||
| do_bench_fn=lambda call, warmup, rep: (call(), 1.0)[1], | |||||||||||||||||
| ) | |||||||||||||||||
| args = (FakeTensor((8,)), FakeTensor((1,))) | |||||||||||||||||
| assert tuner(*args) is None | |||||||||||||||||
| assert args[1]._data[0] == 64.0 | |||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that this is rebased there are four
The default path is the one I would most want covered: it is what the only real consumer uses ( Both are cheap now — #786 already ships |
|||||||||||||||||
|
|
|||||||||||||||||
|
|
|||||||||||||||||
| if __name__ == "__main__": | |||||||||||||||||
| raise SystemExit(pytest.main([__file__, "-v"])) | |||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you document the contract this establishes? There is a sharp edge worth writing down: this only yields a value when
@autotunewraps a plain Python callable. When it wraps a@flyc.jitfunction directly — the form theautotune()docstring shows — the host launch path structurally cannot return anything:func.FuncOp(self.func.__name__, (ir_types, []))(jit_function.py:1507), terminated byfunc.ReturnOp([])(:1534)ctypes.CFUNCTYPE(None, ctypes.c_void_p)(jit_executor.py:363), sorestypeisNone(
JitFunction.__call__does have areturn self.func(...)atjit_function.py:1369, but that is their.Context.current is not Nonetrace-time inlining branch, not a host call.)That does not make the fix pointless —
Autotunerexplicitly accepts plain callables (autotune.py:231,source = fn.func if hasattr(fn, "func") else fn), so "autotune a Python wrapper that allocates and returns its output" is a real and useful pattern, and this unblocks it. It just is not the@autotune+@flyc.jitstack the docstring describes. A sentence or two onautotune()saying that the wrapper forwards the tuned callable's return value, and that a directly-wrapped jit kernel always returnsNone, would save the next reader the trace I just did.