Skip to content

Add fast-path logic to some libcudf regex APIs - #22178

Merged
rapids-bot[bot] merged 95 commits into
NVIDIA:mainfrom
davidwendt:regex-fast-paths
Jul 14, 2026
Merged

Add fast-path logic to some libcudf regex APIs#22178
rapids-bot[bot] merged 95 commits into
NVIDIA:mainfrom
davidwendt:regex-fast-paths

Conversation

@davidwendt

@davidwendt davidwendt commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Description

Adds fast-path operations to some of the xx_re() APIs where depending on the pattern, a more efficient algorithm may be used. For example, contains_re() can call regular contains() for literals and starts_with() or ends_with() appropriately if the regex pattern indicates.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@davidwendt davidwendt self-assigned this Apr 16, 2026
@davidwendt davidwendt added 2 - In Progress Currently a work in progress libcudf Affects libcudf (C++/CUDA) code. improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Apr 16, 2026
@copy-pr-bot

copy-pr-bot Bot commented Apr 16, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

@davidwendt

Copy link
Copy Markdown
Contributor Author

/ok to test

rapids-bot Bot pushed a commit that referenced this pull request Apr 27, 2026
Adds a new libcudf API for counting string matches within rows of a strings column. This is meant to be a fast-path option for the current `cudf::strings::count()` which only accepts a regex pattern. If that API determines the pattern only contains a literal string (no regex pattern or flags) then this can be called internally automatically. Exposing this as a public API provides some consistency with other APIs like `find`, `contains`, and `split` which have both regex and non-regex versions.

This API will match the behavior of the regex count API which includes not counting overlapping matches.

Reference fast-path logic being worked on here: #22178

Authors:
  - David Wendt (https://github.com/davidwendt)

Approvers:
  - Bradley Dice (https://github.com/bdice)
  - Muhammad Haseeb (https://github.com/mhaseeb123)

URL: #22288
@davidwendt
davidwendt requested a review from shrshi July 2, 2026 17:45
@NVIDIA NVIDIA deleted a comment from coderabbitai Bot Jul 2, 2026

@bdice bdice left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clear design, good solution.

@GregoryKimball GregoryKimball moved this to Burndown in libcudf Jul 6, 2026
@wjxiz1992

Copy link
Copy Markdown
Contributor

cudf-spark correctness and performance validation

I validated the current head 47158137e0b7ef5ea9b28eea1ac339d3096e2122 against base 33e4e72d42756346f4e25b2ae308c5dca3bb8f16, using spark-rapids-jni 714dad63adabfc043ce45bbc56d72ad661417fd4 and cudf-spark 91cb898b1857fb495052e571624e92a9ee96e4fa on Spark 3.3.0 / Python 3.10 / an RTX 5880 Ada GPU.

Correctness blocker

I found a reproducible semantic regression in the new $ fast path. The direct cuDF Java probe used DEFAULT flags and these inputs:

"abc\n", "abc", "xabc\n", "xabc"

Observed results:

Base:
abc$=true,true,true,true
abc\Z=false,true,false,true

PR head:
abc$=false,true,false,true
abc\Z=false,true,false,true

Under the existing cuDF behavior, $ can match immediately before a final newline. Classifying abc$ as an ordinary ends_with("abc") changes that behavior. \Z remains strict and unchanged.

This looks like a merge blocker. I recommend either preserving the before-final-newline behavior or excluding $ patterns from the ends-with fast path, plus adding a regression test covering "abc\n" with both $ and \Z.

cudf-spark compatibility

The current head completed the full cudf-spark regexp_test.py suite:

75 passed, 0 failed, 0 errors, 0 skipped in 69.36s

Important scope note: cudf-spark passes RegexFlag.EXT_NEWLINE, while this PR enables the literal classifier only for RegexFlag.DEFAULT. cudf-spark also rewrites some pure literals into non-regex expressions. Therefore this result shows compatibility/no regression in existing cudf-spark paths, but it does not exercise the new fast path or clear the direct DEFAULT-flags failure above.

Direct performance

Method: 2,000,000 rows, 3 warmups, 9 measured samples, and three independent fresh JVMs per artifact. The values below are the median of the three per-JVM medians.

Case Base PR head Result
contains literal 2.110 ms 0.813 ms 2.60x faster
contains ^literal 4.217 ms 0.180 ms 23.39x faster
contains literal$ 4.742 ms 0.160 ms 29.69x faster, but semantically incorrect as described above
matches literal 2.165 ms 0.177 ms 12.21x faster
replace literal 16.885 ms 8.462 ms 2.00x faster
split literal 23.443 ms 29.440 ms 25.58% slower

The shortest base kernels showed two GPU clock bands across processes, so the exact large speedup multipliers should not be treated as application-level Spark gains. The split result was stable across all three processes and consistently slower.

My validation recommendation is do not merge the current head until the $ correctness issue is fixed; the split regression should also be investigated.

@davidwendt

davidwendt commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@wjxiz1992
Thanks Allen.
I've fixed the $ issue (and added gtest)
and removed the fast-path for split.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6623ea36-1c36-4348-aeb6-2e7ae750b843

📥 Commits

Reviewing files that changed from the base of the PR and between 2951b25 and b98ea54.

📒 Files selected for processing (2)
  • cpp/include/cudf/strings/detail/find.hpp
  • cpp/tests/strings/contains_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • cpp/tests/strings/contains_tests.cpp

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • String regex handling now uses faster literal-based matching when possible, improving performance for simple patterns.
    • Added support for detecting literal-only, starts-with, and ends-with regex patterns.
  • Bug Fixes
    • Improved results for string search, match, count, and replace operations when regex patterns can be treated as plain text.
    • Better handling of end-of-string matching in regex searches.
  • Tests
    • Expanded string operation coverage for count and end-of-string regex behavior.

Walkthrough

Adds a literal_fast_path enum and regex compilation logic (reprog::check_for_literal_fast_path, regex_program::get_literal_fast_path) to classify regex patterns as literal-only, starts-with, or ends-with. Wires fast-path dispatch into contains_re/matches_re/count_re and replace_re via a new strings::detail::find.hpp header, and updates tests accordingly.

Changes

Regex literal fast-path optimization

Layer / File(s) Summary
New detail header for find APIs
cpp/include/cudf/strings/detail/find.hpp
Declares contains, starts_with, ends_with, count detail functions with explicit stream/mr parameters.
literal_fast_path enum and regex_program API
cpp/include/cudf/strings/regex/flags.hpp, cpp/include/cudf/strings/regex/regex_program.hpp, cpp/src/strings/regex/regex_program.cpp
Adds literal_fast_path enum (NONE/LITERAL_ONLY/STARTS_WITH/ENDS_WITH) and exposes get_literal_fast_path() on regex_program, delegating to reprog.
reprog literal fast-path detection
cpp/src/strings/regex/regcomp.cpp, cpp/src/strings/regex/regcomp.h
Implements check_for_literal_fast_path() classifying compiled regex instructions, removes [[maybe_unused]] from _flags, and extends debug print() output.
contains/matches/count fast-path dispatch
cpp/src/strings/contains.cu, cpp/src/strings/search/count.cu
contains_re, matches_re, count_re query the literal fast path and dispatch to contains/starts_with/ends_with/count when applicable; falls back to regex otherwise.
replace_re fast-path dispatch
cpp/src/strings/replace/replace_re.cu
Reorders replace_re to check the literal fast path early and delegate to generic replace() for LITERAL_ONLY patterns.
Updated count_re and contains_re tests
cpp/tests/strings/contains_tests.cpp
Adds new count_re and contains_re test assertions covering literal and end-of-string regex patterns.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • rapidsai/cudf#22874: Adds regex gtests including in contains_tests.cpp validating similar contains/count/replace regex behavior.
  • rapidsai/cudf#22994: Adds the _flags member to detail::reprog that the new literal fast-path classification depends on.
  • rapidsai/cudf#23065: Expands Python pytest coverage for the same contains_re/replace_re behaviors affected here.

Suggested labels: bug

Suggested reviewers: vyasr, qbacpey, PointKernel

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding fast-path logic to libcudf regex APIs.
Description check ✅ Passed The description is directly related and explains the regex fast-path behavior and examples.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/include/cudf/strings/detail/find.hpp`:
- Around line 14-45: The cudf::strings::detail APIs in find.hpp are incorrectly
exposing default values for stream and mr, which should only be provided by
public APIs. Remove the default arguments from contains, starts_with, ends_with,
and count so these detail-layer functions match the coding guideline and the
pattern used in cudf::strings::detail::split. Keep the parameter order unchanged
with stream before mr, and update the declarations consistently across these
symbols.

In `@cpp/tests/strings/contains_tests.cpp`:
- Around line 806-814: The MULTILINE regex path is not being exercised because
`prog_ml` is created in the `contains_tests` block but the second
`cudf::strings::contains_re` call still uses `*prog` instead of `*prog_ml`.
Update the `contains_re` invocation tied to the `prog_ml =
cudf::strings::regex_program::create(...,
cudf::strings::regex_flags::MULTILINE)` setup so it passes `*prog_ml`, keeping
the expected assertions aligned with the multiline-specific behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 10609d93-f6cc-4c9a-b221-447c304dbb0d

📥 Commits

Reviewing files that changed from the base of the PR and between 815919a and 2951b25.

📒 Files selected for processing (12)
  • cpp/include/cudf/strings/detail/find.hpp
  • cpp/include/cudf/strings/detail/split.hpp
  • cpp/include/cudf/strings/regex/flags.hpp
  • cpp/include/cudf/strings/regex/regex_program.hpp
  • cpp/src/strings/contains.cu
  • cpp/src/strings/regex/regcomp.cpp
  • cpp/src/strings/regex/regcomp.h
  • cpp/src/strings/regex/regex_program.cpp
  • cpp/src/strings/replace/replace_re.cu
  • cpp/src/strings/search/count.cu
  • cpp/src/strings/split/split_re.cu
  • cpp/tests/strings/contains_tests.cpp

Comment thread cpp/include/cudf/strings/detail/find.hpp Outdated
Comment thread cpp/tests/strings/contains_tests.cpp

@lamarrr lamarrr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is really great, thanks for working on this!

@davidwendt

davidwendt commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@wjxiz1992 @mythrocks Could I get a spark review/approval?

@wjxiz1992 wjxiz1992 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Retested the updated HEAD (b98ea54282351dbdc7696f8f2365a3e8359acf10). The correctness issue and the previously observed split regression are resolved.

Validation:

  • STRINGS_TEST: 536/536 passed.
  • End-anchor smoke passed:
    • abc$=true,true,true,true
    • abc\Z=false,true,false,true
  • JNI/libcudf validation build: BUILD SUCCESS.

Direct Java/JNI performance benchmark: 2,000,000 rows, 3 warmups, 9 measured samples, and 3 fresh JVMs per case. Values below are the median of the three per-JVM medians.

Case Previous base Previous PR head Updated PR head
contains literal 2.110 ms 0.813 ms 0.828 ms
contains ^literal 4.217 ms 0.180 ms 0.185 ms
contains literal$ 4.742 ms 0.160 ms 3.154 ms
matches literal 2.165 ms 0.177 ms 0.188 ms
replace literal 16.885 ms 8.462 ms 8.577 ms
split literal 23.443 ms 29.440 ms 23.727 ms

The updated split result is 19.40% faster than the previous PR head and only 1.21% slower than the base, so the prior 25.58% split regression is no longer present. The updated literal$ result is within 0.20% of the previously measured correct control path (3.147949 ms); the old 0.160 ms result came from the incorrect end-anchor fast path.

Looks good to me.

@davidwendt

Copy link
Copy Markdown
Contributor Author

@wjxiz1992 I had already removed the split fast-path since it showed a regression in cudf-spark. Our microbenchmarks showed a significant improvement so I would be grateful for a link to your split benchmark input for comparison and analysis.

Meanwhile I will merge this as is since there are no more regressions and iterate specifically on split in follow up work.

@davidwendt

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit 36366f8 into NVIDIA:main Jul 14, 2026
136 checks passed
@davidwendt
davidwendt deleted the regex-fast-paths branch July 14, 2026 13:41
@davidwendt davidwendt moved this from Burndown to Landed in libcudf Jul 14, 2026
@wjxiz1992

Copy link
Copy Markdown
Contributor

@davidwendt Here is the complete benchmark source and the refreshed results: https://gist.github.com/wjxiz1992/9390e8e9a258cb5cb337b3b54a4ade7c

One clarification: the 25.58% split regression was measured with a direct Java/JNI benchmark through the cuDF Java API, not as a Spark end-to-end timing. I used that layer to isolate the libcudf operation from Spark scheduling and aggregation noise.

The exact split workload is:

  • 2,000,000 non-null rows, alternating 50/50 between these two ASCII strings:
    • alphaNEEDLE-xyz0123456789-alphaNEEDLE-xyz0123456789-alphaNEEDLE-xyz0123456789-alphaNEEDLE-xyz0123456789-tail
    • alphaNEEDLE-xyz0123456789-alphaNEEDLE-xyz0123456789-alphaNEEDLE-xyz0123456789-alphaNEEDLE-xyz0123456789-other
  • The strings are 108 and 109 bytes respectively and each contains eight - delimiters, producing nine pieces per row (18 million child strings in the result).
  • Regex: new RegexProgram("-", CaptureGroups.NON_CAPTURE).
  • Operation: input.stringSplitRecord(program, -1).
  • The input ColumnVector and Java RegexProgram are constructed once outside the timed region and reused.
  • Each timed invocation includes the API call, Cuda.DEFAULT_STREAM.sync(), and closing the output column.
  • 3 warmups, then 9 measured invocations; repeated in 3 fresh JVMs. The reported value is the median of the three per-JVM medians.

Equivalent invocation after compiling the Gist source against the JNI JAR:

REGEX_PERF_VARIANT=head \
java -Xms2g -Xmx8g -cp "$CUDF_JNI_JAR:$SLF4J_JAR:." \
  RegexFastPathBenchmark split_literal 2000000 3 9

The original comparison was 23.443 ms on base versus 29.440 ms on the earlier PR head. After the split fast path was removed, I measured 23.727 ms on b98ea542, which is back within 1.21% of the original base. The relatively high output cardinality (eight matches/nine pieces per row) may be a useful difference to compare with the existing libcudf microbenchmark input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3 - Ready for Review Ready for review by team improvement Improvement / enhancement to an existing function Java Affects Java cuDF API. libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

Status: Landed

Development

Successfully merging this pull request may close these issues.

6 participants