Skip to content

Preserve signed zeros in floating-point SIMD sorting networks - #244

Open
LittlePyx wants to merge 5 commits into
numpy:mainfrom
LittlePyx:fix/preserve-signed-zeros
Open

LittlePyx wants to merge 5 commits into
numpy:mainfrom
LittlePyx:fix/preserve-signed-zeros

Conversation

@LittlePyx

Copy link
Copy Markdown

On AVX2, sorting {+0.0, +0.0, +0.0, +0.0, -0.0} with x86simdsort::qsort produces four negative zeros in ascending order and none in descending order. Both results should contain exactly one negative zero. This addresses the x86 case reported in numpy/numpy#33286.

When two lanes compare equal, x86 floating-point min and max both select their second operand. For independent registers, the sorting network now uses min(a, b) and max(b, a) so each input contributes once. A mirrored merge already compares each pair in both lane orders and takes one lane from each result, so it keeps the original operand order. The change applies only to native floating-point comparisons.

Validation

  • AVX2 hardware — On a Ryzen 7 5800H with GCC 13.3, the new C++ regression test calls the public qsort API in both directions at lengths 5 and 17. All four cases failed on the original code and passed with the fix. The full GoogleTest suite passed (394 tests, zero skips).
  • AVX2 performance — For ascending sorts of 128 random int32 values, median runtime differed from upstream by −0.63% to +0.97% across three input seeds and two library load orders.
  • AVX-512 emulation — Under Intel SDE 9.58, all 2,640 checks preserved the input bit patterns and their counts across sort, select, and partial sort on the Skylake-X, Tiger Lake, and Sapphire Rapids models. Coverage included native FP16 on Sapphire Rapids. These were correctness checks.

AI disclosure

Codex assisted with writing and testing the patch and drafting this PR description.

Signed-off-by: 潘彦茜 <627875273@qq.com>
@r-devulap

Copy link
Copy Markdown
Member

On AVX2, sorting {+0.0, +0.0, +0.0, +0.0, -0.0} with x86simdsort::qsort

is this AVX2 specific? Sounds like this should be true on AVX-512 as well, right?

@LittlePyx

Copy link
Copy Markdown
Author

Yes, I reproduced the signed-zero count issue on AVX-512 as well. I tested AVX2 on a Ryzen 7 5800H and AVX-512 under Intel SDE 9.58 using the Skylake-X model. In the AVX-512 run, the original code changed the zero counts for both float and double. With the patch, the same cases preserved the counts.

Comment thread tests/test-qsort.cpp Outdated
{
for (size_t size : {5, 17}) {
for (bool descending : {false, true}) {
std::vector<double> values(size, 0.0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem exists for all 3 floating point types: _Float16, float and double. Can you extend these tests for these types too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. In 00d7c7c I extended the regression test to _Float16, float, and double in both sort directions. I added a 33-element case because the original lengths (5 and 17) passed for _Float16 even with the old code. Under Intel SDE on the Sapphire Rapids model, all three typed tests failed against the original library and passed with the fix.

Signed-off-by: LittlePyx <627875273@qq.com>
Include 33 elements so the half-precision case crosses an AVX-512 register boundary and reproduces the original failure.

Signed-off-by: LittlePyx <627875273@qq.com>
Comment thread src/xss-common-comparators.hpp Outdated
X86_SIMD_SORT_FINLINE void COEX(reg_t &a, reg_t &b)
{
if constexpr (descend) { ::COEX<vtype, reg_t>(b, a); }
if constexpr (!mirrored && xss::fp::is_floating_point_v<type_t>) {

@r-devulap r-devulap Nov 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this logic into the original COEX definition? You would have to pass mirrored template argument in there but that should be easy. It keeps tie-breaking logic in one place.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the floating-point tie handling into the original COEX in 23419ef. The comparator now forwards the policy and swaps the arguments for descending order. The signed-zero tests for _Float16, float, and double passed under Sapphire Rapids emulation.

Comment thread src/xss-common-comparators.hpp Outdated
}
}

template <bool mirrored = false>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest renaming mirrored to keep_lane_on_tie?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to keep_lane_on_tie in 23419ef. It defaults to true for independent registers, including pivot sampling. Only the reverse-and-merge and swap-and-merge calls pass false: those compare each pair in both lane orders before blending.

Comment thread tests/test-qsort.cpp Outdated
double>;
TYPED_TEST_SUITE(FloatingPointQSort, FloatingPointTypes);

TYPED_TEST(FloatingPointQSort, preserves_signed_zeros)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We dont need a new class, you can use the existing simdsort class and run the test conditional on floating point:

Suggested change
TYPED_TEST(FloatingPointQSort, preserves_signed_zeros)
TYPED_TEST_P(simdsort, preserves_signed_zeros)
{
if constexpr (xss::fp::is_floating_point_v<TypeParam>) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. In d9b4824 I moved the test into the existing simdsort suite, guarded it with if constexpr (xss::fp::is_floating_point_v<TypeParam>), and removed the separate fixture and type list. Under Sapphire Rapids emulation, all three floating-point cases still fail against the original library and pass with the fix.

Keep independent lanes on ties by default. Mirrored reverse and swap merges compare each pair in both orders and opt out before blending the results.

Signed-off-by: LittlePyx <627875273@qq.com>
Signed-off-by: LittlePyx <627875273@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants