Repository navigation
Filter subsets of channels at a time to reduce peak memory - #4840
h-mayorquin wants to merge 10 commits into
Conversation
|
cool! that's a great improvement. I'm surprised looping over channels is faster than numpy's internal axis handling. I guess like you show there is some speedup for lower channel counts but for large counts something is degrading in numpys implementation. Still surprised a python loop is faster than whatever they are doing! very cool, I also agree I don't think there's much to be gained by exposing num channels in the block through the API for that small speed up. |
|
The thing is that their functions loop inside. Usually, python operations are slower but the loop itself is not that bad. My hypothesis is that we are comparing a c loop vs a python loop and there is not much difference and that the speedup comes from avoiding the big temporary allocations. |
alejoe91
left a comment
There was a problem hiding this comment.
@h-mayorquin Just a tiny comment / suggestion
Co-authored-by: Alessio Buccino <alejoe9187@gmail.com>
This reverts commit 8642d4d.
|
One final thing I just thought about before waking up. I did not measure using really small chunks (time chunks) as you guys tend to use. It is me who uses really large chunks. |
|
what chunk size did you use for the plots? Can you try to run it with 1s chunks? I guess things should be similar no? |
|
The chunk size was 1 GiB trace which is what I have used on the past. I did some benchmarks closer to our defaults and I would like to delay merging this and/or keep it out of the release. The default is
So what do I think:
|
|
I think it's fair to postpone to 0.106.0, since this will be mainly impactful on Zarr writes + sharding. Given your initial benchmark and the 1s one, it seems that 16 channels banks could be a good default? I don't mind exposing the new parameter and set a good default honestly... @h-mayorquin does that sound fair? |
|
@alejoe91 16-channel banks look like a good default to me too. My numbers agree with @h-mayorquin's plot: banks of 16 are slightly faster than main even on one thread, and keep peak RAM at about 140% of the chunk instead of 900%. One more option, which I would prefer: sosfiltfilt releases the GIL, so a thread pool can filter the banks in parallel within a single get_traces call, about 5x faster than main at about 400% peak RAM instead of 900%. That fits the trade #4564 measured: fewer chunk workers, each running threads, matched the speed of many workers at a fraction of the total RAM. Default bandpass, 384 int16 channels, best of 3. Peak RAM is above the chunk, as a percentage of its size:
Output is identical to main in every case. Single-channel banks should not be threaded: on 8 threads at 1 s they were slower than main, since the per-task overhead outweighs the work. Threading the banks means submitting each one to a pool instead of looping over them, about ten lines. The thread count could come from |
|
@galenlynch thanks for the additional benchmark! I agree with the 16-channel default and with following up with the threading per bank. This is a natural follow up and easier implementation than #4564 and could be implemented per processor. @h-mayorquin can you make |
|
@h-mayorquin here is the whole-pipeline benchmark you asked for:
In the full pipeline this PR costs no speed, and banks of 16 are 9% faster. Peak RAM is the same for all three, so another step sets that peak usage, likely the FFT in Threading the banks helps less here than on the filter alone: 2 jobs × 4 threads gives 36 MS/s against 31, because only the bandpass uses the threads. |
|
I changed the internal bank size to 16 channels as I think it is an overall improvement. The full-pipeline results address my concern about slowing down the common workflow (@galenlynch, thanks for running these!). I also started the broader preprocessing survey. My first isolated filtering measurements show about 83% less incremental peak memory with banks of 16, but filtering took 3-8% longer for the tested 1-second and 10-second chunks. That is a different result from your measurements, so I would not claim a universal speed improvement. Complete-pipeline and storage measurements on my machine are running right now. I am still wary of exposing As an example of why I don't want to get into these details yet, this parameter controls execution during |
|
On your question about which execution controls preprocessing needs: I opened #4857 with one idea for threads inside a step. It's set from the existing |
| # much lower peak memory (scipy makes several float copies of its input) | ||
| num_samples = traces_chunk.shape[0] - left_margin - right_margin | ||
| num_channels = traces_chunk.shape[1] | ||
| num_channels_per_bank = 16 |
There was a problem hiding this comment.
Can we define this at the top as NUM_CHANNELS_PER_BANK?
What about |

Follows the discussion in #4813 (Filter in float32 if data is not float64) and #4837 (Implement filtering by banks of channels to reduce RAM).
sosfiltfiltandfiltfiltmake several float64 copies of their input, so filtering a 1 GiB int16 trace peaks at 10 times its size. This PR filters one channel at a time inFilterRecordingSegment.get_tracesand writes each result into a preallocated output, which takes the peak down to 1.02 times the trace size (the output array is almost all of it). The output is identical to main for thesosandbaforms, forward-backward and causal.I did not add a parameter for how many channels to filter at a time, as I don't want to get into how to expose it in this PR. Larger blocks are a bit faster (best is 8 to 16 channels, 9.7 s against 12.7 s) and use more memory, as the figure shows, but the gain is small next to the memory gain here. I would merge this and then discuss separately a common interface to run preprocessing operations per channel, as that needs more research first. Basically, I don't want to get us bogged down discussing an interface where there is an obvious gain to be picked.
sosfiltfilt, float64 coefficients, default bandpass (5th order Butterworth, 300 to 6000 Hz), 1 GiB int16 trace (384 channels, 46.6 s at 30 kHz). The peak includes the output array, the time is the best of 3 runs.The
baform (filtfilt) behaves the same way on the same trace, one channel at a time against all channels at once:ba, mainba, this PRiirnotch), mainiirnotch), this PR