GH-48177: [C++][Parquet] Fix arrow-acero-asof-join-node-test failures on s390x#48180
GH-48177: [C++][Parquet] Fix arrow-acero-asof-join-node-test failures on s390x#48180Vishwanatha-HD wants to merge 3 commits intoapache:mainfrom
Conversation
|
|
kou
left a comment
There was a problem hiding this comment.
Could you fix lint failure?
https://github.com/apache/arrow/actions/runs/19504115732/job/55873075753?pr=48180#step:6:84
diff --git a/cpp/src/arrow/compute/util.cc b/cpp/src/arrow/compute/util.cc
index 66c48631dc..3b671db021 100644
--- a/cpp/src/arrow/compute/util.cc
+++ b/cpp/src/arrow/compute/util.cc
@@ -325,10 +325,11 @@ void bytes_to_bits(int64_t hardware_flags, const int num_bits, const uint8_t* by
bytes_next = SafeLoadUpTo8Bytes(bytes + num_bits - tail, tail);
#else
if (tail == 8) {
- bytes_next = util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes + num_bits - tail));
+ bytes_next =
+ util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes + num_bits - tail));
} else {
- // On Big-endian systems, for bytes_to_bits, load all tail bytes in little-endian order
- // to ensure compatibility with subsequent bit operations
+ // On Big-endian systems, for bytes_to_bits, load all tail bytes in little-endian
+ // order to ensure compatibility with subsequent bit operations
bytes_next = 0;
for (int i = 0; i < tail; ++i) {
bytes_next |= static_cast<uint64_t>((bytes + num_bits - tail)[i]) << (8 * i);
You can use nice pre-commit run --show-diff-on-failure --color=always --all-files cpp.
| #endif | ||
| ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8); | ||
| if (num_bytes == 8) { | ||
| return util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes)); |
There was a problem hiding this comment.
Does this work on big-endian system?
There was a problem hiding this comment.
Thanks for pointing out this.. Now with the way we are handling the tail_bytes and loading the word data, we dont actually need to change "SafeLoadUpTo8Bytes()" function.. With the conditional compilation, this function will never be called on Big-endian architecture.
I have reverted this change.. Tested completely on s390x to see if all the test work. I have pushed a new commit. Please give your review comments. Thanks.
There was a problem hiding this comment.
So we are not going to update this function for big-endian because it won't be called? If so, why don't we keep the above DCHECK(false)?
cpp/src/arrow/compute/util.cc
Outdated
| #if ARROW_LITTLE_ENDIAN | ||
| uint64_t word = SafeLoadUpTo8Bytes(bits_tail, (tail + 7) / 8); | ||
| #else | ||
| int tail_bytes = (tail + 7) / 8; | ||
| uint64_t word; | ||
| if (tail_bytes == 8) { | ||
| word = util::SafeLoad(reinterpret_cast<const uint64_t*>(bits_tail)); | ||
| } else { | ||
| // For bit manipulation, always load into least significant bits | ||
| // to ensure compatibility with CountTrailingZeros on Big-endian systems | ||
| word = 0; | ||
| for (int i = 0; i < tail_bytes; ++i) { | ||
| word |= static_cast<uint64_t>(bits_tail[i]) << (8 * i); | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Why do we need this?
The SafeLoadUpTo8Bytes() change adds support for big-endian, right?
There was a problem hiding this comment.
Now that I have removed the big-endian support to SafeLoadUpTo8Bytes() function, these changes are required as these handle the way we handle the tail_bytes on big-endian systems. If the tail_bytes are equal to 8, then we call directly the SafeLoad to load the data onto "word" variable. And for rest other cases, we need to take care of loading least significant bits to ensure compatibility with "CountTrailingZeros". This is the reason why we wont be able to make a direct call "SafeLoadUpTo8Bytes()" for every tail_bytes.
There was a problem hiding this comment.
I have fixed the lint errors and pushed my changes. Thanks..
There was a problem hiding this comment.
IIUC, here you want to load these bytes in little-endian to be further processed by CountTrailingZeros. What you are doing is not leveraging SafeLoadUpTo8Bytes(), which is supposed to load bytes in big-endian (and currently not implemented), but write your own little-endian loading.
This should work. But I think we'd better do it the other way:
- Implement the big-endian loading in
SafeLoadUpTo8Bytes()(you already did it in your previous commit), keep the call to it here, for both little- and big-endian. - For big-endian, issue an explicit byte swapping for big-endian:
#if !ARROW_LITTLE_ENDIAN word = bit_util::ByteSwap(word); #endif
This way, the code can be more compact and semantic clear. The cost is an extra byte-swapping, which is trivial imho. cc @kou
There was a problem hiding this comment.
Hi @zanmato1984..
I made the code changes as per your suggestion above.. but unfortunately, the testcase doesnt pass.. The thing is that its not just the "byteswap" that is required on the BE systems..
./debug/arrow-compute-row-test --gtest_filter=KeyCompare.CompareColumnsToRowsCuriousFSB
Note: Google Test filter = KeyCompare.CompareColumnsToRowsCuriousFSB
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from KeyCompare
[ RUN ] KeyCompare.CompareColumnsToRowsCuriousFSB
arrow/cpp/src/arrow/compute/row/compare_test.cc:103: Failure
Expected equality of these values:
num_rows_no_match
Which is: 7
1
[ FAILED ] KeyCompare.CompareColumnsToRowsCuriousFSB (2 ms)
[----------] 1 test from KeyCompare (2 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (18 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] KeyCompare.CompareColumnsToRowsCuriousFSB
1 FAILED TEST
There was a problem hiding this comment.
Also, please update the code so I can help on any further failures. The current change isn't sufficient and I'm worrying about we may have false positives.
There was a problem hiding this comment.
@zanmato1984.. Please get my latest code changes to util.cc file..
inline uint64_t SafeLoadUpTo8Bytes(const uint8_t* bytes, int num_bytes) {
ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8);
if (num_bytes == 8) {
return util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes));
} else {
uint64_t word = 0;
#if ARROW_LITTLE_ENDIAN
for (int i = 0; i < num_bytes; ++i) {
word |= static_cast<uint64_t>(bytes[i]) << (8 * i);
}
#else
// Big-endian: most significant byte first
for (int i = 0; i < num_bytes; ++i) {
word |= static_cast<uint64_t>(bytes[i]) << (8 * (num_bytes - 1 - i));
}
#endif
return word;
}
}
In the bits_to_indexes_internal() function >>>>>>>>>>
// Optionally process the last partial word with masking out bits outside range
if (tail) {
const uint8_t* bits_tail = bits + (num_bits - tail) / 8;
uint64_t word = SafeLoadUpTo8Bytes(bits_tail, (tail + 7) / 8);
#if !ARROW_LITTLE_ENDIAN
word = ::arrow::bit_util::ByteSwap(word);
#endif
if (bit_to_search == 0) {
word = ~word;
}
word &= ~0ULL >> (64 - tail);
if (filter_input_indexes) {
bits_filter_indexes_helper(word, input_indexes + num_bits - tail, num_indexes,
indexes);
} else {
bits_to_indexes_helper(word, num_bits - tail + base_index, num_indexes, indexes);
}
}
}
In the bytes_to_bits() function >>>>>>>>>>>>
if (tail) {
uint64_t bytes_next;
bytes_next = SafeLoadUpTo8Bytes(bytes + num_bits - tail, tail);
#if !ARROW_LITTLE_ENDIAN
bytes_next = ::arrow::bit_util::ByteSwap(bytes_next);
#endif
bytes_next &= 0x0101010101010101ULL;
bytes_next |= (bytes_next >> 7); // Pairs of adjacent output bits in individual bytes
bytes_next |= (bytes_next >> 14); // 4 adjacent output bits in individual bytes
bytes_next |= (bytes_next >> 28); // All 8 output bits in the lowest byte
bits[num_bits / 8] = static_cast<uint8_t>(bytes_next & 0xff);
}
And, yes.. This looks to be a new testcase failure with the above mentioned changes..
There was a problem hiding this comment.
Thanks for the update. I'll look into it.
There was a problem hiding this comment.
Hi @Vishwanatha-HD , I guess that test KeyCompare.CompareColumnsToRowsCuriousFSB would still fail even w/o the change I proposed. The fact that it is failing means it is calling SafeLoadUpTo8Bytes, which is supposed to be a DCHECK failure. Your change of removing that DCHECK, which is against its by-design intention, makes it passing false-positively.
Meanwhile, do you see other tests failing with the change I proposed?
There was a problem hiding this comment.
I've pushed a commit containing more fixes that I see necessary. I don't have a big-endian hardware so I'm not able to test it in local. Please pull the code and see if the tests pass and let me know the result. Thanks @Vishwanatha-HD .
d80ee37 to
ec51da8
Compare
Vishwanatha-HD
left a comment
There was a problem hiding this comment.
I have addressed all the review comments. Please re-review. Thanks.
| #endif | ||
| ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8); | ||
| if (num_bytes == 8) { | ||
| return util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes)); |
There was a problem hiding this comment.
Thanks for pointing out this.. Now with the way we are handling the tail_bytes and loading the word data, we dont actually need to change "SafeLoadUpTo8Bytes()" function.. With the conditional compilation, this function will never be called on Big-endian architecture.
I have reverted this change.. Tested completely on s390x to see if all the test work. I have pushed a new commit. Please give your review comments. Thanks.
cpp/src/arrow/compute/util.cc
Outdated
| #if ARROW_LITTLE_ENDIAN | ||
| uint64_t word = SafeLoadUpTo8Bytes(bits_tail, (tail + 7) / 8); | ||
| #else | ||
| int tail_bytes = (tail + 7) / 8; | ||
| uint64_t word; | ||
| if (tail_bytes == 8) { | ||
| word = util::SafeLoad(reinterpret_cast<const uint64_t*>(bits_tail)); | ||
| } else { | ||
| // For bit manipulation, always load into least significant bits | ||
| // to ensure compatibility with CountTrailingZeros on Big-endian systems | ||
| word = 0; | ||
| for (int i = 0; i < tail_bytes; ++i) { | ||
| word |= static_cast<uint64_t>(bits_tail[i]) << (8 * i); | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Now that I have removed the big-endian support to SafeLoadUpTo8Bytes() function, these changes are required as these handle the way we handle the tail_bytes on big-endian systems. If the tail_bytes are equal to 8, then we call directly the SafeLoad to load the data onto "word" variable. And for rest other cases, we need to take care of loading least significant bits to ensure compatibility with "CountTrailingZeros". This is the reason why we wont be able to make a direct call "SafeLoadUpTo8Bytes()" for every tail_bytes.
cpp/src/arrow/compute/util.cc
Outdated
| #if ARROW_LITTLE_ENDIAN | ||
| uint64_t word = SafeLoadUpTo8Bytes(bits_tail, (tail + 7) / 8); | ||
| #else | ||
| int tail_bytes = (tail + 7) / 8; | ||
| uint64_t word; | ||
| if (tail_bytes == 8) { | ||
| word = util::SafeLoad(reinterpret_cast<const uint64_t*>(bits_tail)); | ||
| } else { | ||
| // For bit manipulation, always load into least significant bits | ||
| // to ensure compatibility with CountTrailingZeros on Big-endian systems | ||
| word = 0; | ||
| for (int i = 0; i < tail_bytes; ++i) { | ||
| word |= static_cast<uint64_t>(bits_tail[i]) << (8 * i); | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
I have fixed the lint errors and pushed my changes. Thanks..
kou
left a comment
There was a problem hiding this comment.
Does this work?
diff --git a/cpp/src/arrow/compute/util.cc b/cpp/src/arrow/compute/util.cc
index b90b3a6405..163a80d9d4 100644
--- a/cpp/src/arrow/compute/util.cc
+++ b/cpp/src/arrow/compute/util.cc
@@ -30,33 +30,41 @@ namespace util {
namespace bit_util {
inline uint64_t SafeLoadUpTo8Bytes(const uint8_t* bytes, int num_bytes) {
- // This will not be correct on big-endian architectures.
-#if !ARROW_LITTLE_ENDIAN
- ARROW_DCHECK(false);
-#endif
ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8);
if (num_bytes == 8) {
- return util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes));
+ auto word = util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes));
+#if !ARROW_LITTLE_ENDIAN
+ word = bit_util::ByteSwap(word);
+#endif
+ return word;
} else {
uint64_t word = 0;
for (int i = 0; i < num_bytes; ++i) {
+#if ARROW_LITTLE_ENDIAN
word |= static_cast<uint64_t>(bytes[i]) << (8 * i);
+#else
+ word |= static_cast<uint64_t>(bytes[num_bytes - 1 - i]) << (8 * i);
+#endif
}
return word;
}
}
inline void SafeStoreUpTo8Bytes(uint8_t* bytes, int num_bytes, uint64_t value) {
- // This will not be correct on big-endian architectures.
-#if !ARROW_LITTLE_ENDIAN
- ARROW_DCHECK(false);
-#endif
ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8);
if (num_bytes == 8) {
+#if ARROW_LITTLE_ENDIAN
util::SafeStore(reinterpret_cast<uint64_t*>(bytes), value);
+#else
+ util::SafeStore(reinterpret_cast<uint64_t*>(bytes), bit_util::ByteSwap(value));
+#endif
} else {
for (int i = 0; i < num_bytes; ++i) {
+#if ARROW_LITTLE_ENDIAN
bytes[i] = static_cast<uint8_t>(value >> (8 * i));
+#else
+ bytes[i] = static_cast<uint8_t>(value >> (8 * (num_bytes - 1 - i)));
+#endif
}
}
}|
Mostly looks good to me — just one thought after reading through the recent back-and-forth... Given the updated handling of tail bytes and the SafeLoadUpTo8Bytes discussion, I think this PR’s direction still makes sense. I’d just double-check that the tail==8 path really can’t happen with the current unroll logic, since @kou kou raised that question. Willing to help test once the approach is finalized. |
Hi @kou , |
Thanks @k8ika0s as well for your review comments.. I have checked the tail==8 code path, and its not required anymore. I have reverted the changes and pushed the code changes again.. |
ec51da8 to
6b16301
Compare
471c817 to
4d4691a
Compare
Vishwanatha-HD
left a comment
There was a problem hiding this comment.
I have addressed all the review comments.. Please re-review the changes.. Thanks..
@k8ika0s.. Thanks very much for your review on this.. Yeah sure.. You please go ahead and cherry-pick my PR patches and run the tests from your end.. Please let me know the final status.. Thanks.. !! |
Vishwanatha-HD
left a comment
There was a problem hiding this comment.
Resolved all the code review comments
cpp/src/arrow/compute/util.cc
Outdated
| uint64_t bytes_next; | ||
| #if ARROW_LITTLE_ENDIAN | ||
| bytes_next = SafeLoadUpTo8Bytes(bytes + num_bits - tail, tail); | ||
| #else | ||
| // On Big-endian systems, for bytes_to_bits, load all tail bytes in little-endian | ||
| // order to ensure compatibility with subsequent bit operations | ||
| bytes_next = 0; | ||
| for (int i = 0; i < tail; ++i) { | ||
| bytes_next |= static_cast<uint64_t>((bytes + num_bits - tail)[i]) << (8 * i); | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Can we revert this change with the latest SafeLoadUpTo8Bytes() (that has big endian support)?
There was a problem hiding this comment.
@kou.. I tried doing that but the testcase failed on s390x.. We need the "bytes_next |= static_cast<uint64_t>((bytes + num_bits - tail)[i]) << (8 * i);" on big-endian, which we dont get it when we directly call the SafeLoadUpTo8Bytes()..
There was a problem hiding this comment.
Could you share code you tried?
#48180 (review) includes word |= static_cast<uint64_t>(bytes[num_bytes - 1 - i]) << (8 * i);.
There was a problem hiding this comment.
@kou.. The SafeLoadUpTo8Bytes() function remains unchanged..
inline uint64_t SafeLoadUpTo8Bytes(const uint8_t* bytes, int num_bytes) {
ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8);
if (num_bytes == 8) {
return util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes));
} else {
uint64_t word = 0;
for (int i = 0; i < num_bytes; ++i) {
word |= static_cast<uint64_t>(bytes[i]) << (8 * i);
}
return word;
}
}
The bytes_to_bits() function is handling the endianness fix independently.. If I do the endianness conversion inside SafeLoadUpTo8Bytes() function, rather than here, then the testcase is not working..
if (tail) {
uint64_t bytes_next;
#if ARROW_LITTLE_ENDIAN
bytes_next = SafeLoadUpTo8Bytes(bytes + num_bits - tail, tail);
#else
// On Big-endian systems, for bytes_to_bits, load all tail bytes in little-endian
// order to ensure compatibility with subsequent bit operations
bytes_next = 0;
for (int i = 0; i < tail; ++i) {
bytes_next |= static_cast<uint64_t>((bytes + num_bits - tail)[i]) << (8 * i);
}
#endif
bytes_next &= 0x0101010101010101ULL;
bytes_next |= (bytes_next >> 7); // Pairs of adjacent output bits in individual bytes
bytes_next |= (bytes_next >> 14); // 4 adjacent output bits in individual bytes
bytes_next |= (bytes_next >> 28); // All 8 output bits in the lowest byte
bits[num_bits / 8] = static_cast<uint8_t>(bytes_next & 0xff);
}
4d4691a to
b564d35
Compare
Hi @kou, I have a question: why do we need to swap the bytes for big-endian when |
|
I thought that we need to convert to little endian. But is my assumption wrong...? If so, my suggested code was wrong. Sorry. |
Thanks for explaining. I'm not sure either. My assumption is that by |
| #endif | ||
| ARROW_DCHECK(num_bytes >= 0 && num_bytes <= 8); | ||
| if (num_bytes == 8) { | ||
| return util::SafeLoad(reinterpret_cast<const uint64_t*>(bytes)); |
There was a problem hiding this comment.
So we are not going to update this function for big-endian because it won't be called? If so, why don't we keep the above DCHECK(false)?
cpp/src/arrow/compute/util.cc
Outdated
| #if ARROW_LITTLE_ENDIAN | ||
| uint64_t word = SafeLoadUpTo8Bytes(bits_tail, (tail + 7) / 8); | ||
| #else | ||
| int tail_bytes = (tail + 7) / 8; | ||
| uint64_t word; | ||
| if (tail_bytes == 8) { | ||
| word = util::SafeLoad(reinterpret_cast<const uint64_t*>(bits_tail)); | ||
| } else { | ||
| // For bit manipulation, always load into least significant bits | ||
| // to ensure compatibility with CountTrailingZeros on Big-endian systems | ||
| word = 0; | ||
| for (int i = 0; i < tail_bytes; ++i) { | ||
| word |= static_cast<uint64_t>(bits_tail[i]) << (8 * i); | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
IIUC, here you want to load these bytes in little-endian to be further processed by CountTrailingZeros. What you are doing is not leveraging SafeLoadUpTo8Bytes(), which is supposed to load bytes in big-endian (and currently not implemented), but write your own little-endian loading.
This should work. But I think we'd better do it the other way:
- Implement the big-endian loading in
SafeLoadUpTo8Bytes()(you already did it in your previous commit), keep the call to it here, for both little- and big-endian. - For big-endian, issue an explicit byte swapping for big-endian:
#if !ARROW_LITTLE_ENDIAN word = bit_util::ByteSwap(word); #endif
This way, the code can be more compact and semantic clear. The cost is an extra byte-swapping, which is trivial imho. cc @kou
b564d35 to
4daec9b
Compare
d4ad7fb to
965c9e8
Compare
Rationale for this change
This PR is intended to enable Parquet DB support on Big-endian (s390x) systems. The fix in this PR fixes "arrow-acero-asof-join-node-test" testcase failure.
The "arrow-acero-asof-join-node-test" testcase was Aborted/core dumped on Big-endian platforms.
What changes are included in this PR?
The fix includes changes to "util.cc" file to address the Abort/Core dump issues.
Are these changes tested?
Yes. The changes are tested on s390x arch to make sure things are working fine. The fix is also tested on x86 arch, to make sure there is no new regression introduced.
Are there any user-facing changes?
No