Conversation
SwissTable::early_filter_imp_avx2_x8 computed the byte offset of each block with _mm256_mullo_epi32, that is modulo 2^32. With 32-bit group ids (log_blocks >= 14) a block takes 40 bytes, so the offset of every block with id >= 107,374,183 (ceil(2^32 / 40)) wrapped around and the kernel read the status bytes of an unrelated location. The early filter then reported no match, or a match in the wrong slot, and find() missed the key. A hash join whose build side has more than about 400M distinct keys (log_blocks >= 27) silently dropped about 20%, 60% or 80% of the matches at log_blocks 27, 28 and 29. The kernel only runs on CPUs with AVX2 and efficient BMI2 (Intel), so the same join is exact on AMD and ARM, and with ARROW_USER_SIMD_LEVEL=NONE. Compute the offsets in 64 bits with _mm256_mul_epu32, which multiplies the even (A) or, after a 32-bit shift, the odd (B) 32-bit block ids into 64-bit products, as extract_group_ids_avx2 has done since apacheGH-44513. Add a large memory test that runs early_filter on the blocks on both sides of the 4GB boundary of a table with 2^27 blocks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When grow_double() reinserts an entry that had overflowed its home block and the entry's new home block is full, it probes the following blocks. The address of those blocks was computed as block_id_new * block_size_after in 32-bit unsigned arithmetic, which wraps for block ids >= 107,374,183 once the table grows to 2^27 blocks (more than about 400M keys in one table). The entry was then written to an unrelated location of the new table, corrupting other entries, and could not be found afterwards. Use mutable_block_data(), which computes the offset in 64 bits, like the other block accesses since apacheGH-45506. Add a large memory test that grows a table from 2^26 blocks with an entry that has to move past a full block across the 4GB boundary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The new SwissTable tests were only added to the CMake sources of arrow-compute-row-test. Add them to the Meson test sources as well. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
|
Author
|
This is a fix for #51496 |
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.
Rationale for this change
SwissTable::early_filter_imp_avx2_x8computes each block's byte offset with_mm256_mullo_epi32, so modulo 2^32. With 32-bit group ids a block takes 40 bytes, and every block with id >= 107,374,183 (ceil(2^32 / 40)) gets a wrapped offset. The kernel then reads the status bytes from the wrong place, reports no match (or the wrong slot), andfind()misses the key. There is no error.A hash join whose build side has more than ~403M distinct keys (
log_blocks>= 27) loses about 20%, 60% and 80% of its matches atlog_blocks27, 28 and 29. The kernel is only dispatched on CPUs with AVX2 and efficient BMI2 (HasEfficientBmi2(), that is Intel), so the same join is correct on AMD, on ARM and withARROW_USER_SIMD_LEVEL=NONE. See GH-XXXXX for the full analysis and a pyarrow reproducer.While checking the other block offset computations I found the same overflow in scalar code. When
grow_double()reinserts an overflow entry and the entry's new home block is full, it probes the next blocks atblocks_new->mutable_data() + block_id_new * block_size_after, a 32-bit multiply. GH-45506 fixed the line just above but not this one. Once a single table grows to 2^27 blocks, such an entry is written to an unrelated location and can't be found. This happens on any CPU. A multi-threaded hash join builds small per-partition tables and merges them into a table that is sized up front, so the join numbers below come from the early filter bug alone. A single table that grows past ~403M keys can hit this one, for example in a group-by or in a join built as one partition.What changes are included in this PR?
key_map_internal_avx2.cc: computevoffset_A/voffset_Bin 64 bits with_mm256_mul_epu32on the even and odd 32-bit block ids. This is the same even/odd split as before, now without wraparound, and matches whatextract_group_ids_avx2does since [C++][Python] Pyarrow.Table.join() breaks on large tables v.18.0.0.dev486 #44513.key_map_internal.cc: usemutable_block_data(), which computes the offset in 64 bits, for the probe ingrow_double().key_map_test.cc(added toarrow-compute-row-testin CMake and Meson) with two large-memory tests:SwissTable.EarlyFilterOver4GB: a table with 2^27 blocks and no hash array (5.4 GB). It inserts keys into the blocks on both sides of the 4GB boundary and checksearly_filterfor every supported hardware flag set (scalar, and AVX2 where available).SwissTable.GrowOver4GB(about 14.5 GB peak): it grows a table from 2^26 to 2^27 blocks with an entry that has to move past a full block beyond 4GB, then checksearly_filterandfindfor all keys. It triggers the growth throughnum_inserted()and the 75% fill threshold, and asserts that the table grew.Are these changes tested?
Yes. Both new tests are
LARGE_MEMORY_TESTs, so they only run withARROW_LARGE_MEMORY_TESTS=ON. The AVX2 part ofEarlyFilterOver4GBonly exercises the bug on Intel CPUs with AVX2 and BMI2.The results below are from Google Cloud
n1-highmem-32(Intel Xeon @ 2.00GHz, AVX2, BMI2 and AVX-512), Ubuntu 24.04, gcc 13.3, withARROW_RUNTIME_SIMD_LEVEL=MAX, in Release and in Debug withBUILD_WARNING_LEVEL=CHECKIN:mainwith only the new tests added:EarlyFilterOver4GBfails atkey_map_test.cc:78withlocal_slots[i]7, expected 0, atblock_id = 107374183,hardware_flags = 32(AVX2). It passes withARROW_USER_SIMD_LEVEL=NONE.GrowOver4GBfails atkey_map_test.cc:151(key 8 not found,hardware_flags = 0), with any SIMD level.EarlyFilterOver4GBpasses, andGrowOver4GBstill fails at:151.ARROW_USER_SIMD_LEVEL=NONE(about 10 s and 16 s in Release).arrow-compute-row-test: 90 tests pass (the 88 existing ones and the 2 new ones).arrow-acero-hash-join-node-test: 36 pass and 1 is skipped (BuildSideLargeRowIds, which is skipped in its body), the same as onmain.-Werrorbuild.Before the fix, the pyarrow reproducer from the issue gave these results on the same machine type with pyarrow 25.0.1 (an inner join where all 2,000,000 probe keys have a match):
ARROW_USER_SIMD_LEVEL=NONEI did not rerun the pyarrow reproducer on a patched build. The C++ tests above cover the same code path.
Are there any user-facing changes?
No API changes. Hash joins with more than ~403M distinct build keys on Intel AVX2 CPUs, and single hash tables that grow past ~403M keys, now return correct results.
This PR contains a "Critical Fix". Both bugs silently produce incorrect data. The AVX2 early filter makes hash joins with more than ~403M distinct build-side keys drop 20% to 80% of the matching rows on Intel CPUs with AVX2 and BMI2. The
grow_double()overflow can misplace entries, and so lose keys, when a hash table grows to 2^27 blocks or more (past ~403M keys), on any CPU.Was AI used for this PR?
In accordance to the AI generation guidelines, please disclose below whether and how AI was used in this PR.
PR code and description written by:
Reviewed before submission by:
Claude Code (Claude Opus) did the root-cause analysis and wrote the fix, the tests and this description, under my direction. I found the bug in a production pipeline. A separate AI pass reviewed the diff and rebuilt and reran the tests on an Intel machine.