Skip to content

[FEA] Multi-String Output & LTO Transform Examples - #23173

Open
lamarrr wants to merge 46 commits into
NVIDIA:mainfrom
lamarrr:multi-string-output
Open

[FEA] Multi-String Output & LTO Transform Examples#23173
lamarrr wants to merge 46 commits into
NVIDIA:mainfrom
lamarrr:multi-string-output

Conversation

@lamarrr

@lamarrr lamarrr commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Description

This pull request implements an HTTP request line parser. It compares cuDF's regex API, JIT-transform API, and the new LTO-transform API.

It is intended to illustrate:

  • Multi-output transforms
  • Efficient string sizing
  • Usage & linking of pre-compiled fragments
  • How to use librtcx to manage pre-compiled fragments

This pull request also fixes a bug where the memory resource wasn't being used in the regex extract API

Checklist

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

@copy-pr-bot

copy-pr-bot Bot commented Jul 8, 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.

@github-actions github-actions Bot added libcudf Affects libcudf (C++/CUDA) code. CMake CMake build issue labels Jul 8, 2026
Comment thread cpp/examples/string_transforms/http_logs/udf.cuh Outdated
@lamarrr lamarrr changed the title [FEA] Multi string output [FEA] Multi-String Output Examples (JIT & LTO) Jul 14, 2026
@lamarrr lamarrr added feature request New feature or request non-breaking Non-breaking change labels Jul 14, 2026
@lamarrr
lamarrr marked this pull request as ready for review July 14, 2026 19:32
@lamarrr
lamarrr requested review from a team as code owners July 14, 2026 19:32
@lamarrr
lamarrr requested review from simoneves and vuule July 14, 2026 19:32
@lamarrr
lamarrr force-pushed the multi-string-output branch from 5ab62fb to 9f364ec Compare July 14, 2026 19:33
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an HTTP log transformation example with regex, precompiled, JIT, and LTO execution paths, embedded request-line parsing kernels, configurable fragment dependencies, relocated embed outputs, updated UDF signatures, and memory-resource propagation.

Changes

HTTP log transformations

Layer / File(s) Summary
Configurable fragment build and embedding
cpp/cmake/Modules/AddFragment.cmake, cpp/CMakeLists.txt, cpp/benchmarks/CMakeLists.txt, cpp/tests/CMakeLists.txt, cpp/examples/string_transforms/CMakeLists.txt
Adds explicit fragment include directories, link libraries, and compile options; relocates generated embed and instantiation outputs; updates library, benchmark, test, and example fragment wiring.
HTTP log fragment kernels
cpp/examples/string_transforms/http_logs/fragments.cu
Defines device functions and conditional UDF entry points that compute field sizes and write method, query-stripped path, and HTTP version outputs.
HTTP log execution paths
cpp/examples/string_transforms/http_logs/transforms.cpp, cpp/examples/string_transforms/README.md
Implements regex, precompiled, JIT, and LTO variants with CLI handling, sampling, timing, output, allocation statistics, and documentation.
Runtime contract and allocation updates
cpp/src/transform/transform.cu, cpp/src/jit/cache.hpp, cpp/src/strings/extract/extract.cu
Updates reflected user-data UDF signatures, makes kernel launching const-qualified, and routes extraction temporary allocations through the supplied memory resource.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • rapidsai/cudf#23137: Updates the rtcx embedding and CMake integration used by these fragment build changes.

Suggested labels: improvement, bug

Suggested reviewers: vuule, simoneves, vyasr

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% 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
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.
Title check ✅ Passed The title clearly summarizes the main change: multi-output string examples with LTO transform support.
Description check ✅ Passed The description is directly related to the HTTP request-line parser, multi-output transforms, LTO fragments, and the regex memory resource fix.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@lamarrr

lamarrr commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

HTTP log transform benchmark results

Benchmarks were run on an NVIDIA RTX A6000 using a Release build with CUDA 13.3 and NVIDIA
driver 595.71.05.

Each result is the mean and sample standard deviation from three independent processes. Each
process measured one cold transform followed by three warm transforms. Warm time is the mean of
those three calls, and warm speedup is relative to the precompiled implementation at the same row
count.

Rows Variant Cold (s) Warm (ms) Warm Mrows/s Warm GiB/s Peak (GiB) Alloc/call (GiB) Warm speedup
1,000,000 precompiled 0.0677 ± 0.0002 69.364 ± 0.206 14.4 0.961 0.076 0.076 1.0x
1,000,000 JIT 1.4394 ± 0.0066 3.128 ± 0.060 319.8 21.327 0.042 0.053 22.2x
1,000,000 LTO 1.5334 ± 0.0041 3.200 ± 0.017 312.5 20.841 0.042 0.053 21.7x
10,000,000 precompiled 0.5678 ± 0.0008 576.842 ± 0.654 17.3 1.156 0.759 0.762 1.0x
10,000,000 JIT 1.4549 ± 0.0162 17.398 ± 0.359 574.9 38.333 0.423 0.535 33.2x
10,000,000 LTO 1.5532 ± 0.0083 17.597 ± 0.360 568.4 37.899 0.423 0.535 32.8x
50,000,000 precompiled 3.3342 ± 0.0224 3,362.385 ± 17.594 14.9 0.991 3.797 3.809 1.0x
50,000,000 JIT 1.4785 ± 0.0135 42.777 ± 0.349 1,168.9 77.925 2.115 2.674 78.6x
50,000,000 LTO 1.5598 ± 0.0181 43.013 ± 0.216 1,162.5 77.496 2.115 2.674 78.2x
149,829,350 precompiled 12.0598 ± 0.2866 12,087.652 ± 281.887 12.4 0.873 11.378 11.413 1.0x
149,829,350 JIT 1.5285 ± 0.0016 101.220 ± 0.676 1,480.3 104.201 6.337 8.012 119.4x
149,829,350 LTO 1.6104 ± 0.0222 101.314 ± 0.972 1,479.0 104.107 6.337 8.012 119.3x

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/src/strings/extract/extract.cu (1)

163-163: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

extract_single's temporary buffer still bypasses the caller's mr.

auto indices = rmm::device_uvector<string_index_pair>(input.size(), stream); omits mr, the exact same anti-pattern that Line 88's fix just addressed for extract. The final column at Line 170 correctly uses mr, but this intermediate allocation still goes through the default resource — the memory-resource fix in this PR appears incomplete.

🔧 Proposed fix
-  auto indices = rmm::device_uvector<string_index_pair>(input.size(), stream);
+  auto indices = rmm::device_uvector<string_index_pair>(input.size(), stream, mr);

As per coding guidelines, **/* changes should "Add unit tests and unit benchmarks for feature and bug-fix contributions" — worth confirming a test exercises extract_single with a non-default MR (e.g., a tracking/statistics resource) to catch this class of bug going forward.

🤖 Prompt for 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.

In `@cpp/src/strings/extract/extract.cu` at line 163, Update the temporary indices
allocation in extract_single to pass the caller-provided mr, matching the
existing allocation pattern in extract and the final column allocation. Confirm
or add coverage that invokes extract_single with a non-default memory resource
and verifies the allocation uses it.

Source: Coding guidelines

🤖 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/examples/string_transforms/http_logs/fragments.cu`:
- Around line 15-39: Guard malformed request lines before calculating offsets in
fragments.cu ranges 15-39 and 43-72, and in transforms.cpp range 38-93. Update
each duplicated request-line parsing implementation to detect missing separators
or the HTTP/ marker and return the established failure result before assigning
path_size or version_size, preventing negative sizes from reaching the
offset-building scan.

---

Outside diff comments:
In `@cpp/src/strings/extract/extract.cu`:
- Line 163: Update the temporary indices allocation in extract_single to pass
the caller-provided mr, matching the existing allocation pattern in extract and
the final column allocation. Confirm or add coverage that invokes extract_single
with a non-default memory resource and verifies the allocation uses it.
🪄 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: 06f2f8bd-5d19-41b5-ac22-1696bad211b3

📥 Commits

Reviewing files that changed from the base of the PR and between 709e821 and 9f364ec.

⛔ Files ignored due to path filters (1)
  • cpp/examples/string_transforms/http_logs/logs.csv is excluded by !**/*.csv
📒 Files selected for processing (6)
  • cpp/cmake/Modules/AddFragment.cmake
  • cpp/examples/string_transforms/CMakeLists.txt
  • cpp/examples/string_transforms/README.md
  • cpp/examples/string_transforms/http_logs/fragments.cu
  • cpp/examples/string_transforms/http_logs/transforms.cpp
  • cpp/src/strings/extract/extract.cu

Comment thread cpp/examples/string_transforms/http_logs/fragments.cu Outdated
lamarrr added 2 commits August 3, 2026 17:37
- Implemented a CUDA-based URL parser in `fragments.cu` that extracts components such as protocol, host, port, path, query, and fragment from URLs.
- Created a CSV log file `logs.csv` containing sample log entries with various URL formats for testing purposes.
- Developed a comprehensive transformation utility in `transforms.cpp` that utilizes the URL parser to process log entries and extract URL components.
- Added support for multiple transformation methods including regex, precompiled, JIT, and LTO.
- Enhanced error handling and input validation in the main execution flow.
@lamarrr

lamarrr commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

URL multi-extract benchmark results

This benchmarks extracting the six RFC 3986 URL components (scheme, host, port,
path, query, and fragment) from unstructured CSV log lines. The workload includes
low-cardinality messages, records without URLs, and URLs embedded at varying positions so
the parser must search within each string.

Benchmark Data

benchmark.csv

Summary

At the largest common interval (80M rows), the fused JIT and LTO implementations reach
approximately 34 GB/s and have nearly identical warm performance. Compared with the
regex-free precompiled libcudf pipeline, JIT is 13.0x faster, reduces peak allocations by
85.5%, and reduces cumulative allocation traffic by 93.8%. Compared with the RFC-derived
regex implementation, JIT is 372x faster at this interval.

Implementation Throughput (GB/s) Rows/s Peak memory (GB) NSYS total allocations (GB)
Regex 0.092 0.696M 11.39 70.64
Precompiled 2.641 19.98M 38.51 357.44
JIT 34.244 259.13M 5.57 22.31
LTO 33.961 256.99M 5.57 22.31

Peak memory is the executable's logical RMM high-water mark. NSYS total allocations is
cumulative CUDA allocation traffic across one cold and one warm call; it is not simultaneous
resident memory and can exceed physical GPU capacity.

Throughput

Compilation latency

These are medians of three 100K-row trials. Each value includes compilation and the measured
transform call:

Compilation state Median time
JIT, no pre-existing PCH 1.551 s
JIT, live warm PCH 0.577 s
LTO, cold cache 0.225 s

The warm-PCH JIT call is 62.8% faster than cold JIT. Cold LTO is 6.9x faster than cold
JIT while retaining equivalent warm throughput. For the PCH comparison, the libcudf kernel
cache and CUDA codegen cache were disabled; both JIT calls compile in one process, allowing
the second call to reuse NVRTC's live automatic PCH without reusing a compiled kernel.

Compilation latency

Capacity

The largest verified row count and the immediately following failed probe were measured for
each implementation:

Implementation Maximum verified First failed probe
Regex 190M 200M
Precompiled 82M 83M
JIT 290M 291M
LTO 290M 291M

JIT/LTO reaches 290M rows with a 20.18 GB executable allocation high-water mark. Precompiled
runs out of allocatable GPU memory at a much lower row count because its sequence of string-operations creates substantially more intermediate data and allocation traffic.

Peak memory

Total allocation traffic

Methodology and environment
  • GPU: NVIDIA RTX A6000, 49,140 MiB; driver 595.84; CUDA 13.3
  • Tools: NSYS 2026.1.3, NCU 2026.2.1, Linux perf 7.0.12
  • Throughput is decimal GB/s: (input bytes + output bytes) / warm seconds / 1e9

@GregoryKimball

GregoryKimball commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Is the regex timing correct in #23173 (comment) above? 300s for 150M rows is much higher than expected. Usually regex is in the 1 GB/s throughput in the worst case.

Also the cold JIT times seem a bit too fast in the "benchmark.csv"
image

@lamarrr

lamarrr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@GregoryKimball, the timing is as expected.
The regex used for extracting an RFC 3986 URI component string in an arbitrary string is ((?:^|[^A-Za-z0-9+.-])([A-Za-z][A-Za-z0-9+.-]*):\/\/(?:[^@\/?# \t\n\r"<>]*@)?(\[[^\]\/?# \t\n\r"<>]*\]|[^\/:?# \t\n\r"<>]*)(?::([0-9]*))?([^?# \t\n\r"<>]*)(?:\?([^# \t\n\r"<>]*))?(?:#([^ \t\n\r"<>]*))?(?:$|[ \t\n\r"<>])).
The regex engine's throughput isn't absolute; it is entirely dependent on the complexity of the regex, the number of state transitions/restarts, and the amount of work the regex interpreter needs to perform for each encountered character in the string.

@lamarrr
lamarrr requested review from a team as code owners August 14, 2026 12:57
@lamarrr

lamarrr commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 8bdc9aa

@lamarrr

lamarrr commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

URL log transform benchmark results

These benchmarks compare the regex implementation, the existing precompiled string-API
implementation, CUDA source JIT, and LTO JIT for extracting six URL components based on the RFC 3986 URI Specification (https://www.rfc-editor.org/info/rfc3986/#page-50).
This is equivalent to Velox's extract_uri function.

Throughput

At 67,108,864 rows—the largest size successfully processed by every implementation—the warmed JIT
implementations achieve:

JIT implementation Speedup vs. precompiled Speedup vs. regex
CUDA JIT, warm 9.89× 348.42×
LTO JIT, warm 9.94× 350.40×
input_throughput

Both JIT implementations process approximately 18.7 GiB/s, compared with 1.89 GiB/s for the
precompiled string APIs and 0.054 GiB/s for regex.

The JIT implementations also reached 268,435,456 rows successfully. The precompiled implementation
reached 67,108,864 rows, while regex reached 189,812,531 rows.

Compilation time

Compilation was measured for a one-row input using Nsight Systems and the cuDF NVTX ranges
surrounding kernel construction. Each value is the sum of two kernel-construction instances: one
sizing kernel and one output kernel.

Before every profile:

  • $HOME/.nv/ComputeCache was cleared.
  • $HOME/.cache/libcudf was cleared.
  • The CUDA compilation cache was disabled.
  • The cuDF/RTCx kernel cache was disabled.
Compilation path NVTX range Time
CUDA source JIT, fully cold get_kernel 1,955.0 ms
CUDA source JIT, PCH warmed get_kernel 565.0 ms
LTO JIT, fully cold get_lto_linked_kernel 194.2 ms
compilation_times

Warming the PCH reduces CUDA source compilation time by approximately 71%. Using precompiled LTO
fragments reduces kernel construction time by approximately:

  • 90% compared with fully cold CUDA source JIT
  • 66% compared with CUDA source JIT after PCH warm-up

The PCH-warmed measurement covers compilation of the two benchmark kernels after the PCH has been
prepared; it does not include the separate one-time PCH construction.

Memory Pressure

At the largest size supported by every implementation, the JIT paths require substantially less
peak GPU memory and generate far less cumulative allocation traffic:

Implementation Peak GPU allocation Total GPU allocation volume
Regex 13.92 GiB 28.68 GiB
Precompiled string APIs 32.56 GiB 136.73 GiB
CUDA JIT 4.34 GiB 5.85 GiB
LTO JIT 4.34 GiB 5.85 GiB

Relative to the precompiled string-API implementation, the JIT paths reduce peak allocation by
approximately 86.7% and total allocation volume by approximately 95.7%. Relative to regex,
they reduce peak allocation by approximately 68.8% and total allocation volume by approximately
79.6%.

All CUDA JIT and LTO JIT warm-up modes have the same measured allocation behavior. The compilation
strategy changes startup cost, but not the allocation pattern of the resulting transform. The JIT
implementation computes exact string offsets and writes the six outputs directly, avoiding the
intermediate columns and repeated materialization required by the regex and composed string-API
implementations. This lower peak footprint and allocation volume also helps the JIT variants process
larger inputs before exhausting GPU memory.

peak_gpu_allocation total_gpu_allocation

Summary

The JIT implementation provides roughly a 10× throughput improvement over the existing string-API
implementation
at large input sizes. LTO JIT preserves that throughput while reducing fully cold
kernel construction from approximately 2.0 seconds to 0.19 seconds.

This makes LTO JIT the strongest overall configuration: near-identical runtime performance to CUDA
source JIT, considerably lower startup latency, and support for larger inputs than the regex and precompiled string API implementations.

Profile Data

Questions

Why is the regex implementation so slow?

The regex path is slow because it executes a complex, general-purpose regex program rather than a
URL-specific GPU kernel.

The main causes are:

  • Instruction interpretation: Each row is processed by cuDF's device regex interpreter. GPU
    threads repeatedly decode regex instructions and update matching state instead of executing
    specialized URL-parsing instructions directly.
  • Control-flow divergence: URL length, delimiters, optional fields, and capture groups vary
    between rows. Threads within a warp therefore follow different regex states and branches.
  • Complex pattern: The RFC-style expression contains six captures and several optional
    components. It must recognize schemes, authority, IPv6 hosts, ports, paths, queries, fragments,
    and surrounding delimiters.
  • Additional output passes: After extract, the implementation calls replace_nulls separately
    for each of the six output columns. This adds GPU passes, allocations, and memory traffic.
  • Intermediate materialization: Regex extraction constructs captured columns through the general
    strings framework. The JIT implementation instead computes exact sizes and writes all six outputs
    directly.
  • Limited compiler specialization: The regex program is precompiled into regex instructions, but
    those instructions are still interpreted on the GPU. CUDA JIT turns the URL parser itself into
    native GPU machine code.

This is why regex remains around 0.054 GiB/s while the warmed JIT paths reach approximately
18.7 GiB/s. The roughly 350× difference reflects both specialized native execution and
avoidance of the regex path's intermediate operations.

This result is specific to this relatively complex extraction workload; it should not be interpreted
as a universal 350× difference for every regex operation.

@lamarrr
lamarrr requested a review from vyasr August 20, 2026 14:07
@GregoryKimball GregoryKimball moved this to Burndown in libcudf Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CMake CMake build issue feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants