forked from ROCm/FlyDSL
-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature] Extract reusable event-based benchmarking helper #2
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
Draft
jhinpan
wants to merge
1
commit into
main
Choose a base branch
from
feat/profile-tools
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+233
−26
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # Copyright (c) 2026 FlyDSL Project Contributors | ||
|
|
||
| """GPU event timing shared across FlyDSL compile backends.""" | ||
|
|
||
| from typing import Callable, List, Optional, Sequence, Union | ||
|
|
||
| __all__ = ["do_bench"] | ||
|
|
||
|
|
||
| def _get_torch_cuda(): | ||
| """Return PyTorch's CUDA-compatible namespace lazily. | ||
|
|
||
| PyTorch exposes the same ``torch.cuda`` event API on CUDA and HIP builds, | ||
| so callers do not need to branch on the FlyDSL compile backend. | ||
| """ | ||
| try: | ||
| import torch | ||
| except ImportError as exc: | ||
| raise RuntimeError("GPU profiling requires PyTorch with CUDA or HIP support") from exc | ||
| if not torch.cuda.is_available(): | ||
| raise RuntimeError("GPU profiling requires an available CUDA or HIP device") | ||
| return torch.cuda | ||
|
|
||
|
|
||
| def do_bench( | ||
| fn: Callable[[], object], | ||
| warmup: int = 5, | ||
| rep: int = 25, | ||
| quantiles: Optional[Sequence[float]] = None, | ||
| setup: Optional[Callable[[], object]] = None, | ||
| ) -> Union[float, List[float]]: | ||
| """Benchmark a GPU callable with CUDA/HIP events. | ||
|
|
||
| ``warmup`` and ``rep`` are iteration counts. Timings are returned in | ||
| milliseconds. By default the upper-middle sample is returned; when a | ||
| non-empty ``quantiles`` sequence is provided, the corresponding sorted | ||
| samples are returned. ``warmup`` must be non-negative, ``rep`` must be | ||
| positive, and quantiles must be in the inclusive range ``[0, 1]``. | ||
|
|
||
| ``fn`` must enqueue the measured work on PyTorch's current CUDA/HIP stream. | ||
| Work submitted only to another stream is outside the event interval unless | ||
| ``fn`` synchronizes that stream itself. | ||
|
|
||
| When provided, ``setup`` runs before every warmup and measured iteration. | ||
| For measured iterations it runs before the start event, so restore/reset | ||
| work is not included in the reported kernel latency. | ||
| """ | ||
| if warmup < 0: | ||
| raise ValueError("warmup must be non-negative") | ||
| if rep <= 0: | ||
| raise ValueError("rep must be positive") | ||
| if quantiles is not None and any(not 0.0 <= q <= 1.0 for q in quantiles): | ||
| raise ValueError("quantiles must be between 0 and 1") | ||
|
|
||
| device = _get_torch_cuda() | ||
|
|
||
| for _ in range(warmup): | ||
| if setup is not None: | ||
| setup() | ||
| fn() | ||
| device.synchronize() | ||
|
|
||
| times = [] | ||
| for _ in range(rep): | ||
| if setup is not None: | ||
| setup() | ||
| start = device.Event(enable_timing=True) | ||
| end = device.Event(enable_timing=True) | ||
| start.record() | ||
| fn() | ||
| end.record() | ||
| device.synchronize() | ||
| times.append(start.elapsed_time(end)) | ||
|
|
||
| times.sort() | ||
| if quantiles: | ||
| return [times[min(int(q * len(times)), len(times) - 1)] for q in quantiles] | ||
| return times[len(times) // 2] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,131 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||
| # Copyright (c) 2026 FlyDSL Project Contributors | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| """GPU-free tests for the shared profiling helpers.""" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||
| from types import SimpleNamespace | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import flydsl.profiling as profiling | ||||||||||||||||||||||||||||||||||||||||||||||
| from flydsl.autotune import do_bench as autotune_do_bench | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| pytestmark = pytest.mark.l0_backend_agnostic | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class FakeEvent: | ||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, device): | ||||||||||||||||||||||||||||||||||||||||||||||
| self.device = device | ||||||||||||||||||||||||||||||||||||||||||||||
| self.timestamp = None | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def record(self): | ||||||||||||||||||||||||||||||||||||||||||||||
| self.timestamp = self.device.clock | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def elapsed_time(self, end): | ||||||||||||||||||||||||||||||||||||||||||||||
| return end.timestamp - self.timestamp | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class FakeDeviceInterface: | ||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||||
| self.clock = 0.0 | ||||||||||||||||||||||||||||||||||||||||||||||
| self.event_count = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| self.synchronize_count = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def Event(self, *, enable_timing): | ||||||||||||||||||||||||||||||||||||||||||||||
| assert enable_timing | ||||||||||||||||||||||||||||||||||||||||||||||
| self.event_count += 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| return FakeEvent(self) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def synchronize(self): | ||||||||||||||||||||||||||||||||||||||||||||||
| self.synchronize_count += 1 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _install_fake(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| device = FakeDeviceInterface() | ||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setattr(profiling, "_get_torch_cuda", lambda: device) | ||||||||||||||||||||||||||||||||||||||||||||||
| return device | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_warmup_repetitions_and_median(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| device = _install_fake(monkeypatch) | ||||||||||||||||||||||||||||||||||||||||||||||
| durations = iter([99.0, 3.0, 1.0, 5.0, 2.0, 4.0]) | ||||||||||||||||||||||||||||||||||||||||||||||
| call_count = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def fn(): | ||||||||||||||||||||||||||||||||||||||||||||||
| nonlocal call_count | ||||||||||||||||||||||||||||||||||||||||||||||
| call_count += 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| device.clock += next(durations) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| assert profiling.do_bench(fn, warmup=1, rep=5) == 3.0 | ||||||||||||||||||||||||||||||||||||||||||||||
| assert call_count == 6 | ||||||||||||||||||||||||||||||||||||||||||||||
| assert device.event_count == 10 | ||||||||||||||||||||||||||||||||||||||||||||||
| assert device.synchronize_count == 6 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_preserves_upper_middle_for_even_repetitions(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| device = _install_fake(monkeypatch) | ||||||||||||||||||||||||||||||||||||||||||||||
| durations = iter([4.0, 1.0, 3.0, 2.0]) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def fn(): | ||||||||||||||||||||||||||||||||||||||||||||||
| device.clock += next(durations) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| assert profiling.do_bench(fn, warmup=0, rep=4) == 3.0 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_quantiles(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| device = _install_fake(monkeypatch) | ||||||||||||||||||||||||||||||||||||||||||||||
| durations = iter([3.0, 1.0, 5.0, 2.0, 4.0]) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def fn(): | ||||||||||||||||||||||||||||||||||||||||||||||
| device.clock += next(durations) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| assert profiling.do_bench(fn, warmup=0, rep=5, quantiles=[0.0, 0.5, 0.9, 1.0]) == [1.0, 3.0, 5.0, 5.0] | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+76
to
+83
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. Add a test case to verify that
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_runs_setup_before_each_iteration_and_outside_timing(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| device = _install_fake(monkeypatch) | ||||||||||||||||||||||||||||||||||||||||||||||
| durations = iter([99.0, 1.0, 2.0]) | ||||||||||||||||||||||||||||||||||||||||||||||
| order = [] | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def setup(): | ||||||||||||||||||||||||||||||||||||||||||||||
| order.append("setup") | ||||||||||||||||||||||||||||||||||||||||||||||
| device.clock += 100.0 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def fn(): | ||||||||||||||||||||||||||||||||||||||||||||||
| order.append("kernel") | ||||||||||||||||||||||||||||||||||||||||||||||
| device.clock += next(durations) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| assert profiling.do_bench(fn, warmup=1, rep=2, setup=setup) == 2.0 | ||||||||||||||||||||||||||||||||||||||||||||||
| assert order == ["setup", "kernel", "setup", "kernel", "setup", "kernel"] | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_reports_missing_pytorch(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setitem(sys.modules, "torch", None) | ||||||||||||||||||||||||||||||||||||||||||||||
| with pytest.raises(RuntimeError, match="requires PyTorch"): | ||||||||||||||||||||||||||||||||||||||||||||||
| profiling._get_torch_cuda() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_reports_unavailable_device(monkeypatch): | ||||||||||||||||||||||||||||||||||||||||||||||
| cuda = SimpleNamespace(is_available=lambda: False) | ||||||||||||||||||||||||||||||||||||||||||||||
| monkeypatch.setitem(sys.modules, "torch", SimpleNamespace(cuda=cuda)) | ||||||||||||||||||||||||||||||||||||||||||||||
| with pytest.raises(RuntimeError, match="available CUDA or HIP device"): | ||||||||||||||||||||||||||||||||||||||||||||||
| profiling._get_torch_cuda() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.mark.parametrize( | ||||||||||||||||||||||||||||||||||||||||||||||
| ("kwargs", "message"), | ||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||
| ({"warmup": -1, "rep": 1}, "warmup must be non-negative"), | ||||||||||||||||||||||||||||||||||||||||||||||
| ({"warmup": 0, "rep": 0}, "rep must be positive"), | ||||||||||||||||||||||||||||||||||||||||||||||
| ({"warmup": 0, "rep": 1, "quantiles": [-0.1]}, "quantiles must be between 0 and 1"), | ||||||||||||||||||||||||||||||||||||||||||||||
| ({"warmup": 0, "rep": 1, "quantiles": [1.1]}, "quantiles must be between 0 and 1"), | ||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| def test_do_bench_rejects_invalid_arguments(kwargs, message): | ||||||||||||||||||||||||||||||||||||||||||||||
| with pytest.raises(ValueError, match=message): | ||||||||||||||||||||||||||||||||||||||||||||||
| profiling.do_bench(lambda: None, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def test_autotune_keeps_compatibility_alias(): | ||||||||||||||||||||||||||||||||||||||||||||||
| assert autotune_do_bench is profiling.do_bench | ||||||||||||||||||||||||||||||||||||||||||||||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
If
quantilesis passed as an iterator or generator (e.g., a generator expression or amapobject), checking it withany(...)will completely exhaust the iterator. Consequently, the subsequent list comprehension on line 78 will iterate over an empty generator and return an empty list[]instead of the expected quantiles.To prevent this, convert
quantilesto a list first if it is notNone. This ensures the input is safely consumed and can be iterated over multiple times.