Repository navigation
Conversation
Contributor
Author
|
Hey @r-devulap I know its been a while. I've been allowed to restart the project which I had tabled long ago working on integrating this code into oneDAL. I will be submitting a second PR which does a more formal enablement, this gets basic use of keyvalue store fixed for our use case. Let me know if you have any questions. |
Member
|
Good to hear from you again @icfaust and thanks for the PR. I will take a look soon and looking forward to your next PR. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fix key-value sort stalling on 129-256 element arrays with a dominant minimum
Summary
keyvalue_qsortwith 32-bit keys and 32-bit values on AVX-512 runs 8-20x slower than on distinct keys when one valueis most of a 129-256 element array (sub)range and that value is the range's minimum. It recurses to its
max_iterslimit and finishes in the scalar
heap_sort, and ends up 2x slower thanstd::sort. Zero-heavy data (sparsefeatures, counts, one-hot columns) hits this whenever a sort or one of its partitions lands in that size range.
The fix applies the existing median-is-smallest / median-is-largest / near-constant pivot rules of
get_pivot_smartto its small-array branch as well, which today returns the plain
get_pivotmedian. One function, +27/-1.What goes wrong
kvsort_(src/xss-common-keyvaluesort.hpp, v7.0):kvsort_n<..., 128>.get_pivot_smart. For ranges of at most4 * numVecs * numlaneselements(256 for
zmm_vector<float>,<int32_t>and<uint32_t>, 16 lanes), it returnsget_pivot: the median ofnumlanesevenly spaced samples, with no check for duplicates.kvpartition_unrolledsplits the range into[< pivot]and[>= pivot];kvsort_recurses on the left sideonly if
pivot != smallest, and on the right side only ifpivot != biggest.If more than half of the samples equal the range's minimum, the median is the minimum. The left side is then empty
and the right side is the whole range again, so each level partitions the same elements and makes no progress until
max_iters = 2 * log2(n)runs out andheap_sortsorts the range one element at a time.A dominant maximum does not stall: the median is the maximum, everything below it moves left, and the right side is
constant. Descending sorts behave like ascending ones, since
kvsort_sorts ascending and reverses.Ranges above 256 elements already avoid this:
get_pivot_smarthandlesmedian == smallestwithchoosePivotMedianIsSmallest,median == largestwithchoosePivotMedianIsLargest, and an all-equal sample withget_pivot_near_constant. Only the small-array branch skips them. They can still reach it through recursion:partitioning a 300-element range with 50% zeros leaves a zero-heavy left part of 129-256 elements.
Where it applies
The small-array branch is reached for a range of
nelements only when the sort's base case is below4 * 4 * numlanes:keyvalue_qsort, 32-bit key and 32-bit value, AVX-512zmm_vector(16 lanes)keyvalue_qsort, 64-bit key, AVX-512zmm_vector(8 lanes)keyvalue_qsort, mixed 32/64-bit key and valuekeyvalue_qsort, AVX2qsort, AVX-512 32-bitqsort, AVX-512 64-bitqsort, AVX-512 16-bitqsort, AVX2 32-bitqsort, AVX2 64-bitqsort<_Float16>, AVX512-FP16qsortbuilt withXSS_MINIMAL_NETWORK_SORTnumlanesnumlanesThe change
src/xss-pivot-selection.hpp,get_pivot_smart: the small-array branch takes the samenumlanessamples asget_pivot, sorts them withvtype::sort_vec, and runs the same decision chain as the large-array branch below it:all samples equal ->
get_pivot_near_constant; median equals the smallest sample ->choosePivotMedianIsSmallest;median equals the largest ->
choosePivotMedianIsLargest; otherwise the median. The large-array branch,get_pivotand
get_pivot_blocks(used bykvselect_) are unchanged.Results
One core of a Xeon Platinum 8480+ (Sapphire Rapids), GCC,
-O2 -march=sapphirerapids, 20000 repetitions, nanosecondsper sort.
std::sortsorts(key, value)pairs, for reference. Keys: distinct (uniform), or 70% / 90% of thearray equal to the minimum (0) or the maximum (1000) with the rest uniform in 1..999 (
bench_kv_duplicates.cpp).Testing
test_kv_correctness.cpp:keyvalue_qsortagainststd::sortfor float/int32 (with and without NaN andhasnan), int32/int32, uint32/uint32, float/int64, double/int32 and double/int64; n = 1..300 and every 37th sizeto 2000; five patterns (distinct, 90% minimum, 60% of one middle value, 80% of one value, three values); ascending
and descending. It checks the sorted keys, NaN placement, and that the values are a permutation each carrying its
own key. 0 mismatches on
-march=sapphirerapidsand-march=haswell, with and without the change.Not fixed here
qsort<_Float16>on AVX512-FP16 also slows down with a dominant zero (n = 200-500, 70-90% zeros: 3.6-9.3 us against0.6-1.1 us for distinct keys) and falls back to
std::sort. It goes through the patched branch, but the change doesnot help it, so the cause is elsewhere (possibly
next_value<_Float16>inchoosePivotMedianIsSmallest). Notinvestigated further.
Context
Found in a oneDAL's decision forest training prototype, which sorts each feature's values in tree nodes of a few hundred rows
(float keys, int32 row indices). On scikit-learn_bench's RandomForestRegressor case for gisette (1000 x 5000, 87%
zeros), 49% of fit time was in
kvsort_'sheap_sortfallback; with this change the fit took 0.64 s instead of1.87 s, with identical test scores (e.g. this bug caused a x3 slowdown in the prototype in regression on short wide data).
Analysis and implementation was done by claude opus 5.5 (with feedback by me). I have reviewed the code myself and stand by it.