-
Notifications
You must be signed in to change notification settings - Fork 55
ASV better memory benchmarks #1609
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
8707c16
2113022
7a17f02
0069bae
4a921f5
86b5aa5
eb91f70
0f8c128
4f9e0d1
1c3d176
cd7dabf
f74431c
b96aade
c9c55a1
c77be6a
8bfd409
88d25de
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 |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
|
|
||
| __all__ = ["grid_nbytes", "dataset_nbytes"] | ||
|
|
||
|
|
||
| def grid_nbytes(uxgrid): | ||
| """Total size of the arrays a ``Grid`` currently holds, in bytes.""" | ||
| return uxgrid._ds.nbytes | ||
|
|
||
|
|
||
| def dataset_nbytes(uxds): | ||
| """Total size of a ``UxDataset``, in bytes, including its grid.""" | ||
| return uxds.nbytes + grid_nbytes(uxds.uxgrid) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
|
|
||
| import contextlib | ||
| import subprocess | ||
| import sys | ||
|
|
||
| import numba | ||
|
|
||
| __all__ = ["peak_allocated", "numba_threads", "subprocess_peak_rss"] | ||
|
|
||
|
|
||
| def peak_allocated(build): | ||
| """Bytes held at the high-water allocation point of ``build``. | ||
|
|
||
| ``tracemalloc.start`` begins with an empty trace table, so whatever the | ||
| process is already holding -- including everything allocated in ``setup`` -- | ||
| is excluded, and only what ``build`` allocates counts. A ``reset_peak()`` | ||
| here would be a no-op for that reason. | ||
| """ | ||
| # Imported here rather than at module scope: asv preimports every benchmark | ||
| # module under its default ``forkserver`` launch method | ||
| import tracemalloc | ||
|
|
||
| if tracemalloc.is_tracing(): | ||
| raise RuntimeError("tracemalloc is already tracing") | ||
|
|
||
| # nframe=1: the reported peak is identical at any traceback depth, while the | ||
| # cost is not -- nframe=25 runs 26x slower. | ||
| tracemalloc.start(1) | ||
| try: | ||
| build() | ||
| _, peak = tracemalloc.get_traced_memory() | ||
| finally: | ||
| tracemalloc.stop() | ||
| return peak | ||
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def numba_threads(n): | ||
| """Runs the block with numba's thread pool held at ``n``. | ||
|
|
||
| Tracing serializes on tracemalloc's global allocator lock, so a | ||
| ``parallel=True`` kernel under :func:`peak_allocated` degrades badly as | ||
| threads contend for it. | ||
| """ | ||
| restore = numba.get_num_threads() | ||
| numba.set_num_threads(n) | ||
| try: | ||
| yield | ||
| finally: | ||
| numba.set_num_threads(restore) | ||
|
|
||
|
|
||
| # ``ru_maxrss`` is bytes on macOS and kilobytes elsewhere, mirroring | ||
| # ``asv_runner/benchmarks/_maxrss.py:117,132``. | ||
| _MAXRSS_TO_BYTES = 1 if sys.platform == "darwin" else 1024 | ||
|
|
||
|
|
||
| def subprocess_peak_rss(statement): | ||
| """Peak resident bytes of a fresh interpreter that has run ``statement``. | ||
|
|
||
| For the cases an in-process metric cannot reach: measuring an import, whose | ||
| cost is already paid before a ``peakmem_*`` body runs, and measuring | ||
| ``numba.typed`` containers, which tracemalloc does not see at all. | ||
|
|
||
| The child reports its own ``ru_maxrss`` rather than the parent reading | ||
| ``RUSAGE_CHILDREN``, which is a maximum over every child that has exited and | ||
| so would not isolate this one. | ||
| """ | ||
| reporter = ( | ||
| "import resource, sys\n" | ||
| f"exec({statement!r})\n" | ||
| "sys.stdout.write(str(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss))\n" | ||
| ) | ||
| completed = subprocess.run( | ||
| [sys.executable, "-c", reporter], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
| return int(completed.stdout) * _MAXRSS_TO_BYTES |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,14 @@ | ||
| from .helpers._peakmem import subprocess_peak_rss | ||
|
|
||
|
|
||
| class Imports: | ||
| """Benchmark importing uxarray.""" | ||
|
|
||
| def timeraw_import_uxarray(self): | ||
| return "import uxarray" | ||
|
|
||
| def track_peakmem_import_uxarray(self): | ||
| """Peak resident memory of a process that has imported uxarray.""" | ||
| return subprocess_peak_rss("import uxarray") | ||
|
|
||
| track_peakmem_import_uxarray.unit = "bytes" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,9 @@ | |
|
|
||
| import uxarray as ux | ||
|
|
||
| from .helpers._memsize import grid_nbytes | ||
| from .helpers._peakmem import numba_threads, peak_allocated | ||
|
|
||
| current_path = Path(os.path.dirname(os.path.realpath(__file__))) | ||
|
|
||
| data_var = 'bottomDepth' | ||
|
|
@@ -57,27 +60,92 @@ def teardown(self, resolution, *args, **kwargs): | |
|
|
||
|
|
||
| class FaceAreas(GridBenchmark): | ||
| def time_compute_face_areas(self, resolution): | ||
| self.uxgrid.compute_face_areas() | ||
| number = 1 | ||
| warmup_time = 0 | ||
|
|
||
| def setup(self, resolution, *args, **kwargs): | ||
| # The coarsest grid, purely to compile the njit kernel | ||
| warmup_grid = ux.open_grid(file_path_dict[self.params[0][0]][0]) | ||
| _ = warmup_grid.face_areas | ||
| super().setup(resolution, *args, **kwargs) | ||
| self.uxgrid._ds = self.uxgrid._ds.drop_vars("face_areas", errors="ignore") | ||
|
|
||
| def time_face_areas(self, resolution): | ||
| _ = self.uxgrid.face_areas | ||
|
|
||
| def track_nbytes_face_areas(self, resolution): | ||
| """Size of the materialized ``Grid.face_areas`` array.""" | ||
| return self.uxgrid.face_areas.nbytes | ||
|
|
||
| track_nbytes_face_areas.unit = "bytes" | ||
|
|
||
| def peakmem_compute_face_areas(self, resolution): | ||
| self.uxgrid.compute_face_areas() | ||
| def track_peakmem_face_areas(self, resolution): | ||
| """Transient high-water allocation of computing ``Grid.face_areas``.""" | ||
| with numba_threads(1): | ||
| return peak_allocated(lambda: self.uxgrid.face_areas) | ||
|
|
||
| track_peakmem_face_areas.unit = "bytes" | ||
|
|
||
|
|
||
| class Gradient(DatasetBenchmark): | ||
| def setup(self, resolution, *args, **kwargs): | ||
| super().setup(resolution, *args, **kwargs) | ||
| # Compiles the gradient kernels on the coarsest grid | ||
| grid, data = file_path_dict[self.params[0][0]] | ||
| _ = ux.open_dataset(grid, data)[data_var].gradient() | ||
|
|
||
| def time_gradient(self, resolution): | ||
| self.uxds[data_var].gradient() | ||
|
|
||
| def peakmem_gradient(self, resolution): | ||
| grad = self.uxds[data_var].gradient() | ||
| def track_nbytes_gradient(self, resolution): | ||
| """Size of the gradient result.""" | ||
| return self.uxds[data_var].gradient().nbytes | ||
|
|
||
| track_nbytes_gradient.unit = "bytes" | ||
|
|
||
| def track_peakmem_gradient(self, resolution): | ||
| """Transient high-water allocation of taking a gradient.""" | ||
| return peak_allocated(lambda: self.uxds[data_var].gradient()) | ||
|
|
||
| track_peakmem_gradient.unit = "bytes" | ||
|
|
||
|
|
||
| class Integrate(DatasetBenchmark): | ||
|
|
||
| def time_integrate(self, resolution): | ||
| self.uxds[data_var].integrate() | ||
|
|
||
| def peakmem_integrate(self, resolution): | ||
|
Member
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. Why no
Collaborator
Author
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. Hm, actually looking at it again,
Member
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. Hmm, I think that makes sense to me re peakmem and nbytes.
Collaborator
Author
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. Is there still something we need here?
Member
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. If you took action to remove them, no I don't think anything else left here. |
||
| integral = self.uxds[data_var].integrate() | ||
| def track_nbytes_integrate(self, resolution): | ||
| """Grid footprint after integrating.""" | ||
| self.uxds[data_var].integrate() | ||
| return grid_nbytes(self.uxds.uxgrid) | ||
|
|
||
| track_nbytes_integrate.unit = "bytes" | ||
|
|
||
|
|
||
| class GradientColdStartRss: | ||
| """Peak memory of a cold start: import uxarray, open a dataset, take a gradient. | ||
|
|
||
| Whole-process ``ru_maxrss``, not tracemalloc -- the ~250MB uxarray import is | ||
| part of the number by design, because the cold start is the subject. For the | ||
| gradient's own transient cost see ``Gradient.track_peakmem_gradient``, which | ||
| runs one to three orders of magnitude lower. | ||
| """ | ||
|
|
||
| param_names = ["resolution"] | ||
| params = [["480km", "120km"]] | ||
|
|
||
| def setup_cache(self): | ||
| """Compile the njit kernels before anything is measured.""" | ||
| for resolution in self.params[0]: | ||
| grid, data = file_path_dict[resolution] | ||
| ux.open_dataset(grid, data)[data_var].gradient() | ||
|
|
||
| setup_cache.timeout = 1800 | ||
|
|
||
| def peakmem_gradient(self, resolution): | ||
| grid, data = file_path_dict[resolution] | ||
| ux.open_dataset(grid, data)[data_var].gradient() | ||
|
|
||
|
|
||
| class GeoDataFrame(DatasetBenchmark): | ||
|
|
@@ -90,7 +158,7 @@ def time_to_geodataframe(self, resolution, exclude_antimeridian): | |
|
|
||
| class ConnectivityConstruction(DatasetBenchmark): | ||
| def time_n_nodes_per_face(self, resolution): | ||
| self.uxds.uxgrid.n_nodes_per_face | ||
| _ = self.uxds.uxgrid.n_nodes_per_face | ||
|
|
||
| def time_face_face_connectivity(self, resolution): | ||
| ux.grid.connectivity._populate_face_face_connectivity(self.uxds.uxgrid) | ||
|
|
||
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.
Thank you for leaving this comment (and similar comment on the peakmem_gradient benchmark). I saw the benchmark results and came to this part of the code specifically to comment that maybe it should be like track_peakmem benchmarks instead of ASV's default peakmem here. But now I see that it is intentional.
So, instead of suggesting to change any implementation, my only suggestion would be to change the class name to make it clearer that this is intentionally for a cold start. Maybe something like
FaceBoundsPeakMemColdStart?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.
Renamed to
FaceBoundsTracemallocto signal that this is more of a tracemalloc benchmark than a true peakmem benchmarkThere 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.
nvm, in working with the other benchmarks PR, it's clear that
ColdStartis a better description