Repository navigation
Add a votable parse memory use benchmark - #158
Conversation
|
One thing I notice here is that the These benchmarks report the peak memory of the whole process that runs the benchmark. That includes the Python interpreter, astropy and the other packages it imports, the modules in this benchmark suite along with anything kept in memory in But astropy's github actions benchmark CI asks a different question: did this pull request change memory use? It answers by dividing the PR's result by For the VOTable benchmarks in this PR on Linux, the overhead is about 330 MB. A change that halved the memory use of a parse from 200 MB to 100 MB would show as 530 MB against 430 MB, a ratio of 0.81, and CI wouldn't flag it as an improvement, even though it halved memory use. One can go with larger files, but then that slows down the benchmark. I do notice there are some I noticed this because the ratio above, 0.47, is less for these small files than it was for the very large ~1.2GB file in the PR. For that our improvement was ~38%, and it was larger still for the 2.5GB file from the original issue. So it really depends on the use case - is this primarily for CI and differences? Or for total memory usage for this benchmark test case? |
- Replace peakmem_parse with track_peak_memory_parse, which reports the peak memory added by the parse rather than the peak of the whole process - This leaves out the interpreter, imported modules and asv itself, which are the same for both commits and hid smaller changes when comparing them - Add a peak_added_bytes helper that takes the peak after the call minus the memory in use before it, using resource.getrusage on Linux and the process's physical footprint on macOS. Follow asv patterns. - Skip the benchmark on Windows and other platforms, as I can't test locally.
|
OK, so I've updated the benchmark to subtract off the python interpreter (and astropy, numpy, asv stack) using the
As you can see in the table, each before/after value is ~300MB less. Results on MacOS for astropy/astropy#20509: Which now shows what I expect. And it shows the same regardless of size of the arrays, i.e. I varied LARGE_SIZE which gives the number of rows in the table, and above 100k rows, the ratios are the same. Below that, the fixed size of the parser is added in, so a 25k row table shows a 0.31 and 0.36 ratio for binary and binary2, respectively. Previously, using peakmem_ which includes the size of the interpreter stack in the numerator and denominator, it washes out the changes much more, even when the input files are very large, and at the 60 second limit of the asv benchmarks themselves. For example 1 million row tables just fail to run because of the 60 sec limit. |
| return info | ||
|
|
||
|
|
||
| def peak_added_bytes(func, *args): |
There was a problem hiding this comment.
Would this be useful in a more general module outside of votable?
There was a problem hiding this comment.
Perhaps! I followed the patterns in the asv implementation for the peakmem_ benchmarks. See airspeed-velocity/asv#260 For now, I just put it in this module. peakmem_ benchmarks are not used anywhere in astropy-benchmarks. But it's possible that this fix could go upstream to asv. If the benchmarks are designed to look for memory usage regressions or improvements the way astropy uses them, then they are not working currently as the memory overhead of the python stack trends them towards 1.
There was a problem hiding this comment.
There was a problem hiding this comment.
That looks interesting!
There was a problem hiding this comment.
I am just wondering if instead of building off something that is barely maintained, we should do the memory stuff using something more mainstream. I think memray is the thing to use nowadays (please correct me if I am mistaken).
For the wider discussion of replacing everything, please see:
There was a problem hiding this comment.
But if you prefer to get this in for now to support astropy/astropy#20509 , we can do that too, and punt the wider discussion to a new issue. Lemme know. Thanks!
There was a problem hiding this comment.
Agree that this is sort-of-a one-off type test, but that's just because none of the other tests measure memory usage by a process. As I said, there are no peakmem_ tests in this repo, even though asv supports that, but the asv implementation doesn't do what it advertises, so hence this implementation.
Agree that using something more standard would be good, but I think whether pytest-memray is the correct tool or not should be part of the larger discussion in #120. I don't know enough to have an opinion yet.
If that decision is made in the future, or someone shows how to get pytest plugins to work with asv, or replace asv, then that would be cool! Given we depend on asv here, I think asv's track_ pattern is fine, and my small helper here to track peak memory (before and after) is a reasonable use of it.
If someone else want's to reuse this helper for memory tracking somewhere else in astropy, we can always move the helpers up a level, out of votable and reuse them. I just didn't want to assume to do that too early.
stvoutsin
left a comment
There was a problem hiding this comment.
Thanks for this, very useful benchmark to have!
| raise NotImplementedError('peak_added_bytes needs Linux or macOS') | ||
|
|
||
|
|
||
| class TrackMemVOTableArrays: |
There was a problem hiding this comment.
One small thing I noticed while testing locally, is that setup_cache took about 50s on my machine (astropy 8.0.1), with most of that going into writing the tabledata file (~35s).
It doesn't look as if we override the 60s default (https://asv.readthedocs.io/en/stable/benchmarks.html#benchmark-attributes) so we may run the risk of timing out on a slow runner.
Should we bump the timeout here to something like 180?
There was a problem hiding this comment.
Thanks! I'm seeing a ~13 sec setup_cache time here on my laptop, which is a bit shorter, but point taken on runners being slower. So I benchmarked my benchmark (yo dog):
class TimeVOTableArraySetupCache:
"""Time generation of the large array-column VOTable cache."""
def time_setup_cache(self):
TrackMemVOTableArrays().setup_cache()If I reduce table size from 200k rows to 50k rows and use views of the module-level ra/dec that are created as module globals on votable.py benchmark import, then it reduces my new benchmark's setup time by a factor of 4:
[ 0.00%] ·· Benchmarking virtualenv-py3.12
[50.00%] ··· Running (votable.TimeVOTableArraySetupCache.time_setup_cache--).
[100.00%] ··· votable.TimeVOTableArraySetupCache.time_setup_cache 3.22±0.02s
And it still shows a similar memory use improvement.
I decided to add a time benchmark for the same table parse (runs in ~3 seconds), so as to flag any time speedups or slowdowns for the same table. I already did some work on speedups in the parser, to be completed in a future astropy.io.votable PR
An aside - the ugly helpers I had added here to get the memory measurements on macOS were workarounds for the fact that I was seeing differences between Linux and macOS improvements when using psutil. Linux (in a docker container) was generally giving better improvement numbers than macOS. And that is because macOS compresses memory and might give inconsistent measurements back when I was using asv's built-in peakmem_ pattern. But now that I'm doing an improved before/after and subtracting off the size of the stack, I always see the improvement difference on macOS, it's just less than on Linux. So for consistency, I think it is fine to do something simpler - just use
resource.getrusage(resource.RUSAGE_SELF).ru_maxrssbefore and after. import resource is in the python standard library for Linux and macOS. This is correct on Linux, and will slightly underestimate the improvement on macOS because of the compression. But this is fine.
Our benchmarks actually run on Linux. And this greatly simplifies the helper. If we wanted this to work on Windows, it would need psutil, but again, astropy-benchmarks is not a python package and can't have dependencies. And astropy doesn't have psutil as a dependency except when it pulls in ipython, so I just put a guard there for Windows.
The results from the current state of the benchmark on macOS:
$ python -m asv continuous --config asv.ci.conf.json --split --show-stderr --bench TrackMemoryTimeVOTableArrays --no-only-changed upstream/main 8d7992300b
...
Benchmarks that have improved:
| Change | Before [b9c7d5a5] <votable-iterparse-memory~4> | After [8d799230] <xml-decode-base64-while-parsing> | Ratio | Benchmark (Parameter) |
|----------|--------------------------------------------------|------------------------------------------------------|---------|-------------------------------------------------------------------------|
| - | 267M | 103M | 0.39 | votable.TrackMemoryTimeVOTableArrays.track_peak_memory_parse('binary') |
| - | 267M | 101M | 0.38 | votable.TrackMemoryTimeVOTableArrays.track_peak_memory_parse('binary2') |
Benchmarks that have stayed the same:
| Change | Before [b9c7d5a5] <votable-iterparse-memory~4> | After [8d799230] <xml-decode-base64-while-parsing> | Ratio | Benchmark (Parameter) |
|----------|--------------------------------------------------|------------------------------------------------------|---------|---------------------------------------------------------------------------|
| | 79M | 78.9M | 1 | votable.TrackMemoryTimeVOTableArrays.track_peak_memory_parse('tabledata') |
| | 2.54±0.03s | 2.57±0.06s | 1.01 | votable.TrackMemoryTimeVOTableArrays.time_parse_tabledata('tabledata') |
| | 448±3ms | 438±5ms | 0.98 | votable.TrackMemoryTimeVOTableArrays.time_parse_tabledata('binary') |
| | 540±5ms | 535±7ms | 0.99 | votable.TrackMemoryTimeVOTableArrays.time_parse_tabledata('binary2') |
SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE INCREASED.
- Reduce size of table by 4x so initial writing of the 3 serializations is speeds up. - Simplify the macOS memory before/after measurement by using same method as Linux. This may under-report total memory before and/or after, as macOS uses memory compression. - Add a time_parse_tabledata benchmark for the same table, to track any parsing time improvements.
stvoutsin
left a comment
There was a problem hiding this comment.
Smaller table sort out the timeout for me thanks!
One thing I was wondering, do we know the peak isn't already above current usage when we take before?
The import time temp lists (short_names, long_descriptions) made me wonder, especially for tabledata now that the table is smaller.
If one uses If It is quite difficult to figure out this memory stuff. There's no perfect measurement, as far as I can tell. Everything is a bit of a compromise. |
|
Anything else that needs to happen in this PR? Summary
|
|
Just waiting for an approval from @stvoutsin :) |
stvoutsin
left a comment
There was a problem hiding this comment.
Thanks for your work on this, this all looks great to me!
pllim
left a comment
There was a problem hiding this comment.
Approving by proxy. LGTM at a glance and I trust y'all. Thanks!
This adds a peak memory usage benchmark for votable parse. Spurred by issue astropy/astropy#8946 and PR astropy/astropy#20509
Running it today locally, comparing main to the PR branch:
That PR does not touch
tabledata_format="tabledata"so this is the expected result. The benchmark shows the improvement I was expecting, and allows us to perhaps improve it further in the future.