Add Spark-safe UTF-8 string reverse for truncated trailing bytes - #4930
Add Spark-safe UTF-8 string reverse for truncated trailing bytes#4930firestarman wants to merge 2 commits into
Conversation
Libcudf reverse assumes well-formed UTF-8 and can over-read into the next row when Spark StringType holds truncated trailing multi-byte sequences. Add a JNI reverse that matches UTF8String.numBytesForFirstByte with SPARK-57507 remaining-byte clamping. Signed-off-by: Liangcai Li <firestarmanllc@gmail.com>
Greptile SummaryAdds a Spark-compatible UTF-8 string reversal operation that clamps character widths to each row’s remaining bytes, preventing truncated trailing sequences from crossing row boundaries.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant Spark as Spark caller
participant Java as StringUtils.reverseStrings
participant JNI as StringUtilsJni
participant CUDA as reverse_strings kernel
Spark->>Java: ColumnView
Java->>JNI: native column handle
JNI->>CUDA: strings_column_view
loop Each non-null row
CUDA->>CUDA: Determine Spark character width
CUDA->>CUDA: Clamp width to remaining row bytes
CUDA->>CUDA: Copy chunk into reversed row position
end
CUDA-->>JNI: Output STRING column
JNI-->>Java: Native result handle
Java-->>Spark: ColumnVector
Reviews (2): Last reviewed commit: "Address review: self-contained headers a..." | Re-trigger Greptile |
wjxiz1992
left a comment
There was a problem hiding this comment.
Defensive review complete: no blocking findings on 43ef77b. The SPARK-57507 byte-width/clamping semantics, current cuDF STRING layout, slice and row bounds, stream/MR handling, and JNI/CMake integration are consistent. The Greptile parent-pointer concern does not apply to the cuDF revision pinned here.
|
|
||
| #include <cuda/iterator> | ||
| #include <cuda/std/algorithm> | ||
| #include <thrust/for_each.h> |
There was a problem hiding this comment.
[SHOULD FIX] Include the standard headers used by this translation unit directly
This file uses bare uint8_t and std::make_unique, but currently gets both declarations through transitive CUDA/cuDF includes. That makes the build sensitive to unrelated dependency-header changes. Please keep the repository's prevalent bare uint8_t spelling and include its C header directly; no uint8_t to std::uint8_t rename is needed.
| #include <thrust/for_each.h> | |
| #include <thrust/for_each.h> | |
| #include <memory> | |
| #include <stdint.h> |
There was a problem hiding this comment.
Done in f99f545 — added #include <memory> and #include <stdint.h> (C header for bare uint8_t).
| #include <cudf/column/column.hpp> | ||
| #include <cudf/strings/strings_column_view.hpp> | ||
| #include <cudf/utilities/default_stream.hpp> | ||
| #include <cudf/utilities/memory_resource.hpp> |
There was a problem hiding this comment.
[SHOULD FIX] Make this public header self-contained for std::unique_ptr
The declaration below exposes std::unique_ptr<cudf::column>, but this header does not include <memory> itself. A consumer should not need a particular transitive include order for this declaration to compile.
| #include <cudf/utilities/memory_resource.hpp> | |
| #include <cudf/utilities/memory_resource.hpp> | |
| #include <memory> |
There was a problem hiding this comment.
Done in f99f545 — #include <memory> added to the public header.
|
|
||
| #include <cudf/strings/strings_column_view.hpp> | ||
|
|
||
| #include <string> |
There was a problem hiding this comment.
[SHOULD FIX] Include the defining headers for the test helper's types
bytes_to_string uses std::initializer_list and bare uint8_t, while the test currently relies on other headers to expose both. Please add their defining headers directly and retain the repository-consistent bare uint8_t spelling.
| #include <string> | |
| #include <initializer_list> | |
| #include <stdint.h> | |
| #include <string> |
There was a problem hiding this comment.
Done in f99f545 — added #include <initializer_list>, #include <stdint.h>, and #include <cstring>.
| CUDF_TEST_EXPECT_COLUMNS_EQUAL(*result, input); | ||
|
|
||
| auto const with_nulls = | ||
| cudf::test::strings_column_wrapper({"abc", "", "世界"}, {true, false, true}); |
There was a problem hiding this comment.
[SHOULD FIX] Cover a valid empty string and a nonzero-offset slice
The only empty row here is masked null, so it exits through the null branch instead of exercising the valid-empty branch. The new implementation also combines an offset-aware input device view with normalized output offsets, but no test passes a sliced strings view. Please add both cases so an input-slice offset regression cannot pass unnoticed.
A concrete test edit is:
#include <cudf_test/column_wrapper.hpp>
+#include <cudf/copying.hpp>
#include <cudf/strings/strings_column_view.hpp>
@@
CUDF_TEST_EXPECT_COLUMNS_EQUAL(*reversed, expected);
+
+ auto const slice_source = cudf::test::strings_column_wrapper(
+ {"prefix", "", "ignored", "世界"}, {true, true, false, true});
+ auto const slice = cudf::slice(slice_source, {1, 4}).front();
+ auto const slice_expected =
+ cudf::test::strings_column_wrapper({"", "ignored", "界世"}, {true, false, true});
+ auto const slice_reversed =
+ spark_rapids_jni::reverse_strings(cudf::strings_column_view{slice});
+ CUDF_TEST_EXPECT_COLUMNS_EQUAL(*slice_reversed, slice_expected);
}There was a problem hiding this comment.
Done in f99f545 — extended EmptyAndNulls with a valid empty string and a nonzero-offset cudf::slice case. Local ctest -R REVERSE_STRINGS passes.
|
|
||
| auto const result = spark_rapids_jni::reverse_strings(cudf::strings_column_view{input}); | ||
| CUDF_TEST_EXPECT_COLUMNS_EQUAL(*result, expected); | ||
| } |
There was a problem hiding this comment.
[SHOULD FIX] Exercise Spark's disallowed UTF-8 lead-byte boundaries
The classifier has distinct width-1 behavior for 0xC0-0xC1 and 0xF5-0xFF, but the current tests only cover continuation bytes and valid 2-, 3-, and 4-byte lead bytes. A boundary change at either branch would therefore preserve the current test results while diverging from UTF8String.numBytesForFirstByte. Please add a focused regression test containing both ends of both disallowed ranges.
| } | |
| } | |
| TEST_F(ReverseStringsTests, DisallowedUtf8LeadBytesAreWidthOne) | |
| { | |
| auto const input = cudf::test::strings_column_wrapper( | |
| {bytes_to_string({0x41, 0xC0, 0x42, 0xC1, 0x43, 0xF5, 0x44, 0xFF, 0x45})}); | |
| auto const expected = cudf::test::strings_column_wrapper( | |
| {bytes_to_string({0x45, 0xFF, 0x44, 0xF5, 0x43, 0xC1, 0x42, 0xC0, 0x41})}); | |
| auto const result = spark_rapids_jni::reverse_strings(cudf::strings_column_view{input}); | |
| CUDF_TEST_EXPECT_COLUMNS_EQUAL(*result, expected); | |
| } |
There was a problem hiding this comment.
Done in f99f545 — added DisallowedUtf8LeadBytesAreWidthOne covering 0xC0/0xC1 and 0xF5/0xFF.
Include memory/stdint headers directly, and add C++ coverage for valid empty strings, sliced inputs, and Spark's disallowed UTF-8 lead bytes. Signed-off-by: Liangcai Li <firestarmanllc@gmail.com>
|
build |
Summary
StringUtils.reverseStrings) that followsUTF8String.numBytesForFirstByteand clamps character width to bytes remaining in each row (SPARK-57507).strings::reverseover-reading into the next row when SparkStringTypeholds truncated trailing multi-byte UTF-8.REVERSE_STRINGS) and Java (StringUtilsTest) coverage for well-formed and truncated trailing cases with neighboring sentinel rows.This unblocks cudf-spark work for NVIDIA/cudf-spark#15383 (GPU
reverseon malformed trailing UTF-8).Test plan
ctest -R REVERSE_STRINGSStringUtilsTestreverse cases