Repository navigation
Faster exact median for common_reference - #4854
galenlynch wants to merge 7 commits into
Conversation
np.median partitions each row with data-dependent branches and mispredicts about once per value. row_median gives the same values and dtype: int16 rows of 4 or more channels are counted into a histogram around the previous row's median, and float32 rows of 64 or more channels are narrowed with branchless counting passes. Other inputs, and installs without numba, keep np.median. On 1 s of 384 column-major channels, as filter steps return them, the median drops from 72 to 11 ms for int16 and from 101 to 38 ms for float32.
|
|
||
|
|
||
| @njit(nogil=True, cache=True) | ||
| def int_row_median(x, window): |
There was a problem hiding this comment.
These functions look very hard to understand and maintain. There's an alternative CMR PR open at the moment (#4836) , which looks simpler. It hasn't been jit-ed, but looks like it could be.
There was a problem hiding this comment.
The open pr is another sorting approach to finding the median, which will have a lot of branches. The histogram approach for integers and counting approach for float should have fewer branches than any sorting approach, and therefore be faster on modern hardware. I can try to JIT the other approach and directly compare if you’d like. My guess is it would still be slower. Happy to add comments to the code here if ease of understanding the code is a problem.
There was a problem hiding this comment.
|
Thanks @galenlynch this is very cool, however my 2 cents is as a library I do not think we should be in the business of rewriting core mathematical functions. This also applies to #4836. In general it makes the library harder to maintain and the risk of bugs greatly increases. Unless the speed up is absolutely mindboggling it is safer to rely on these standard library implementations wherever we can. |
The histogram and interval-narrowing kernels now state their invariants, early exits and fallbacks, so a reader can check them without reconstructing the algorithm. No code changes.
np.sort vectorises 32- and 64-bit sorts, and on the default chain's float32 output it beats the counting kernel (29 vs 39 ms per 1 s of 384 channels) and np.median (100 ms). This is the approach of SpikeInterface#4836. It also covers float64 and needs no numba. The int16 histogram kernel stays, since np.sort is 2.6x slower than np.median there.
|
Thanks for the reviews, @JoeZiminski and @chrishalcrow, both are fair points. I've cut this PR down to one integer kernel, a 55-line numba function with comments, about the size of the correlogram kernels in
One core, 384 channels at 30 kHz, int16 data in RAM. "main" is upstream main; #4859 is the fused prototype. Here is the timing of the median alone, on what
The For float32, For int16, The tests compare it with I think this one kernel earns its place, since it is small, exact, on the default path, and 5.9 times faster than |
Sorting float rows for the median, in beec896, follows @JESUSROYETH's approach in SpikeInterface#4836. Co-authored-by: JESUSROYETH <42451234+JESUSROYETH@users.noreply.github.com>
numba does not accept non-native byte order, so the integer kernel raised on big-endian int16 or uint16 traces. Those now use np.median, as other unsupported dtypes already did.

common_reference(operator="median")callsnp.median(traces, axis=1)on every chunk.np.medianpartitions each row with branches that depend on the data, and on 384 channels it mispredicts about one branch per value.This PR adds
row_median, a numba kernel that returns exactlynp.median's values and dtype:common_referencegets in the default chain: each row is counted into a histogram around the previous row's median. Neighbouring samples have nearly the same median, so most rows take one pass; a row whose median falls outside the histogram is sorted instead.Other inputs, and installs without numba, keep
np.median.Speed. 1 s of 384 highpassed channels, column-major as filter steps return them, one thread, i9-12900K:
common_reference, int16common_reference, float32common_reference(reference="local"), int16The median of 1 s of highpassed traces against the number of channels in each median, as
reference="local"andgroupsproduce.Mispredicts per row drop from 354 to 4 for int16, and from 392 to 47 for float32, measured with hardware counters.
One case is slower: float32 holding only whole numbers, such as unfiltered int16 cast to float32, takes 108 ms against 97.
Tests.
row_medianequalsnp.medianon int8 to uint16 and float32, including ties, saturated channels, a median jumping outside the histogram, column-major and non-contiguous input, and NaN and inf.common_referenceoutput is unchanged for int16 and float32.