From 77006804749af4381d276e9d34cf6e3e068b39b8 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Sun, 5 Jul 2026 14:56:56 +0100 Subject: [PATCH 01/27] Add multi-output HTTP string transform example --- cpp/examples/string_transforms/CMakeLists.txt | 39 +- cpp/examples/string_transforms/README.md | 45 ++ .../fragments/http_high_output.cu | 26 ++ .../fragments/http_high_sizes.cu | 26 ++ .../fragments/http_medium_output.cu | 18 + .../fragments/http_medium_sizes.cu | 18 + .../string_transforms/http_log_transforms.cpp | 424 ++++++++++++++++++ .../string_transforms/http_log_udf.cuh | 98 ++++ cpp/examples/string_transforms/http_logs.csv | 13 + .../string_transforms/results/README.md | 16 + .../string_transforms/tools/benchmark.py | 296 ++++++++++++ .../string_transforms/tools/plot_results.py | 109 +++++ 12 files changed, 1126 insertions(+), 2 deletions(-) create mode 100644 cpp/examples/string_transforms/fragments/http_high_output.cu create mode 100644 cpp/examples/string_transforms/fragments/http_high_sizes.cu create mode 100644 cpp/examples/string_transforms/fragments/http_medium_output.cu create mode 100644 cpp/examples/string_transforms/fragments/http_medium_sizes.cu create mode 100644 cpp/examples/string_transforms/http_log_transforms.cpp create mode 100644 cpp/examples/string_transforms/http_log_udf.cuh create mode 100644 cpp/examples/string_transforms/http_logs.csv create mode 100644 cpp/examples/string_transforms/results/README.md create mode 100644 cpp/examples/string_transforms/tools/benchmark.py create mode 100644 cpp/examples/string_transforms/tools/plot_results.py diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index c6869fe12d9e..dd7ce13b7c41 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -21,6 +21,12 @@ include(../fetch_dependencies.cmake) include(rapids-cmake) rapids_cmake_build_type("Release") +# Build and embed the AOT CUDA fragments consumed by cudf::transform_lto. +include(${CMAKE_CURRENT_LIST_DIR}/../../librtcx/embed.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/Modules/AddFragment.cmake) +set(CUDF_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") +set(CUDF_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") + # For now, disable CMake's automatic module scanning for C++ files. There is an sccache bug in the # version RAPIDS uses in CI that causes it to handle the resulting -M* flags incorrectly with # gcc>=14. We can remove this once we upgrade to a newer sccache version. @@ -48,6 +54,35 @@ add_string_transforms_example(format_phone_precompiled format_phone_precompiled. add_string_transforms_example(localize_phone_jit localize_phone_jit.cpp) add_string_transforms_example(localize_phone_precompiled localize_phone_precompiled.cpp) -install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv - DESTINATION bin/examples/libcudf/string_transformers +rtcx_add_embed(http_log_fragments) +add_fragment(http_log_fragments FRAGMENT http_medium_sizes SOURCE + fragments/http_medium_sizes.cu) +add_fragment(http_log_fragments FRAGMENT http_medium_output SOURCE + fragments/http_medium_output.cu) +add_fragment(http_log_fragments FRAGMENT http_high_sizes SOURCE + fragments/http_high_sizes.cu) +add_fragment(http_log_fragments FRAGMENT http_high_output SOURCE + fragments/http_high_output.cu) + +foreach( + fragment_target + http_log_fragments_http_medium_sizes http_log_fragments_http_medium_output + http_log_fragments_http_high_sizes http_log_fragments_http_high_output) + target_include_directories(${fragment_target} + PRIVATE ${CMAKE_CURRENT_LIST_DIR}) +endforeach() + +rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY + "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed") + +add_string_transforms_example( + http_log_transforms + "http_log_transforms.cpp;${http_log_fragments_SOURCE_DIR}/http_log_fragments.s" ) +target_include_directories(http_log_transforms + PRIVATE ${http_log_fragments_SOURCE_DIR}) +add_dependencies(http_log_transforms http_log_fragments) + +install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv + ${CMAKE_CURRENT_LIST_DIR}/http_logs.csv + DESTINATION bin/examples/libcudf/string_transformers) diff --git a/cpp/examples/string_transforms/README.md b/cpp/examples/string_transforms/README.md index 1886f6d86ad8..fae4f3e1cdda 100644 --- a/cpp/examples/string_transforms/README.md +++ b/cpp/examples/string_transforms/README.md @@ -15,6 +15,17 @@ The following examples are included: 5. `extract_email_precompiled` - Performs same transformation on the table as `output` but uses precompiled public APIs 6. `format_phone_jit` - Using a transform kernel to output a string to a pre-allocated buffer 7. `format_phone_precompiled` - Performs same transformation on the table as `preallocated` but uses precompiled public APIs +8. `http_log_transforms` - Compares three multi-output HTTP log extractors: + - `precompiled`: `cudf::strings::extract` with a public regex program. + - `jit`: two CUDA source transforms compiled at runtime. The first produces exact per-row string + sizes; inclusive scans turn those sizes into run-end offsets, and the second writes directly to + the resulting string character buffers. + - `lto`: the same sizing and output transform ABI, AOT-compiled to embedded fatbins and JIT-linked + with libcudf's precompiled transform kernels. + +The HTTP example has a medium request-line workload (method, path, and HTTP version) and a +high-complexity combined-log workload (client IP, timestamp, method, path, status, referer, and user +agent). Both implement the same extraction groups as their comparative regex variant. ## Compile and execute @@ -27,6 +38,40 @@ cmake --build build/ --parallel $PARALLEL_LEVEL build/output info.csv output.csv 100000 ``` +Run the HTTP example directly: + +```bash +build/http_log_transforms http_logs.csv output.csv jit medium 1000000 10 +build/http_log_transforms http_logs.csv output.csv lto high 1000000 10 +build/http_log_transforms http_logs.csv output.csv precompiled high 1000000 10 +``` + +Use `-` for the output path to skip CSV materialization during benchmark-only runs. + +## Benchmarking and profiling + +The benchmark runner records cold and warm wall time, throughput, effective input/output bandwidth, +and RMM allocation cost for all three variants. With `--profile`, Nsight Compute also records kernel +time, achieved warp occupancy, DRAM utilization, and bytes read/written. Use an otherwise idle GPU: + +```bash +python tools/benchmark.py \ + --executable build/http_log_transforms \ + --input http_logs.csv \ + --output-dir results \ + --rows 100000,1000000,10000000 \ + --iterations 10 --repeats 5 --gpu 0 --profile + +python tools/plot_results.py results/benchmark_results.csv +``` + +This creates CSV and Markdown tables plus 16:9 PNG presentation graphs using NVIDIA green, black, +and dark gray. Results should always be reported with the GPU, driver, CUDA toolkit, branch SHA, row +counts, repetitions, and exact command; the repository does not ship fabricated baseline numbers. +The runner gives every repetition a fresh `LIBCUDF_KERNEL_CACHE_PATH`, so the reported cold time +includes CUDA-source compilation or LTO linking while warm iterations reuse the in-process cache; +it also sets `LIBCUDF_JIT_DISABLE_CUDA_CACHE=1` to prevent the CUDA disk cache from hiding cold cost. + If your machine does not come with a pre-built libcudf binary, expect the first build to take some time, as it would build libcudf on the host machine. It may be sped up by configuring the proper `PARALLEL_LEVEL` number. diff --git a/cpp/examples/string_transforms/fragments/http_high_output.cu b/cpp/examples/string_transforms/fragments/http_high_output.cu new file mode 100644 index 000000000000..48fa6310b065 --- /dev/null +++ b/cpp/examples/string_transforms/fragments/http_high_output.cu @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "http_log_udf.cuh" + +extern "C" __device__ int transform(cuda::std::span* client_ip, + cuda::std::span* timestamp, + cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* status, + cuda::std::span* referer, + cuda::std::span* user_agent, + cudf::string_view input) +{ + auto const fields = http_log_udf::parse_high(input); + http_log_udf::copy_range(*client_ip, input, fields.client_ip); + http_log_udf::copy_range(*timestamp, input, fields.timestamp); + http_log_udf::copy_range(*method, input, fields.method); + http_log_udf::copy_range(*path, input, fields.path); + http_log_udf::copy_range(*status, input, fields.status); + http_log_udf::copy_range(*referer, input, fields.referer); + http_log_udf::copy_range(*user_agent, input, fields.user_agent); + return 0; +} diff --git a/cpp/examples/string_transforms/fragments/http_high_sizes.cu b/cpp/examples/string_transforms/fragments/http_high_sizes.cu new file mode 100644 index 000000000000..50eae81df6bf --- /dev/null +++ b/cpp/examples/string_transforms/fragments/http_high_sizes.cu @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "http_log_udf.cuh" + +extern "C" __device__ int transform(int32_t* client_ip_size, + int32_t* timestamp_size, + int32_t* method_size, + int32_t* path_size, + int32_t* status_size, + int32_t* referer_size, + int32_t* user_agent_size, + cudf::string_view input) +{ + auto const fields = http_log_udf::parse_high(input); + *client_ip_size = fields.client_ip.size(); + *timestamp_size = fields.timestamp.size(); + *method_size = fields.method.size(); + *path_size = fields.path.size(); + *status_size = fields.status.size(); + *referer_size = fields.referer.size(); + *user_agent_size = fields.user_agent.size(); + return 0; +} diff --git a/cpp/examples/string_transforms/fragments/http_medium_output.cu b/cpp/examples/string_transforms/fragments/http_medium_output.cu new file mode 100644 index 000000000000..355ddcc6ed7a --- /dev/null +++ b/cpp/examples/string_transforms/fragments/http_medium_output.cu @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "http_log_udf.cuh" + +extern "C" __device__ int transform(cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) +{ + auto const fields = http_log_udf::parse_medium(input); + http_log_udf::copy_range(*method, input, fields.method); + http_log_udf::copy_range(*path, input, fields.path); + http_log_udf::copy_range(*version, input, fields.version); + return 0; +} diff --git a/cpp/examples/string_transforms/fragments/http_medium_sizes.cu b/cpp/examples/string_transforms/fragments/http_medium_sizes.cu new file mode 100644 index 000000000000..cd901da6f952 --- /dev/null +++ b/cpp/examples/string_transforms/fragments/http_medium_sizes.cu @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "http_log_udf.cuh" + +extern "C" __device__ int transform(int32_t* method_size, + int32_t* path_size, + int32_t* version_size, + cudf::string_view input) +{ + auto const fields = http_log_udf::parse_medium(input); + *method_size = fields.method.size(); + *path_size = fields.path.size(); + *version_size = fields.version.size(); + return 0; +} diff --git a/cpp/examples/string_transforms/http_log_transforms.cpp b/cpp/examples/string_transforms/http_log_transforms.cpp new file mode 100644 index 000000000000..8ca26751bc01 --- /dev/null +++ b/cpp/examples/string_transforms/http_log_transforms.cpp @@ -0,0 +1,424 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +enum class variant { PRECOMPILED, JIT, LTO }; +enum class workload { MEDIUM, HIGH }; + +struct options { + std::string input_path; + std::string output_path; + variant implementation; + workload complexity; + cudf::size_type rows; + int iterations; +}; + +constexpr char medium_sizes_udf[] = R"***( +__device__ void size_http_request(int32_t* method_size, int32_t* path_size, + int32_t* version_size, cudf::string_view input) { + auto find_char = [&](char needle, int32_t begin) { + for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; + return input.size_bytes(); + }; + auto method_end = find_char(' ', 0); + auto target_end = find_char(' ', method_end + 1); + auto query_begin = find_char('?', method_end + 1); + auto path_end = query_begin < target_end ? query_begin : target_end; + *method_size = method_end; + *path_size = path_end - method_end - 1; + *version_size = input.size_bytes() - target_end - 6; +} +)***"; + +constexpr char medium_output_udf[] = R"***( +__device__ void extract_http_request(cuda::std::span* method, cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) { + auto find_char = [&](char needle, int32_t begin) { + for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; + return input.size_bytes(); + }; + auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { + for (int32_t i = begin; i < end; ++i) out[i - begin] = input.data()[i]; + }; + auto method_end = find_char(' ', 0); + auto target_end = find_char(' ', method_end + 1); + auto query_begin = find_char('?', method_end + 1); + auto path_end = query_begin < target_end ? query_begin : target_end; + copy_field(*method, 0, method_end); + copy_field(*path, method_end + 1, path_end); + copy_field(*version, target_end + 6, input.size_bytes()); +} +)***"; + +constexpr char high_sizes_udf[] = R"***( +__device__ void size_combined_log(int32_t* ip, int32_t* timestamp, int32_t* method, + int32_t* path, int32_t* status, int32_t* referer, + int32_t* user_agent, cudf::string_view input) { + auto find_char = [&](char needle, int32_t begin) { + for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; + return input.size_bytes(); + }; + auto ip_end = find_char(' ', 0); + auto timestamp_begin = find_char('[', ip_end) + 1; + auto timestamp_end = find_char(']', timestamp_begin); + auto request_begin = find_char('\"', timestamp_end) + 1; + auto method_end = find_char(' ', request_begin); + auto target_end = find_char(' ', method_end + 1); + auto query_begin = find_char('?', method_end + 1); + auto path_end = query_begin < target_end ? query_begin : target_end; + auto request_end = find_char('\"', target_end); + auto status_begin = request_end + 2; + auto status_end = find_char(' ', status_begin); + auto bytes_end = find_char(' ', status_end + 1); + auto referer_begin = find_char('\"', bytes_end) + 1; + auto referer_end = find_char('\"', referer_begin); + auto user_agent_begin = find_char('\"', referer_end + 1) + 1; + auto user_agent_end = find_char('\"', user_agent_begin); + *ip = ip_end; *timestamp = timestamp_end - timestamp_begin; + *method = method_end - request_begin; *path = path_end - method_end - 1; + *status = status_end - status_begin; *referer = referer_end - referer_begin; + *user_agent = user_agent_end - user_agent_begin; +} +)***"; + +constexpr char high_output_udf[] = R"***( +__device__ void extract_combined_log(cuda::std::span* ip, + cuda::std::span* timestamp, + cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* status, + cuda::std::span* referer, + cuda::std::span* user_agent, + cudf::string_view input) { + auto find_char = [&](char needle, int32_t begin) { + for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; + return input.size_bytes(); + }; + auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { + for (int32_t i = begin; i < end; ++i) out[i - begin] = input.data()[i]; + }; + auto ip_end = find_char(' ', 0); + auto timestamp_begin = find_char('[', ip_end) + 1; + auto timestamp_end = find_char(']', timestamp_begin); + auto request_begin = find_char('\"', timestamp_end) + 1; + auto method_end = find_char(' ', request_begin); + auto target_end = find_char(' ', method_end + 1); + auto query_begin = find_char('?', method_end + 1); + auto path_end = query_begin < target_end ? query_begin : target_end; + auto request_end = find_char('\"', target_end); + auto status_begin = request_end + 2; + auto status_end = find_char(' ', status_begin); + auto bytes_end = find_char(' ', status_end + 1); + auto referer_begin = find_char('\"', bytes_end) + 1; + auto referer_end = find_char('\"', referer_begin); + auto user_agent_begin = find_char('\"', referer_end + 1) + 1; + auto user_agent_end = find_char('\"', user_agent_begin); + copy_field(*ip, 0, ip_end); copy_field(*timestamp, timestamp_begin, timestamp_end); + copy_field(*method, request_begin, method_end); copy_field(*path, method_end + 1, path_end); + copy_field(*status, status_begin, status_end); copy_field(*referer, referer_begin, referer_end); + copy_field(*user_agent, user_agent_begin, user_agent_end); +} +)***"; + +[[nodiscard]] std::string const& to_string(variant value) +{ + static std::string const precompiled{"precompiled"}; + static std::string const jit{"jit"}; + static std::string const lto{"lto"}; + switch (value) { + case variant::PRECOMPILED: return precompiled; + case variant::JIT: return jit; + case variant::LTO: return lto; + } + throw std::logic_error("Unknown variant"); +} + +[[nodiscard]] std::string const& to_string(workload value) +{ + static std::string const medium{"medium"}; + static std::string const high{"high"}; + return value == workload::MEDIUM ? medium : high; +} + +[[nodiscard]] options parse_options(int argc, char const** argv) +{ + if (argc != 7) { + throw std::invalid_argument( + "usage: http_log_transforms INPUT.csv OUTPUT.csv " + " ROWS ITERATIONS"); + } + + auto const implementation = std::string_view{argv[3]} == "precompiled" ? variant::PRECOMPILED + : std::string_view{argv[3]} == "jit" ? variant::JIT + : variant::LTO; + if (std::string_view{argv[3]} != "precompiled" && std::string_view{argv[3]} != "jit" && + std::string_view{argv[3]} != "lto") { + throw std::invalid_argument("variant must be precompiled, jit, or lto"); + } + + auto const complexity = std::string_view{argv[4]} == "medium" ? workload::MEDIUM : workload::HIGH; + if (std::string_view{argv[4]} != "medium" && std::string_view{argv[4]} != "high") { + throw std::invalid_argument("workload must be medium or high"); + } + + auto const rows = std::stoll(argv[5]); + auto const iterations = std::stoi(argv[6]); + if (rows < 0 || rows > std::numeric_limits::max()) { + throw std::invalid_argument("ROWS is outside the cudf::size_type range"); + } + if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } + return { + argv[1], argv[2], implementation, complexity, static_cast(rows), iterations}; +} + +[[nodiscard]] std::unique_ptr make_offsets(cudf::column_view const sizes, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + auto inclusive = cudf::scan(sizes, + *cudf::make_sum_aggregation(), + cudf::scan_type::INCLUSIVE, + cudf::null_policy::EXCLUDE, + stream, + mr); + auto const zero = cudf::numeric_scalar{0, true, stream, mr}; + auto first = cudf::make_column_from_scalar(zero, 1, stream, mr); + return cudf::concatenate( + std::vector{first->view(), inclusive->view()}, stream, mr); +} + +[[nodiscard]] std::vector output_specs(std::size_t count, + cudf::type_id type) +{ + return std::vector( + count, cudf::transform_output{cudf::data_type{type}, cudf::output_nullability::ALL_VALID}); +} + +[[nodiscard]] std::span fragment(std::size_t id) +{ + auto const range = http_log_fragments::file_ranges[id]; + return http_log_fragments::files.subspan(range[0], range[1]); +} + +[[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, + workload complexity, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + if (complexity == workload::MEDIUM) { + static auto const program = + cudf::strings::regex_program::create(R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"); + return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); + } + static auto const program = cudf::strings::regex_program::create( + R"regex(^([^ ]+) - [^ ]+ \[([^]]+)\] "([A-Z]+) ([^ ?]+)[^ ]* HTTP/[0-9.]+" ([0-9]{3}) [0-9]+ "([^"]*)" "([^"]*)"$)regex"); + return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); +} + +[[nodiscard]] std::unique_ptr run_two_pass(cudf::column_view input, + workload complexity, + variant implementation, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // Pass 1 emits the exact byte count for every output string and row. Scanning each size column + // produces run-end offsets, so multi_transform can allocate each chars child once. Pass 2 then + // receives a cuda::std::span for every row/output and writes directly into final storage. + auto const count = complexity == workload::MEDIUM ? std::size_t{3} : std::size_t{7}; + auto sizes_out = output_specs(count, cudf::type_id::INT32); + cudf::transform_input inputs[] = {input}; + + std::unique_ptr sizes; + if (implementation == variant::JIT) { + auto const source = complexity == workload::MEDIUM ? medium_sizes_udf : high_sizes_udf; + sizes = cudf::multi_transform(source, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + sizes_out, + std::vector>{}, + std::nullopt, + stream, + mr); + } else { + auto const id = complexity == workload::MEDIUM ? http_log_fragments::http_medium_sizes + : http_log_fragments::http_high_sizes; + sizes = cudf::transform_lto(fragment(id), + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + sizes_out, + std::vector>{}, + std::nullopt, + stream, + mr); + } + + std::vector> offsets; + offsets.reserve(count); + for (auto const& size_column : sizes->view()) { + offsets.push_back(make_offsets(size_column, stream, mr)); + } + + auto strings_out = output_specs(count, cudf::type_id::STRING); + if (implementation == variant::JIT) { + auto const source = complexity == workload::MEDIUM ? medium_output_udf : high_output_udf; + return cudf::multi_transform(source, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + strings_out, + std::move(offsets), + std::nullopt, + stream, + mr); + } + + auto const id = complexity == workload::MEDIUM ? http_log_fragments::http_medium_output + : http_log_fragments::http_high_output; + return cudf::transform_lto(fragment(id), + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + strings_out, + std::move(offsets), + std::nullopt, + stream, + mr); +} + +[[nodiscard]] std::unique_ptr run(cudf::column_view input, + workload complexity, + variant implementation, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + return implementation == variant::PRECOMPILED + ? run_precompiled(input, complexity, stream, mr) + : run_two_pass(input, complexity, implementation, stream, mr); +} + +void write_output(cudf::table_view const result, + workload complexity, + std::string const& output_path) +{ + auto names = complexity == workload::MEDIUM + ? std::vector{"method", "path", "http_version"} + : std::vector{ + "client_ip", "timestamp", "method", "path", "status", "referer", "user_agent"}; + auto options = cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result) + .include_header(true) + .names(names) + .build(); + cudf::io::write_csv(options); +} + +} // namespace + +int main(int argc, char const** argv) +{ + try { + auto const opts = parse_options(argc, argv); + auto const stream = cudf::get_default_stream(); + auto const mr = cudf::get_current_device_resource_ref(); + + auto read_options = + cudf::io::csv_reader_options::builder(cudf::io::source_info{opts.input_path}) + .header(0) + .build(); + auto input_data = cudf::io::read_csv(read_options); + auto input = + opts.rows == input_data.tbl->num_rows() + ? std::move(input_data.tbl) + : cudf::sample(input_data.tbl->view(), opts.rows, cudf::sample_with_replacement::TRUE); + auto const input_index = opts.complexity == workload::MEDIUM ? 0 : 1; + auto const input_bytes = input->get_column(input_index).alloc_size(); + auto const input_column = input->get_column(input_index).view(); + + rmm::mr::statistics_resource_adaptor stats{mr}; + auto const stats_mr = rmm::device_async_resource_ref{stats}; + + stream.synchronize(); + auto const cold_start = std::chrono::steady_clock::now(); + nvtxRangePush("http_log_cold"); + auto cold_result = run(input_column, opts.complexity, opts.implementation, stream, stats_mr); + stream.synchronize(); + nvtxRangePop(); + auto const cold_seconds = + std::chrono::duration{std::chrono::steady_clock::now() - cold_start}.count(); + cold_result.reset(); + + std::unique_ptr result; + auto const warm_start = std::chrono::steady_clock::now(); + nvtxRangePush("http_log_warm"); + for (auto i = 0; i < opts.iterations; ++i) { + result.reset(); + result = run(input_column, opts.complexity, opts.implementation, stream, stats_mr); + } + stream.synchronize(); + nvtxRangePop(); + auto const warm_seconds = + std::chrono::duration{std::chrono::steady_clock::now() - warm_start}.count() / + opts.iterations; + + if (opts.output_path != "-") { + write_output(result->view(), opts.complexity, opts.output_path); + } + + auto const bytes = stats.get_bytes_counter(); + auto const output_bytes = result->alloc_size(); + auto const gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); + + std::cout << std::fixed << std::setprecision(9) + << "RESULT variant=" << to_string(opts.implementation) + << " workload=" << to_string(opts.complexity) << " rows=" << opts.rows + << " cold_seconds=" << cold_seconds << " warm_seconds=" << warm_seconds + << " rows_per_second=" << static_cast(opts.rows) / warm_seconds + << " effective_gib_per_second=" << gib / warm_seconds + << " input_bytes=" << input_bytes << " output_bytes=" << output_bytes + << " peak_memory_bytes=" << bytes.peak << " total_allocated_bytes=" << bytes.total + << " allocated_bytes_per_call=" + << bytes.total / static_cast(opts.iterations + 1) << '\n'; + return EXIT_SUCCESS; + } catch (std::exception const& error) { + std::cerr << error.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/cpp/examples/string_transforms/http_log_udf.cuh b/cpp/examples/string_transforms/http_log_udf.cuh new file mode 100644 index 000000000000..b72dec574eb2 --- /dev/null +++ b/cpp/examples/string_transforms/http_log_udf.cuh @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +namespace http_log_udf { + +struct range { + int32_t begin{}; + int32_t end{}; + + [[nodiscard]] __device__ int32_t size() const { return end - begin; } +}; + +struct medium_fields { + range method; + range path; + range version; +}; + +struct high_fields { + range client_ip; + range timestamp; + range method; + range path; + range status; + range referer; + range user_agent; +}; + +[[nodiscard]] __device__ int32_t find(cudf::string_view const input, + char const needle, + int32_t begin) +{ + for (auto i = begin; i < input.size_bytes(); ++i) { + if (input.data()[i] == needle) { return i; } + } + return input.size_bytes(); +} + +[[nodiscard]] __device__ medium_fields parse_medium(cudf::string_view const input) +{ + auto const method_end = find(input, ' ', 0); + auto const target_end = find(input, ' ', method_end + 1); + auto const query_begin = find(input, '?', method_end + 1); + auto const path_end = query_begin < target_end ? query_begin : target_end; + constexpr int32_t http_prefix_size = 6; // " HTTP/" + + return {{0, method_end}, + {method_end + 1, path_end}, + {target_end + http_prefix_size, input.size_bytes()}}; +} + +[[nodiscard]] __device__ high_fields parse_high(cudf::string_view const input) +{ + auto const ip_end = find(input, ' ', 0); + auto const timestamp_begin = find(input, '[', ip_end) + 1; + auto const timestamp_end = find(input, ']', timestamp_begin); + auto const request_begin = find(input, '"', timestamp_end) + 1; + auto const method_end = find(input, ' ', request_begin); + auto const target_end = find(input, ' ', method_end + 1); + auto const query_begin = find(input, '?', method_end + 1); + auto const path_end = query_begin < target_end ? query_begin : target_end; + auto const request_end = find(input, '"', target_end); + auto const status_begin = request_end + 2; + auto const status_end = find(input, ' ', status_begin); + auto const bytes_end = find(input, ' ', status_end + 1); + auto const referer_begin = find(input, '"', bytes_end) + 1; + auto const referer_end = find(input, '"', referer_begin); + auto const user_agent_begin = find(input, '"', referer_end + 1) + 1; + auto const user_agent_end = find(input, '"', user_agent_begin); + + return {{0, ip_end}, + {timestamp_begin, timestamp_end}, + {request_begin, method_end}, + {method_end + 1, path_end}, + {status_begin, status_end}, + {referer_begin, referer_end}, + {user_agent_begin, user_agent_end}}; +} + +__device__ void copy_range(cuda::std::span output, + cudf::string_view const input, + range const field) +{ + for (auto i = int32_t{0}; i < field.size(); ++i) { + output[i] = input.data()[field.begin + i]; + } +} + +} // namespace http_log_udf diff --git a/cpp/examples/string_transforms/http_logs.csv b/cpp/examples/string_transforms/http_logs.csv new file mode 100644 index 000000000000..ccc9083deb6b --- /dev/null +++ b/cpp/examples/string_transforms/http_logs.csv @@ -0,0 +1,13 @@ +RequestLine,CombinedLog +"GET / HTTP/1.1","203.0.113.10 - alice [05/Jul/2026:14:32:10 +0000] ""GET / HTTP/1.1"" 200 512 ""https://example.com/"" ""Mozilla/5.0 (X11; Linux x86_64)""" +"GET /api/v1/orders/123?expand=items HTTP/1.1","198.51.100.24 - bob [05/Jul/2026:14:32:11 +0000] ""GET /api/v1/orders/123?expand=items HTTP/1.1"" 200 1532 ""https://example.com/cart"" ""Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36""" +"POST /api/v1/login HTTP/2.0","192.0.2.35 - - [05/Jul/2026:14:32:12 +0000] ""POST /api/v1/login HTTP/2.0"" 401 96 ""-"" ""curl/8.8.0""" +"PUT /api/v1/users/8675309 HTTP/1.1","203.0.113.47 - carol [05/Jul/2026:14:32:13 +0000] ""PUT /api/v1/users/8675309 HTTP/1.1"" 204 0 ""https://admin.example.com/users"" ""Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0""" +"DELETE /api/v1/sessions/current HTTP/1.1","198.51.100.58 - dave [05/Jul/2026:14:32:14 +0000] ""DELETE /api/v1/sessions/current HTTP/1.1"" 202 48 ""https://example.com/settings"" ""Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)""" +"PATCH /api/v2/catalog/items/42?locale=en-GB HTTP/2.0","192.0.2.69 - erin [05/Jul/2026:14:32:15 +0000] ""PATCH /api/v2/catalog/items/42?locale=en-GB HTTP/2.0"" 200 2048 ""https://admin.example.com/catalog"" ""Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:127.0) Firefox/127.0""" +"GET /assets/app.8f31c2.js HTTP/1.1","203.0.113.71 - - [05/Jul/2026:14:32:16 +0000] ""GET /assets/app.8f31c2.js HTTP/1.1"" 304 0 ""https://example.com/dashboard"" ""Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) Chrome/126.0 Mobile""" +"HEAD /healthz HTTP/1.1","198.51.100.82 - probe [05/Jul/2026:14:32:17 +0000] ""HEAD /healthz HTTP/1.1"" 200 0 ""-"" ""kube-probe/1.30""" +"OPTIONS /api/v1/orders HTTP/2.0","192.0.2.93 - - [05/Jul/2026:14:32:18 +0000] ""OPTIONS /api/v1/orders HTTP/2.0"" 204 0 ""https://shop.example.net/"" ""Mozilla/5.0 (X11; Fedora; Linux x86_64) Chrome/126.0""" +"POST /graphql?operation=Checkout HTTP/2.0","203.0.113.104 - frank [05/Jul/2026:14:32:19 +0000] ""POST /graphql?operation=Checkout HTTP/2.0"" 200 8192 ""https://shop.example.net/checkout"" ""ShopMobile/6.4.1 (iOS 17.5; Scale/3.00)""" +"GET /search?q=gpu+dataframes&page=2 HTTP/1.1","198.51.100.115 - grace [05/Jul/2026:14:32:20 +0000] ""GET /search?q=gpu+dataframes&page=2 HTTP/1.1"" 200 16384 ""https://www.example.org/"" ""Googlebot/2.1 (+http://www.google.com/bot.html)""" +"POST /events/batch HTTP/1.1","192.0.2.126 - service [05/Jul/2026:14:32:21 +0000] ""POST /events/batch HTTP/1.1"" 503 128 ""-"" ""telemetry-agent/3.12.0 linux/amd64""" diff --git a/cpp/examples/string_transforms/results/README.md b/cpp/examples/string_transforms/results/README.md new file mode 100644 index 000000000000..3c9da97315cf --- /dev/null +++ b/cpp/examples/string_transforms/results/README.md @@ -0,0 +1,16 @@ +# Benchmark result artifacts + +This directory is intentionally source-only. Run `tools/benchmark.py` on a GPU system to generate: + +- `benchmark_results.csv`: raw repetitions for all variants, workloads, and row counts. +- `benchmark_results.md`: presentation-ready aggregate table. +- `ncu__.csv`: per-kernel Nsight Compute data when `--profile` is enabled. +- `profile_results.{csv,md}`: aggregate kernel time, warp occupancy, DRAM utilization, and traffic. +- `environment.md`: branch SHA, GPU/driver, CUDA toolkit, row counts, repetitions, and profile mode. + +Then run `tools/plot_results.py results/benchmark_results.csv` to create NVIDIA-colour PNG graphs +under `results/graphs/`. + +Measured results are not checked in unless they identify the GPU, driver, CUDA toolkit, branch SHA, +row counts, repetitions, and exact profiling command. This avoids presenting synthetic or stale +numbers as measured performance. diff --git a/cpp/examples/string_transforms/tools/benchmark.py b/cpp/examples/string_transforms/tools/benchmark.py new file mode 100644 index 000000000000..511886694f10 --- /dev/null +++ b/cpp/examples/string_transforms/tools/benchmark.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Run the HTTP log transform comparison and write CSV/Markdown result tables.""" + +from __future__ import annotations + +import argparse +import csv +import os +import re +import statistics +import subprocess +from collections import defaultdict +from pathlib import Path + + +RESULT_RE = re.compile(r"^RESULT (?P.+)$", re.MULTILINE) + + +def parse_result(stdout: str) -> dict[str, str]: + match = RESULT_RE.search(stdout) + if match is None: + raise RuntimeError(f"benchmark did not emit a RESULT line:\n{stdout}") + return dict(item.split("=", 1) for item in match.group("values").split()) + + +def command_output(command: list[str]) -> str: + try: + completed = subprocess.run(command, check=False, text=True, capture_output=True) + except OSError as error: + return f"unavailable: {error}" + return (completed.stdout or completed.stderr).strip() + + +def write_metadata(args: argparse.Namespace) -> None: + cuda_output = command_output(["nvcc", "--version"]) + cuda_line = cuda_output.splitlines()[-1] if cuda_output else "unavailable" + metadata = [ + "# Benchmark environment", + "", + f"- Git SHA: `{command_output(['git', 'rev-parse', 'HEAD'])}`", + f"- GPU: `{command_output(['nvidia-smi', '--query-gpu=name,driver_version,memory.total', '--format=csv,noheader', '-i', args.gpu])}`", + f"- CUDA toolkit: `{cuda_line}`", + f"- Executable: `{args.executable}`", + f"- Rows: `{args.rows}`", + f"- Iterations: `{args.iterations}`", + f"- Repeats: `{args.repeats}`", + f"- GPU mask: `{args.gpu}`", + f"- Nsight Compute profiling: `{args.profile}`", + "", + ] + (args.output_dir / "environment.md").write_text("\n".join(metadata), encoding="utf-8") + + +def validate_outputs(args: argparse.Namespace, env: dict[str, str]) -> None: + for workload in ("medium", "high"): + outputs: dict[str, list[list[str]]] = {} + for variant in ("precompiled", "jit", "lto"): + path = args.output_dir / f"validation_{workload}_{variant}.csv" + subprocess.run( + [ + str(args.executable), + str(args.input), + str(path), + variant, + workload, + "12", + "1", + ], + env=env + | { + "LIBCUDF_KERNEL_CACHE_PATH": str( + args.output_dir / "cache" / "validation" / workload / variant + ) + }, + check=True, + text=True, + capture_output=True, + ) + with path.open(newline="", encoding="utf-8") as source: + outputs[variant] = list(csv.reader(source)) + expected = outputs["precompiled"] + for variant in ("jit", "lto"): + if outputs[variant] != expected: + raise RuntimeError(f"{workload} output differs for {variant} and precompiled") + + +def write_markdown(rows: list[dict[str, object]], path: Path) -> None: + grouped: dict[tuple[str, str, int], list[dict[str, object]]] = defaultdict(list) + for row in rows: + grouped[(str(row["workload"]), str(row["variant"]), int(row["rows"]))].append(row) + + headers = [ + "Workload", + "Variant", + "Rows", + "Cold (s)", + "Warm (ms)", + "Throughput (M rows/s)", + "Effective BW (GiB/s)", + "Peak memory (MiB)", + "Allocation cost/call (MiB)", + ] + lines = ["# HTTP log transform benchmark", "", " | ".join(headers), " | ".join(["---"] * len(headers))] + for (workload, variant, row_count), samples in sorted(grouped.items()): + mean = lambda key: statistics.mean(float(sample[key]) for sample in samples) + values = [ + workload, + variant, + f"{row_count:,}", + f"{mean('cold_seconds'):.6f}", + f"{mean('warm_seconds') * 1_000:.3f}", + f"{mean('rows_per_second') / 1_000_000:.3f}", + f"{mean('effective_gib_per_second'):.3f}", + f"{mean('peak_memory_bytes') / (1 << 20):.2f}", + f"{mean('allocated_bytes_per_call') / (1 << 20):.2f}", + ] + lines.append(" | ".join(values)) + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def profile(command: list[str], output: Path, gpu: str) -> None: + metrics = ",".join( + [ + "gpu__time_duration.sum", + "sm__warps_active.avg.pct_of_peak_sustained_active", + "dram__throughput.avg.pct_of_peak_sustained_elapsed", + "dram__bytes_read.sum", + "dram__bytes_write.sum", + ] + ) + env = os.environ | { + "CUDA_VISIBLE_DEVICES": gpu, + "LIBCUDF_JIT_DISABLE_CUDA_CACHE": "1", + "LIBCUDF_KERNEL_CACHE_PATH": str(output.parent / "cache" / "profile" / output.stem), + } + subprocess.run( + [ + "ncu", + "--csv", + "--nvtx", + "--nvtx-include", + "http_log_warm/", + "--metrics", + metrics, + "--log-file", + str(output), + *command, + ], + env=env, + check=True, + text=True, + ) + + +def metric_value(value: str, unit: str) -> float: + number = float(value.replace(",", "")) + prefixes = {"K": 1e3, "M": 1e6, "G": 1e9} + if unit and unit[0] in prefixes: + number *= prefixes[unit[0]] + if unit == "nsecond": + number *= 1e-9 + elif unit == "usecond": + number *= 1e-6 + elif unit == "msecond": + number *= 1e-3 + return number + + +def summarize_profiles(output_dir: Path) -> None: + summaries: list[dict[str, object]] = [] + for path in sorted(output_dir.glob("ncu_*.csv")): + _, workload, variant = path.stem.split("_", 2) + rows = [line for line in path.read_text(encoding="utf-8").splitlines() if not line.startswith("==")] + reader = csv.DictReader(rows) + metrics: dict[str, list[float]] = defaultdict(list) + for row in reader: + name = row.get("Metric Name") + value = row.get("Metric Value") + if not name or value in (None, "n/a"): + continue + metrics[name].append(metric_value(value, row.get("Metric Unit", ""))) + summaries.append( + { + "workload": workload, + "variant": variant, + "kernel_time_seconds": sum(metrics["gpu__time_duration.sum"]), + "warp_occupancy_percent": statistics.mean( + metrics["sm__warps_active.avg.pct_of_peak_sustained_active"] + ), + "dram_throughput_percent": statistics.mean( + metrics["dram__throughput.avg.pct_of_peak_sustained_elapsed"] + ), + "dram_bytes_read": sum(metrics["dram__bytes_read.sum"]), + "dram_bytes_written": sum(metrics["dram__bytes_write.sum"]), + } + ) + if not summaries: + return + with (output_dir / "profile_results.csv").open("w", newline="", encoding="utf-8") as output: + writer = csv.DictWriter(output, fieldnames=list(summaries[0])) + writer.writeheader() + writer.writerows(summaries) + headers = ["Workload", "Variant", "Kernel time (s)", "Warp occupancy (%)", "DRAM peak (%)", "DRAM read (GiB)", "DRAM write (GiB)"] + lines = ["# Nsight Compute profile", "", " | ".join(headers), " | ".join(["---"] * len(headers))] + for row in summaries: + lines.append( + " | ".join( + [ + str(row["workload"]), + str(row["variant"]), + f"{row['kernel_time_seconds']:.6f}", + f"{row['warp_occupancy_percent']:.2f}", + f"{row['dram_throughput_percent']:.2f}", + f"{row['dram_bytes_read'] / (1 << 30):.3f}", + f"{row['dram_bytes_written'] / (1 << 30):.3f}", + ] + ) + ) + (output_dir / "profile_results.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--output-dir", type=Path, default=Path("results")) + parser.add_argument("--rows", default="100000,1000000,10000000") + parser.add_argument("--iterations", type=int, default=10) + parser.add_argument("--repeats", type=int, default=5) + parser.add_argument("--gpu", default="0") + parser.add_argument("--profile", action="store_true") + args = parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + write_metadata(args) + rows: list[dict[str, object]] = [] + env = os.environ | { + "CUDA_VISIBLE_DEVICES": args.gpu, + "LIBCUDF_JIT_DISABLE_CUDA_CACHE": "1", + } + row_counts = [int(value) for value in args.rows.split(",")] + validate_outputs(args, env) + + for workload in ("medium", "high"): + for variant in ("precompiled", "jit", "lto"): + for row_count in row_counts: + command = [ + str(args.executable), + str(args.input), + "-", + variant, + workload, + str(row_count), + str(args.iterations), + ] + for repeat in range(args.repeats): + run_env = env | { + "LIBCUDF_KERNEL_CACHE_PATH": str( + args.output_dir + / "cache" + / "benchmark" + / workload + / variant + / str(row_count) + / str(repeat) + ) + } + completed = subprocess.run( + command, env=run_env, check=True, text=True, capture_output=True + ) + result: dict[str, object] = parse_result(completed.stdout) + result["repeat"] = repeat + rows.append(result) + + if args.profile and row_count == max(row_counts): + profile( + command[:-1] + ["1"], + args.output_dir / f"ncu_{workload}_{variant}.csv", + args.gpu, + ) + + fieldnames = list(rows[0]) + with (args.output_dir / "benchmark_results.csv").open("w", newline="", encoding="utf-8") as output: + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + write_markdown(rows, args.output_dir / "benchmark_results.md") + if args.profile: + summarize_profiles(args.output_dir) + + +if __name__ == "__main__": + main() diff --git a/cpp/examples/string_transforms/tools/plot_results.py b/cpp/examples/string_transforms/tools/plot_results.py new file mode 100644 index 000000000000..b32195f05248 --- /dev/null +++ b/cpp/examples/string_transforms/tools/plot_results.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Create NVIDIA-colour presentation graphs from benchmark and Nsight Compute CSV files.""" + +from __future__ import annotations + +import argparse +import csv +from collections import defaultdict +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + + +NVIDIA_GREEN = "#76B900" +NVIDIA_BLACK = "#000000" +NVIDIA_DARK_GRAY = "#5B5B5B" +COLORS = {"precompiled": NVIDIA_BLACK, "jit": NVIDIA_GREEN, "lto": NVIDIA_DARK_GRAY} + + +def load_results(path: Path) -> dict[tuple[str, str, int], dict[str, float]]: + values: dict[tuple[str, str, int], dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) + with path.open(newline="", encoding="utf-8") as source: + for row in csv.DictReader(source): + key = (row["workload"], row["variant"], int(row["rows"])) + for metric in ( + "cold_seconds", + "warm_seconds", + "rows_per_second", + "effective_gib_per_second", + "peak_memory_bytes", + "total_allocated_bytes", + "allocated_bytes_per_call", + ): + values[key][metric].append(float(row[metric])) + return {key: {metric: sum(samples) / len(samples) for metric, samples in metrics.items()} for key, metrics in values.items()} + + +def grouped_bars(data, workload: str, metric: str, scale: float, ylabel: str, output: Path) -> None: + row_counts = sorted({key[2] for key in data if key[0] == workload}) + variants = ["precompiled", "jit", "lto"] + x = np.arange(len(row_counts)) + width = 0.24 + fig, ax = plt.subplots(figsize=(10, 5.625), layout="constrained") + for index, variant in enumerate(variants): + heights = [data[(workload, variant, rows)][metric] * scale for rows in row_counts] + ax.bar(x + (index - 1) * width, heights, width, label=variant, color=COLORS[variant]) + ax.set_xticks(x, [f"{rows / 1_000_000:g}M" for rows in row_counts]) + ax.set_xlabel("Rows") + ax.set_ylabel(ylabel) + ax.set_title(f"{workload.title()} HTTP log extraction") + ax.legend(frameon=False) + ax.grid(axis="y", alpha=0.2) + fig.savefig(output, dpi=180, transparent=False) + plt.close(fig) + + +def profile_bars(path: Path, output_dir: Path) -> None: + with path.open(newline="", encoding="utf-8") as source: + rows = list(csv.DictReader(source)) + for row in rows: + row["dram_total_gib"] = ( + float(row["dram_bytes_read"]) + float(row["dram_bytes_written"]) + ) / (1 << 30) + variants = ["precompiled", "jit", "lto"] + for workload in ("medium", "high"): + selected = {row["variant"]: row for row in rows if row["workload"] == workload} + metrics = [ + ("warp_occupancy_percent", "Warp occupancy (%)", "warp_occupancy"), + ("dram_throughput_percent", "DRAM peak throughput (%)", "dram_throughput"), + ("dram_total_gib", "DRAM traffic (GiB)", "dram_traffic"), + ("kernel_time_seconds", "Profiled kernel time (seconds)", "kernel_time"), + ] + for metric, ylabel, filename in metrics: + fig, ax = plt.subplots(figsize=(10, 5.625), layout="constrained") + ax.bar(variants, [float(selected[item][metric]) for item in variants], color=[COLORS[item] for item in variants]) + ax.set_ylabel(ylabel) + ax.set_title(f"{workload.title()} HTTP log extraction") + ax.grid(axis="y", alpha=0.2) + fig.savefig(output_dir / f"{workload}_{filename}.png", dpi=180, transparent=False) + plt.close(fig) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("results", type=Path) + parser.add_argument("--output-dir", type=Path) + args = parser.parse_args() + output_dir = args.output_dir or args.results.parent / "graphs" + output_dir.mkdir(parents=True, exist_ok=True) + data = load_results(args.results) + + for workload in ("medium", "high"): + grouped_bars(data, workload, "cold_seconds", 1.0, "Cold time (seconds)", output_dir / f"{workload}_cold_time.png") + grouped_bars(data, workload, "warm_seconds", 1_000.0, "Warm time (milliseconds)", output_dir / f"{workload}_warm_time.png") + grouped_bars(data, workload, "rows_per_second", 1 / 1_000_000, "Throughput (million rows/s)", output_dir / f"{workload}_throughput.png") + grouped_bars(data, workload, "effective_gib_per_second", 1.0, "Effective bandwidth (GiB/s)", output_dir / f"{workload}_bandwidth.png") + grouped_bars(data, workload, "peak_memory_bytes", 1 / (1 << 20), "Peak temporary memory (MiB)", output_dir / f"{workload}_peak_memory.png") + grouped_bars(data, workload, "allocated_bytes_per_call", 1 / (1 << 20), "Allocation traffic per call (MiB)", output_dir / f"{workload}_allocation_cost.png") + profile_path = args.results.parent / "profile_results.csv" + if profile_path.exists(): + profile_bars(profile_path, output_dir) + + +if __name__ == "__main__": + main() From 2e2a13526f884ee06be66ebbd1c4367100f711e1 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Sun, 5 Jul 2026 15:08:07 +0100 Subject: [PATCH 02/27] Refine HTTP transform example interface --- cpp/examples/string_transforms/CMakeLists.txt | 22 +- cpp/examples/string_transforms/README.md | 40 +-- ..._output.cu => http_combined_log_output.cu} | 2 +- ...gh_sizes.cu => http_combined_log_sizes.cu} | 2 +- ..._output.cu => http_request_line_output.cu} | 2 +- ...um_sizes.cu => http_request_line_sizes.cu} | 2 +- .../string_transforms/http_log_transforms.cpp | 96 +++--- .../string_transforms/http_log_udf.cuh | 8 +- .../string_transforms/results/README.md | 16 - .../string_transforms/tools/benchmark.py | 296 ------------------ .../string_transforms/tools/plot_results.py | 109 ------- 11 files changed, 81 insertions(+), 514 deletions(-) rename cpp/examples/string_transforms/fragments/{http_high_output.cu => http_combined_log_output.cu} (94%) rename cpp/examples/string_transforms/fragments/{http_high_sizes.cu => http_combined_log_sizes.cu} (93%) rename cpp/examples/string_transforms/fragments/{http_medium_output.cu => http_request_line_output.cu} (90%) rename cpp/examples/string_transforms/fragments/{http_medium_sizes.cu => http_request_line_sizes.cu} (89%) delete mode 100644 cpp/examples/string_transforms/results/README.md delete mode 100644 cpp/examples/string_transforms/tools/benchmark.py delete mode 100644 cpp/examples/string_transforms/tools/plot_results.py diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index dd7ce13b7c41..30d8338083a9 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -55,19 +55,21 @@ add_string_transforms_example(localize_phone_jit localize_phone_jit.cpp) add_string_transforms_example(localize_phone_precompiled localize_phone_precompiled.cpp) rtcx_add_embed(http_log_fragments) -add_fragment(http_log_fragments FRAGMENT http_medium_sizes SOURCE - fragments/http_medium_sizes.cu) -add_fragment(http_log_fragments FRAGMENT http_medium_output SOURCE - fragments/http_medium_output.cu) -add_fragment(http_log_fragments FRAGMENT http_high_sizes SOURCE - fragments/http_high_sizes.cu) -add_fragment(http_log_fragments FRAGMENT http_high_output SOURCE - fragments/http_high_output.cu) +add_fragment(http_log_fragments FRAGMENT http_request_line_sizes SOURCE + fragments/http_request_line_sizes.cu) +add_fragment(http_log_fragments FRAGMENT http_request_line_output SOURCE + fragments/http_request_line_output.cu) +add_fragment(http_log_fragments FRAGMENT http_combined_log_sizes SOURCE + fragments/http_combined_log_sizes.cu) +add_fragment(http_log_fragments FRAGMENT http_combined_log_output SOURCE + fragments/http_combined_log_output.cu) foreach( fragment_target - http_log_fragments_http_medium_sizes http_log_fragments_http_medium_output - http_log_fragments_http_high_sizes http_log_fragments_http_high_output) + http_log_fragments_http_request_line_sizes + http_log_fragments_http_request_line_output + http_log_fragments_http_combined_log_sizes + http_log_fragments_http_combined_log_output) target_include_directories(${fragment_target} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) endforeach() diff --git a/cpp/examples/string_transforms/README.md b/cpp/examples/string_transforms/README.md index fae4f3e1cdda..b7e55f9a3963 100644 --- a/cpp/examples/string_transforms/README.md +++ b/cpp/examples/string_transforms/README.md @@ -23,9 +23,9 @@ The following examples are included: - `lto`: the same sizing and output transform ABI, AOT-compiled to embedded fatbins and JIT-linked with libcudf's precompiled transform kernels. -The HTTP example has a medium request-line workload (method, path, and HTTP version) and a -high-complexity combined-log workload (client IP, timestamp, method, path, status, referer, and user -agent). Both implement the same extraction groups as their comparative regex variant. +The `request-line` operation extracts method, path, and HTTP version. The `combined-log` operation +extracts client IP, timestamp, method, path, status, referer, and user agent. Both implement the same +extraction groups as their comparative regex variant. ## Compile and execute @@ -38,40 +38,6 @@ cmake --build build/ --parallel $PARALLEL_LEVEL build/output info.csv output.csv 100000 ``` -Run the HTTP example directly: - -```bash -build/http_log_transforms http_logs.csv output.csv jit medium 1000000 10 -build/http_log_transforms http_logs.csv output.csv lto high 1000000 10 -build/http_log_transforms http_logs.csv output.csv precompiled high 1000000 10 -``` - -Use `-` for the output path to skip CSV materialization during benchmark-only runs. - -## Benchmarking and profiling - -The benchmark runner records cold and warm wall time, throughput, effective input/output bandwidth, -and RMM allocation cost for all three variants. With `--profile`, Nsight Compute also records kernel -time, achieved warp occupancy, DRAM utilization, and bytes read/written. Use an otherwise idle GPU: - -```bash -python tools/benchmark.py \ - --executable build/http_log_transforms \ - --input http_logs.csv \ - --output-dir results \ - --rows 100000,1000000,10000000 \ - --iterations 10 --repeats 5 --gpu 0 --profile - -python tools/plot_results.py results/benchmark_results.csv -``` - -This creates CSV and Markdown tables plus 16:9 PNG presentation graphs using NVIDIA green, black, -and dark gray. Results should always be reported with the GPU, driver, CUDA toolkit, branch SHA, row -counts, repetitions, and exact command; the repository does not ship fabricated baseline numbers. -The runner gives every repetition a fresh `LIBCUDF_KERNEL_CACHE_PATH`, so the reported cold time -includes CUDA-source compilation or LTO linking while warm iterations reuse the in-process cache; -it also sets `LIBCUDF_JIT_DISABLE_CUDA_CACHE=1` to prevent the CUDA disk cache from hiding cold cost. - If your machine does not come with a pre-built libcudf binary, expect the first build to take some time, as it would build libcudf on the host machine. It may be sped up by configuring the proper `PARALLEL_LEVEL` number. diff --git a/cpp/examples/string_transforms/fragments/http_high_output.cu b/cpp/examples/string_transforms/fragments/http_combined_log_output.cu similarity index 94% rename from cpp/examples/string_transforms/fragments/http_high_output.cu rename to cpp/examples/string_transforms/fragments/http_combined_log_output.cu index 48fa6310b065..084ad6b89bc7 100644 --- a/cpp/examples/string_transforms/fragments/http_high_output.cu +++ b/cpp/examples/string_transforms/fragments/http_combined_log_output.cu @@ -14,7 +14,7 @@ extern "C" __device__ int transform(cuda::std::span* client_ip, cuda::std::span* user_agent, cudf::string_view input) { - auto const fields = http_log_udf::parse_high(input); + auto const fields = http_log_udf::parse_combined_log(input); http_log_udf::copy_range(*client_ip, input, fields.client_ip); http_log_udf::copy_range(*timestamp, input, fields.timestamp); http_log_udf::copy_range(*method, input, fields.method); diff --git a/cpp/examples/string_transforms/fragments/http_high_sizes.cu b/cpp/examples/string_transforms/fragments/http_combined_log_sizes.cu similarity index 93% rename from cpp/examples/string_transforms/fragments/http_high_sizes.cu rename to cpp/examples/string_transforms/fragments/http_combined_log_sizes.cu index 50eae81df6bf..fbc5c1b92712 100644 --- a/cpp/examples/string_transforms/fragments/http_high_sizes.cu +++ b/cpp/examples/string_transforms/fragments/http_combined_log_sizes.cu @@ -14,7 +14,7 @@ extern "C" __device__ int transform(int32_t* client_ip_size, int32_t* user_agent_size, cudf::string_view input) { - auto const fields = http_log_udf::parse_high(input); + auto const fields = http_log_udf::parse_combined_log(input); *client_ip_size = fields.client_ip.size(); *timestamp_size = fields.timestamp.size(); *method_size = fields.method.size(); diff --git a/cpp/examples/string_transforms/fragments/http_medium_output.cu b/cpp/examples/string_transforms/fragments/http_request_line_output.cu similarity index 90% rename from cpp/examples/string_transforms/fragments/http_medium_output.cu rename to cpp/examples/string_transforms/fragments/http_request_line_output.cu index 355ddcc6ed7a..273485feff54 100644 --- a/cpp/examples/string_transforms/fragments/http_medium_output.cu +++ b/cpp/examples/string_transforms/fragments/http_request_line_output.cu @@ -10,7 +10,7 @@ extern "C" __device__ int transform(cuda::std::span* method, cuda::std::span* version, cudf::string_view input) { - auto const fields = http_log_udf::parse_medium(input); + auto const fields = http_log_udf::parse_request_line(input); http_log_udf::copy_range(*method, input, fields.method); http_log_udf::copy_range(*path, input, fields.path); http_log_udf::copy_range(*version, input, fields.version); diff --git a/cpp/examples/string_transforms/fragments/http_medium_sizes.cu b/cpp/examples/string_transforms/fragments/http_request_line_sizes.cu similarity index 89% rename from cpp/examples/string_transforms/fragments/http_medium_sizes.cu rename to cpp/examples/string_transforms/fragments/http_request_line_sizes.cu index cd901da6f952..43272f7813e8 100644 --- a/cpp/examples/string_transforms/fragments/http_medium_sizes.cu +++ b/cpp/examples/string_transforms/fragments/http_request_line_sizes.cu @@ -10,7 +10,7 @@ extern "C" __device__ int transform(int32_t* method_size, int32_t* version_size, cudf::string_view input) { - auto const fields = http_log_udf::parse_medium(input); + auto const fields = http_log_udf::parse_request_line(input); *method_size = fields.method.size(); *path_size = fields.path.size(); *version_size = fields.version.size(); diff --git a/cpp/examples/string_transforms/http_log_transforms.cpp b/cpp/examples/string_transforms/http_log_transforms.cpp index 8ca26751bc01..822630cc2a9c 100644 --- a/cpp/examples/string_transforms/http_log_transforms.cpp +++ b/cpp/examples/string_transforms/http_log_transforms.cpp @@ -34,18 +34,18 @@ namespace { enum class variant { PRECOMPILED, JIT, LTO }; -enum class workload { MEDIUM, HIGH }; +enum class operation { REQUEST_LINE, COMBINED_LOG }; struct options { std::string input_path; std::string output_path; variant implementation; - workload complexity; + operation selected_operation; cudf::size_type rows; int iterations; }; -constexpr char medium_sizes_udf[] = R"***( +constexpr char request_line_sizes_udf[] = R"***( __device__ void size_http_request(int32_t* method_size, int32_t* path_size, int32_t* version_size, cudf::string_view input) { auto find_char = [&](char needle, int32_t begin) { @@ -62,7 +62,7 @@ __device__ void size_http_request(int32_t* method_size, int32_t* path_size, } )***"; -constexpr char medium_output_udf[] = R"***( +constexpr char request_line_output_udf[] = R"***( __device__ void extract_http_request(cuda::std::span* method, cuda::std::span* path, cuda::std::span* version, cudf::string_view input) { @@ -83,7 +83,7 @@ __device__ void extract_http_request(cuda::std::span* method, cuda::std::s } )***"; -constexpr char high_sizes_udf[] = R"***( +constexpr char combined_log_sizes_udf[] = R"***( __device__ void size_combined_log(int32_t* ip, int32_t* timestamp, int32_t* method, int32_t* path, int32_t* status, int32_t* referer, int32_t* user_agent, cudf::string_view input) { @@ -114,7 +114,7 @@ __device__ void size_combined_log(int32_t* ip, int32_t* timestamp, int32_t* meth } )***"; -constexpr char high_output_udf[] = R"***( +constexpr char combined_log_output_udf[] = R"***( __device__ void extract_combined_log(cuda::std::span* ip, cuda::std::span* timestamp, cuda::std::span* method, @@ -166,19 +166,22 @@ __device__ void extract_combined_log(cuda::std::span* ip, throw std::logic_error("Unknown variant"); } -[[nodiscard]] std::string const& to_string(workload value) +[[nodiscard]] std::string const& to_string(operation value) { - static std::string const medium{"medium"}; - static std::string const high{"high"}; - return value == workload::MEDIUM ? medium : high; + static std::string const request_line{"request-line"}; + static std::string const combined_log{"combined-log"}; + return value == operation::REQUEST_LINE ? request_line : combined_log; } +constexpr std::string_view usage = + "usage: http_log_transforms INPUT.csv OUTPUT.csv " + " ROWS ITERATIONS\n" + " http_log_transforms \n"; + [[nodiscard]] options parse_options(int argc, char const** argv) { if (argc != 7) { - throw std::invalid_argument( - "usage: http_log_transforms INPUT.csv OUTPUT.csv " - " ROWS ITERATIONS"); + throw std::invalid_argument("invalid arguments; run http_log_transforms --help for usage"); } auto const implementation = std::string_view{argv[3]} == "precompiled" ? variant::PRECOMPILED @@ -189,9 +192,10 @@ __device__ void extract_combined_log(cuda::std::span* ip, throw std::invalid_argument("variant must be precompiled, jit, or lto"); } - auto const complexity = std::string_view{argv[4]} == "medium" ? workload::MEDIUM : workload::HIGH; - if (std::string_view{argv[4]} != "medium" && std::string_view{argv[4]} != "high") { - throw std::invalid_argument("workload must be medium or high"); + auto const selected_operation = + std::string_view{argv[4]} == "request-line" ? operation::REQUEST_LINE : operation::COMBINED_LOG; + if (std::string_view{argv[4]} != "request-line" && std::string_view{argv[4]} != "combined-log") { + throw std::invalid_argument("operation must be request-line or combined-log"); } auto const rows = std::stoll(argv[5]); @@ -200,8 +204,12 @@ __device__ void extract_combined_log(cuda::std::span* ip, throw std::invalid_argument("ROWS is outside the cudf::size_type range"); } if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } - return { - argv[1], argv[2], implementation, complexity, static_cast(rows), iterations}; + return {argv[1], + argv[2], + implementation, + selected_operation, + static_cast(rows), + iterations}; } [[nodiscard]] std::unique_ptr make_offsets(cudf::column_view const sizes, @@ -234,11 +242,11 @@ __device__ void extract_combined_log(cuda::std::span* ip, } [[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, - workload complexity, + operation selected_operation, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - if (complexity == workload::MEDIUM) { + if (selected_operation == operation::REQUEST_LINE) { static auto const program = cudf::strings::regex_program::create(R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"); return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); @@ -249,7 +257,7 @@ __device__ void extract_combined_log(cuda::std::span* ip, } [[nodiscard]] std::unique_ptr run_two_pass(cudf::column_view input, - workload complexity, + operation selected_operation, variant implementation, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) @@ -257,13 +265,15 @@ __device__ void extract_combined_log(cuda::std::span* ip, // Pass 1 emits the exact byte count for every output string and row. Scanning each size column // produces run-end offsets, so multi_transform can allocate each chars child once. Pass 2 then // receives a cuda::std::span for every row/output and writes directly into final storage. - auto const count = complexity == workload::MEDIUM ? std::size_t{3} : std::size_t{7}; + auto const count = + selected_operation == operation::REQUEST_LINE ? std::size_t{3} : std::size_t{7}; auto sizes_out = output_specs(count, cudf::type_id::INT32); cudf::transform_input inputs[] = {input}; std::unique_ptr sizes; if (implementation == variant::JIT) { - auto const source = complexity == workload::MEDIUM ? medium_sizes_udf : high_sizes_udf; + auto const source = selected_operation == operation::REQUEST_LINE ? request_line_sizes_udf + : combined_log_sizes_udf; sizes = cudf::multi_transform(source, cudf::udf_source_type::CUDA, cudf::null_aware::NO, @@ -275,8 +285,9 @@ __device__ void extract_combined_log(cuda::std::span* ip, stream, mr); } else { - auto const id = complexity == workload::MEDIUM ? http_log_fragments::http_medium_sizes - : http_log_fragments::http_high_sizes; + auto const id = selected_operation == operation::REQUEST_LINE + ? http_log_fragments::http_request_line_sizes + : http_log_fragments::http_combined_log_sizes; sizes = cudf::transform_lto(fragment(id), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, @@ -297,7 +308,8 @@ __device__ void extract_combined_log(cuda::std::span* ip, auto strings_out = output_specs(count, cudf::type_id::STRING); if (implementation == variant::JIT) { - auto const source = complexity == workload::MEDIUM ? medium_output_udf : high_output_udf; + auto const source = selected_operation == operation::REQUEST_LINE ? request_line_output_udf + : combined_log_output_udf; return cudf::multi_transform(source, cudf::udf_source_type::CUDA, cudf::null_aware::NO, @@ -310,8 +322,9 @@ __device__ void extract_combined_log(cuda::std::span* ip, mr); } - auto const id = complexity == workload::MEDIUM ? http_log_fragments::http_medium_output - : http_log_fragments::http_high_output; + auto const id = selected_operation == operation::REQUEST_LINE + ? http_log_fragments::http_request_line_output + : http_log_fragments::http_combined_log_output; return cudf::transform_lto(fragment(id), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, @@ -325,21 +338,21 @@ __device__ void extract_combined_log(cuda::std::span* ip, } [[nodiscard]] std::unique_ptr run(cudf::column_view input, - workload complexity, + operation selected_operation, variant implementation, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { return implementation == variant::PRECOMPILED - ? run_precompiled(input, complexity, stream, mr) - : run_two_pass(input, complexity, implementation, stream, mr); + ? run_precompiled(input, selected_operation, stream, mr) + : run_two_pass(input, selected_operation, implementation, stream, mr); } void write_output(cudf::table_view const result, - workload complexity, + operation selected_operation, std::string const& output_path) { - auto names = complexity == workload::MEDIUM + auto names = selected_operation == operation::REQUEST_LINE ? std::vector{"method", "path", "http_version"} : std::vector{ "client_ip", "timestamp", "method", "path", "status", "referer", "user_agent"}; @@ -355,6 +368,12 @@ void write_output(cudf::table_view const result, int main(int argc, char const** argv) { try { + if (argc == 2 && + (std::string_view{argv[1]} == "--help" || std::string_view{argv[1]} == "usage")) { + std::cout << usage; + return EXIT_SUCCESS; + } + auto const opts = parse_options(argc, argv); auto const stream = cudf::get_default_stream(); auto const mr = cudf::get_current_device_resource_ref(); @@ -368,7 +387,7 @@ int main(int argc, char const** argv) opts.rows == input_data.tbl->num_rows() ? std::move(input_data.tbl) : cudf::sample(input_data.tbl->view(), opts.rows, cudf::sample_with_replacement::TRUE); - auto const input_index = opts.complexity == workload::MEDIUM ? 0 : 1; + auto const input_index = opts.selected_operation == operation::REQUEST_LINE ? 0 : 1; auto const input_bytes = input->get_column(input_index).alloc_size(); auto const input_column = input->get_column(input_index).view(); @@ -378,7 +397,8 @@ int main(int argc, char const** argv) stream.synchronize(); auto const cold_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_cold"); - auto cold_result = run(input_column, opts.complexity, opts.implementation, stream, stats_mr); + auto cold_result = + run(input_column, opts.selected_operation, opts.implementation, stream, stats_mr); stream.synchronize(); nvtxRangePop(); auto const cold_seconds = @@ -390,7 +410,7 @@ int main(int argc, char const** argv) nvtxRangePush("http_log_warm"); for (auto i = 0; i < opts.iterations; ++i) { result.reset(); - result = run(input_column, opts.complexity, opts.implementation, stream, stats_mr); + result = run(input_column, opts.selected_operation, opts.implementation, stream, stats_mr); } stream.synchronize(); nvtxRangePop(); @@ -399,7 +419,7 @@ int main(int argc, char const** argv) opts.iterations; if (opts.output_path != "-") { - write_output(result->view(), opts.complexity, opts.output_path); + write_output(result->view(), opts.selected_operation, opts.output_path); } auto const bytes = stats.get_bytes_counter(); @@ -408,7 +428,7 @@ int main(int argc, char const** argv) std::cout << std::fixed << std::setprecision(9) << "RESULT variant=" << to_string(opts.implementation) - << " workload=" << to_string(opts.complexity) << " rows=" << opts.rows + << " operation=" << to_string(opts.selected_operation) << " rows=" << opts.rows << " cold_seconds=" << cold_seconds << " warm_seconds=" << warm_seconds << " rows_per_second=" << static_cast(opts.rows) / warm_seconds << " effective_gib_per_second=" << gib / warm_seconds diff --git a/cpp/examples/string_transforms/http_log_udf.cuh b/cpp/examples/string_transforms/http_log_udf.cuh index b72dec574eb2..d49447b7453a 100644 --- a/cpp/examples/string_transforms/http_log_udf.cuh +++ b/cpp/examples/string_transforms/http_log_udf.cuh @@ -19,13 +19,13 @@ struct range { [[nodiscard]] __device__ int32_t size() const { return end - begin; } }; -struct medium_fields { +struct request_line_fields { range method; range path; range version; }; -struct high_fields { +struct combined_log_fields { range client_ip; range timestamp; range method; @@ -45,7 +45,7 @@ struct high_fields { return input.size_bytes(); } -[[nodiscard]] __device__ medium_fields parse_medium(cudf::string_view const input) +[[nodiscard]] __device__ request_line_fields parse_request_line(cudf::string_view const input) { auto const method_end = find(input, ' ', 0); auto const target_end = find(input, ' ', method_end + 1); @@ -58,7 +58,7 @@ struct high_fields { {target_end + http_prefix_size, input.size_bytes()}}; } -[[nodiscard]] __device__ high_fields parse_high(cudf::string_view const input) +[[nodiscard]] __device__ combined_log_fields parse_combined_log(cudf::string_view const input) { auto const ip_end = find(input, ' ', 0); auto const timestamp_begin = find(input, '[', ip_end) + 1; diff --git a/cpp/examples/string_transforms/results/README.md b/cpp/examples/string_transforms/results/README.md deleted file mode 100644 index 3c9da97315cf..000000000000 --- a/cpp/examples/string_transforms/results/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Benchmark result artifacts - -This directory is intentionally source-only. Run `tools/benchmark.py` on a GPU system to generate: - -- `benchmark_results.csv`: raw repetitions for all variants, workloads, and row counts. -- `benchmark_results.md`: presentation-ready aggregate table. -- `ncu__.csv`: per-kernel Nsight Compute data when `--profile` is enabled. -- `profile_results.{csv,md}`: aggregate kernel time, warp occupancy, DRAM utilization, and traffic. -- `environment.md`: branch SHA, GPU/driver, CUDA toolkit, row counts, repetitions, and profile mode. - -Then run `tools/plot_results.py results/benchmark_results.csv` to create NVIDIA-colour PNG graphs -under `results/graphs/`. - -Measured results are not checked in unless they identify the GPU, driver, CUDA toolkit, branch SHA, -row counts, repetitions, and exact profiling command. This avoids presenting synthetic or stale -numbers as measured performance. diff --git a/cpp/examples/string_transforms/tools/benchmark.py b/cpp/examples/string_transforms/tools/benchmark.py deleted file mode 100644 index 511886694f10..000000000000 --- a/cpp/examples/string_transforms/tools/benchmark.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. -# SPDX-License-Identifier: Apache-2.0 - -"""Run the HTTP log transform comparison and write CSV/Markdown result tables.""" - -from __future__ import annotations - -import argparse -import csv -import os -import re -import statistics -import subprocess -from collections import defaultdict -from pathlib import Path - - -RESULT_RE = re.compile(r"^RESULT (?P.+)$", re.MULTILINE) - - -def parse_result(stdout: str) -> dict[str, str]: - match = RESULT_RE.search(stdout) - if match is None: - raise RuntimeError(f"benchmark did not emit a RESULT line:\n{stdout}") - return dict(item.split("=", 1) for item in match.group("values").split()) - - -def command_output(command: list[str]) -> str: - try: - completed = subprocess.run(command, check=False, text=True, capture_output=True) - except OSError as error: - return f"unavailable: {error}" - return (completed.stdout or completed.stderr).strip() - - -def write_metadata(args: argparse.Namespace) -> None: - cuda_output = command_output(["nvcc", "--version"]) - cuda_line = cuda_output.splitlines()[-1] if cuda_output else "unavailable" - metadata = [ - "# Benchmark environment", - "", - f"- Git SHA: `{command_output(['git', 'rev-parse', 'HEAD'])}`", - f"- GPU: `{command_output(['nvidia-smi', '--query-gpu=name,driver_version,memory.total', '--format=csv,noheader', '-i', args.gpu])}`", - f"- CUDA toolkit: `{cuda_line}`", - f"- Executable: `{args.executable}`", - f"- Rows: `{args.rows}`", - f"- Iterations: `{args.iterations}`", - f"- Repeats: `{args.repeats}`", - f"- GPU mask: `{args.gpu}`", - f"- Nsight Compute profiling: `{args.profile}`", - "", - ] - (args.output_dir / "environment.md").write_text("\n".join(metadata), encoding="utf-8") - - -def validate_outputs(args: argparse.Namespace, env: dict[str, str]) -> None: - for workload in ("medium", "high"): - outputs: dict[str, list[list[str]]] = {} - for variant in ("precompiled", "jit", "lto"): - path = args.output_dir / f"validation_{workload}_{variant}.csv" - subprocess.run( - [ - str(args.executable), - str(args.input), - str(path), - variant, - workload, - "12", - "1", - ], - env=env - | { - "LIBCUDF_KERNEL_CACHE_PATH": str( - args.output_dir / "cache" / "validation" / workload / variant - ) - }, - check=True, - text=True, - capture_output=True, - ) - with path.open(newline="", encoding="utf-8") as source: - outputs[variant] = list(csv.reader(source)) - expected = outputs["precompiled"] - for variant in ("jit", "lto"): - if outputs[variant] != expected: - raise RuntimeError(f"{workload} output differs for {variant} and precompiled") - - -def write_markdown(rows: list[dict[str, object]], path: Path) -> None: - grouped: dict[tuple[str, str, int], list[dict[str, object]]] = defaultdict(list) - for row in rows: - grouped[(str(row["workload"]), str(row["variant"]), int(row["rows"]))].append(row) - - headers = [ - "Workload", - "Variant", - "Rows", - "Cold (s)", - "Warm (ms)", - "Throughput (M rows/s)", - "Effective BW (GiB/s)", - "Peak memory (MiB)", - "Allocation cost/call (MiB)", - ] - lines = ["# HTTP log transform benchmark", "", " | ".join(headers), " | ".join(["---"] * len(headers))] - for (workload, variant, row_count), samples in sorted(grouped.items()): - mean = lambda key: statistics.mean(float(sample[key]) for sample in samples) - values = [ - workload, - variant, - f"{row_count:,}", - f"{mean('cold_seconds'):.6f}", - f"{mean('warm_seconds') * 1_000:.3f}", - f"{mean('rows_per_second') / 1_000_000:.3f}", - f"{mean('effective_gib_per_second'):.3f}", - f"{mean('peak_memory_bytes') / (1 << 20):.2f}", - f"{mean('allocated_bytes_per_call') / (1 << 20):.2f}", - ] - lines.append(" | ".join(values)) - path.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def profile(command: list[str], output: Path, gpu: str) -> None: - metrics = ",".join( - [ - "gpu__time_duration.sum", - "sm__warps_active.avg.pct_of_peak_sustained_active", - "dram__throughput.avg.pct_of_peak_sustained_elapsed", - "dram__bytes_read.sum", - "dram__bytes_write.sum", - ] - ) - env = os.environ | { - "CUDA_VISIBLE_DEVICES": gpu, - "LIBCUDF_JIT_DISABLE_CUDA_CACHE": "1", - "LIBCUDF_KERNEL_CACHE_PATH": str(output.parent / "cache" / "profile" / output.stem), - } - subprocess.run( - [ - "ncu", - "--csv", - "--nvtx", - "--nvtx-include", - "http_log_warm/", - "--metrics", - metrics, - "--log-file", - str(output), - *command, - ], - env=env, - check=True, - text=True, - ) - - -def metric_value(value: str, unit: str) -> float: - number = float(value.replace(",", "")) - prefixes = {"K": 1e3, "M": 1e6, "G": 1e9} - if unit and unit[0] in prefixes: - number *= prefixes[unit[0]] - if unit == "nsecond": - number *= 1e-9 - elif unit == "usecond": - number *= 1e-6 - elif unit == "msecond": - number *= 1e-3 - return number - - -def summarize_profiles(output_dir: Path) -> None: - summaries: list[dict[str, object]] = [] - for path in sorted(output_dir.glob("ncu_*.csv")): - _, workload, variant = path.stem.split("_", 2) - rows = [line for line in path.read_text(encoding="utf-8").splitlines() if not line.startswith("==")] - reader = csv.DictReader(rows) - metrics: dict[str, list[float]] = defaultdict(list) - for row in reader: - name = row.get("Metric Name") - value = row.get("Metric Value") - if not name or value in (None, "n/a"): - continue - metrics[name].append(metric_value(value, row.get("Metric Unit", ""))) - summaries.append( - { - "workload": workload, - "variant": variant, - "kernel_time_seconds": sum(metrics["gpu__time_duration.sum"]), - "warp_occupancy_percent": statistics.mean( - metrics["sm__warps_active.avg.pct_of_peak_sustained_active"] - ), - "dram_throughput_percent": statistics.mean( - metrics["dram__throughput.avg.pct_of_peak_sustained_elapsed"] - ), - "dram_bytes_read": sum(metrics["dram__bytes_read.sum"]), - "dram_bytes_written": sum(metrics["dram__bytes_write.sum"]), - } - ) - if not summaries: - return - with (output_dir / "profile_results.csv").open("w", newline="", encoding="utf-8") as output: - writer = csv.DictWriter(output, fieldnames=list(summaries[0])) - writer.writeheader() - writer.writerows(summaries) - headers = ["Workload", "Variant", "Kernel time (s)", "Warp occupancy (%)", "DRAM peak (%)", "DRAM read (GiB)", "DRAM write (GiB)"] - lines = ["# Nsight Compute profile", "", " | ".join(headers), " | ".join(["---"] * len(headers))] - for row in summaries: - lines.append( - " | ".join( - [ - str(row["workload"]), - str(row["variant"]), - f"{row['kernel_time_seconds']:.6f}", - f"{row['warp_occupancy_percent']:.2f}", - f"{row['dram_throughput_percent']:.2f}", - f"{row['dram_bytes_read'] / (1 << 30):.3f}", - f"{row['dram_bytes_written'] / (1 << 30):.3f}", - ] - ) - ) - (output_dir / "profile_results.md").write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--executable", type=Path, required=True) - parser.add_argument("--input", type=Path, required=True) - parser.add_argument("--output-dir", type=Path, default=Path("results")) - parser.add_argument("--rows", default="100000,1000000,10000000") - parser.add_argument("--iterations", type=int, default=10) - parser.add_argument("--repeats", type=int, default=5) - parser.add_argument("--gpu", default="0") - parser.add_argument("--profile", action="store_true") - args = parser.parse_args() - - args.output_dir.mkdir(parents=True, exist_ok=True) - write_metadata(args) - rows: list[dict[str, object]] = [] - env = os.environ | { - "CUDA_VISIBLE_DEVICES": args.gpu, - "LIBCUDF_JIT_DISABLE_CUDA_CACHE": "1", - } - row_counts = [int(value) for value in args.rows.split(",")] - validate_outputs(args, env) - - for workload in ("medium", "high"): - for variant in ("precompiled", "jit", "lto"): - for row_count in row_counts: - command = [ - str(args.executable), - str(args.input), - "-", - variant, - workload, - str(row_count), - str(args.iterations), - ] - for repeat in range(args.repeats): - run_env = env | { - "LIBCUDF_KERNEL_CACHE_PATH": str( - args.output_dir - / "cache" - / "benchmark" - / workload - / variant - / str(row_count) - / str(repeat) - ) - } - completed = subprocess.run( - command, env=run_env, check=True, text=True, capture_output=True - ) - result: dict[str, object] = parse_result(completed.stdout) - result["repeat"] = repeat - rows.append(result) - - if args.profile and row_count == max(row_counts): - profile( - command[:-1] + ["1"], - args.output_dir / f"ncu_{workload}_{variant}.csv", - args.gpu, - ) - - fieldnames = list(rows[0]) - with (args.output_dir / "benchmark_results.csv").open("w", newline="", encoding="utf-8") as output: - writer = csv.DictWriter(output, fieldnames=fieldnames) - writer.writeheader() - writer.writerows(rows) - write_markdown(rows, args.output_dir / "benchmark_results.md") - if args.profile: - summarize_profiles(args.output_dir) - - -if __name__ == "__main__": - main() diff --git a/cpp/examples/string_transforms/tools/plot_results.py b/cpp/examples/string_transforms/tools/plot_results.py deleted file mode 100644 index b32195f05248..000000000000 --- a/cpp/examples/string_transforms/tools/plot_results.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. -# SPDX-License-Identifier: Apache-2.0 - -"""Create NVIDIA-colour presentation graphs from benchmark and Nsight Compute CSV files.""" - -from __future__ import annotations - -import argparse -import csv -from collections import defaultdict -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np - - -NVIDIA_GREEN = "#76B900" -NVIDIA_BLACK = "#000000" -NVIDIA_DARK_GRAY = "#5B5B5B" -COLORS = {"precompiled": NVIDIA_BLACK, "jit": NVIDIA_GREEN, "lto": NVIDIA_DARK_GRAY} - - -def load_results(path: Path) -> dict[tuple[str, str, int], dict[str, float]]: - values: dict[tuple[str, str, int], dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) - with path.open(newline="", encoding="utf-8") as source: - for row in csv.DictReader(source): - key = (row["workload"], row["variant"], int(row["rows"])) - for metric in ( - "cold_seconds", - "warm_seconds", - "rows_per_second", - "effective_gib_per_second", - "peak_memory_bytes", - "total_allocated_bytes", - "allocated_bytes_per_call", - ): - values[key][metric].append(float(row[metric])) - return {key: {metric: sum(samples) / len(samples) for metric, samples in metrics.items()} for key, metrics in values.items()} - - -def grouped_bars(data, workload: str, metric: str, scale: float, ylabel: str, output: Path) -> None: - row_counts = sorted({key[2] for key in data if key[0] == workload}) - variants = ["precompiled", "jit", "lto"] - x = np.arange(len(row_counts)) - width = 0.24 - fig, ax = plt.subplots(figsize=(10, 5.625), layout="constrained") - for index, variant in enumerate(variants): - heights = [data[(workload, variant, rows)][metric] * scale for rows in row_counts] - ax.bar(x + (index - 1) * width, heights, width, label=variant, color=COLORS[variant]) - ax.set_xticks(x, [f"{rows / 1_000_000:g}M" for rows in row_counts]) - ax.set_xlabel("Rows") - ax.set_ylabel(ylabel) - ax.set_title(f"{workload.title()} HTTP log extraction") - ax.legend(frameon=False) - ax.grid(axis="y", alpha=0.2) - fig.savefig(output, dpi=180, transparent=False) - plt.close(fig) - - -def profile_bars(path: Path, output_dir: Path) -> None: - with path.open(newline="", encoding="utf-8") as source: - rows = list(csv.DictReader(source)) - for row in rows: - row["dram_total_gib"] = ( - float(row["dram_bytes_read"]) + float(row["dram_bytes_written"]) - ) / (1 << 30) - variants = ["precompiled", "jit", "lto"] - for workload in ("medium", "high"): - selected = {row["variant"]: row for row in rows if row["workload"] == workload} - metrics = [ - ("warp_occupancy_percent", "Warp occupancy (%)", "warp_occupancy"), - ("dram_throughput_percent", "DRAM peak throughput (%)", "dram_throughput"), - ("dram_total_gib", "DRAM traffic (GiB)", "dram_traffic"), - ("kernel_time_seconds", "Profiled kernel time (seconds)", "kernel_time"), - ] - for metric, ylabel, filename in metrics: - fig, ax = plt.subplots(figsize=(10, 5.625), layout="constrained") - ax.bar(variants, [float(selected[item][metric]) for item in variants], color=[COLORS[item] for item in variants]) - ax.set_ylabel(ylabel) - ax.set_title(f"{workload.title()} HTTP log extraction") - ax.grid(axis="y", alpha=0.2) - fig.savefig(output_dir / f"{workload}_{filename}.png", dpi=180, transparent=False) - plt.close(fig) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("results", type=Path) - parser.add_argument("--output-dir", type=Path) - args = parser.parse_args() - output_dir = args.output_dir or args.results.parent / "graphs" - output_dir.mkdir(parents=True, exist_ok=True) - data = load_results(args.results) - - for workload in ("medium", "high"): - grouped_bars(data, workload, "cold_seconds", 1.0, "Cold time (seconds)", output_dir / f"{workload}_cold_time.png") - grouped_bars(data, workload, "warm_seconds", 1_000.0, "Warm time (milliseconds)", output_dir / f"{workload}_warm_time.png") - grouped_bars(data, workload, "rows_per_second", 1 / 1_000_000, "Throughput (million rows/s)", output_dir / f"{workload}_throughput.png") - grouped_bars(data, workload, "effective_gib_per_second", 1.0, "Effective bandwidth (GiB/s)", output_dir / f"{workload}_bandwidth.png") - grouped_bars(data, workload, "peak_memory_bytes", 1 / (1 << 20), "Peak temporary memory (MiB)", output_dir / f"{workload}_peak_memory.png") - grouped_bars(data, workload, "allocated_bytes_per_call", 1 / (1 << 20), "Allocation traffic per call (MiB)", output_dir / f"{workload}_allocation_cost.png") - profile_path = args.results.parent / "profile_results.csv" - if profile_path.exists(): - profile_bars(profile_path, output_dir) - - -if __name__ == "__main__": - main() From a45f9cab9145c53fb053c5eb301ca6cdb08be5b3 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Sun, 5 Jul 2026 19:41:41 +0100 Subject: [PATCH 03/27] Add HTTP transform performance comparison --- cpp/examples/string_transforms/CMakeLists.txt | 33 ++++++++++++++++--- .../http_log_performance.csv | 7 ++++ .../string_transforms/http_log_performance.md | 18 ++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 cpp/examples/string_transforms/http_log_performance.csv create mode 100644 cpp/examples/string_transforms/http_log_performance.md diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 30d8338083a9..f1a3aa7e139b 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -13,7 +13,7 @@ rapids_cuda_init_architectures(string_transforms_examples) project( string_transforms_examples VERSION 0.0.1 - LANGUAGES CXX CUDA + LANGUAGES CXX CUDA ASM ) include(../fetch_dependencies.cmake) @@ -22,6 +22,31 @@ include(rapids-cmake) rapids_cmake_build_type("Release") # Build and embed the AOT CUDA fragments consumed by cudf::transform_lto. +find_package(zstd CONFIG REQUIRED) +find_package(bs_thread_pool CONFIG REQUIRED) +find_package(ZLIB REQUIRED) +find_package(nvcomp CONFIG REQUIRED) +find_package(kvikio CONFIG REQUIRED) +if(NOT TARGET cuco::cuco) + add_library(cuco::cuco INTERFACE IMPORTED) +endif() +if(NOT TARGET nanoarrow::nanoarrow) + add_library(nanoarrow::nanoarrow INTERFACE IMPORTED) +endif() +if(NOT TARGET zstd) + add_library(zstd INTERFACE) + target_link_libraries(zstd INTERFACE zstd::libzstd) +endif() + +if(NOT TARGET xxhash) + find_library(XXHASH_LIBRARY NAMES xxhash REQUIRED) + find_path(XXHASH_INCLUDE_DIR NAMES xxhash.h REQUIRED) + add_library(xxhash UNKNOWN IMPORTED) + set_target_properties( + xxhash PROPERTIES IMPORTED_LOCATION "${XXHASH_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${XXHASH_INCLUDE_DIR}") +endif() + include(${CMAKE_CURRENT_LIST_DIR}/../../librtcx/embed.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/Modules/AddFragment.cmake) set(CUDF_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") @@ -77,9 +102,9 @@ endforeach() rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed") -add_string_transforms_example( - http_log_transforms - "http_log_transforms.cpp;${http_log_fragments_SOURCE_DIR}/http_log_fragments.s" +add_string_transforms_example(http_log_transforms http_log_transforms.cpp) +target_sources( + http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}/http_log_fragments.s ) target_include_directories(http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}) diff --git a/cpp/examples/string_transforms/http_log_performance.csv b/cpp/examples/string_transforms/http_log_performance.csv new file mode 100644 index 000000000000..c4c8a6860005 --- /dev/null +++ b/cpp/examples/string_transforms/http_log_performance.csv @@ -0,0 +1,7 @@ +operation,variant,cold_mean_s,cold_sd_s,warm_mean_ms,warm_sd_ms,throughput_mean_mrows_s,throughput_sd_mrows_s,effective_bandwidth_mean_gib_s,effective_bandwidth_sd_gib_s,peak_memory_mib,allocated_per_call_mib,warm_speedup_vs_precompiled +request-line,precompiled,0.066313,0.000504,68.416,0.167,14.617,0.035,0.975,0.002,32.00,32.23,1.00 +request-line,jit,1.398848,0.008472,2.574,0.017,388.528,2.554,25.910,0.170,43.32,54.77,26.58 +request-line,lto,1.417205,0.008892,2.618,0.041,381.982,5.925,25.473,0.395,43.32,54.77,26.13 +combined-log,precompiled,0.601166,0.011457,600.868,3.285,1.664,0.009,0.495,0.003,145.13,145.85,1.00 +combined-log,jit,1.554661,0.013899,25.394,0.209,39.382,0.323,11.711,0.096,171.72,198.42,23.66 +combined-log,lto,1.620733,0.055994,25.465,0.166,39.270,0.254,11.677,0.076,171.72,198.42,23.60 diff --git a/cpp/examples/string_transforms/http_log_performance.md b/cpp/examples/string_transforms/http_log_performance.md new file mode 100644 index 000000000000..af047b7f900f --- /dev/null +++ b/cpp/examples/string_transforms/http_log_performance.md @@ -0,0 +1,18 @@ +# HTTP log transform performance + +Measured on 2026-07-05 with GPU 1 of a dual NVIDIA RTX A6000 system. The GPU was idle before the run. The executable was built in Release mode with CUDA 13.2 and run against the current `multi-string-output` working tree at `2e2a13526f` plus the standalone-build fixes in that tree. + +Each result processes 1,000,000 rows. It is the mean of five independent process runs; each process records one cold call and the mean of ten warm calls. JIT calls use a fresh kernel-cache directory in every process. The `±` values are sample standard deviations. + +| Operation | Variant | Cold (s) | Warm (ms) | Throughput (M rows/s) | Effective bandwidth (GiB/s) | Peak allocation (MiB) | Allocated/call (MiB) | Warm speedup | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| `request-line` | precompiled | 0.066313 ± 0.000504 | 68.416 ± 0.167 | 14.617 ± 0.035 | 0.975 ± 0.002 | 32.00 | 32.23 | 1.00× | +| `request-line` | runtime JIT | 1.398848 ± 0.008472 | 2.574 ± 0.017 | 388.528 ± 2.554 | 25.910 ± 0.170 | 43.32 | 54.77 | 26.58× | +| `request-line` | AOT LTO JIT-linked | 1.417205 ± 0.008892 | 2.618 ± 0.041 | 381.982 ± 5.925 | 25.473 ± 0.395 | 43.32 | 54.77 | 26.13× | +| `combined-log` | precompiled | 0.601166 ± 0.011457 | 600.868 ± 3.285 | 1.664 ± 0.009 | 0.495 ± 0.003 | 145.13 | 145.85 | 1.00× | +| `combined-log` | runtime JIT | 1.554661 ± 0.013899 | 25.394 ± 0.209 | 39.382 ± 0.323 | 11.711 ± 0.096 | 171.72 | 198.42 | 23.66× | +| `combined-log` | AOT LTO JIT-linked | 1.620733 ± 0.055994 | 25.465 ± 0.166 | 39.270 ± 0.254 | 11.677 ± 0.076 | 171.72 | 198.42 | 23.60× | + +Effective bandwidth is `(input bytes + output bytes) / warm time`. Peak allocation and allocated-per-call are reported by the example's tracking memory resource; they are allocation metrics, not total device-resident memory. The precompiled implementation intentionally uses public non-JIT cuDF string and regex functions, while the JIT variants fuse parsing, sizing, and output construction. + +The machine-readable comparison is in [`http_log_performance.csv`](http_log_performance.csv). From b5b58cde3653f25b25b8be68ccc151810debbbd6 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Sun, 5 Jul 2026 20:42:11 +0100 Subject: [PATCH 04/27] Organize HTTP log transform example files --- cpp/examples/string_transforms/CMakeLists.txt | 30 +++++++++---------- .../fragments/combined_log_output.cu} | 2 +- .../fragments/combined_log_sizes.cu} | 2 +- .../fragments/request_line_output.cu} | 2 +- .../fragments/request_line_sizes.cu} | 2 +- .../{http_logs.csv => http_logs/logs.csv} | 0 .../performance.csv} | 0 .../performance.md} | 2 +- .../transforms.cpp} | 8 ++--- .../{http_log_udf.cuh => http_logs/udf.cuh} | 0 10 files changed, 24 insertions(+), 24 deletions(-) rename cpp/examples/string_transforms/{fragments/http_combined_log_output.cu => http_logs/fragments/combined_log_output.cu} (97%) rename cpp/examples/string_transforms/{fragments/http_combined_log_sizes.cu => http_logs/fragments/combined_log_sizes.cu} (97%) rename cpp/examples/string_transforms/{fragments/http_request_line_output.cu => http_logs/fragments/request_line_output.cu} (95%) rename cpp/examples/string_transforms/{fragments/http_request_line_sizes.cu => http_logs/fragments/request_line_sizes.cu} (95%) rename cpp/examples/string_transforms/{http_logs.csv => http_logs/logs.csv} (100%) rename cpp/examples/string_transforms/{http_log_performance.csv => http_logs/performance.csv} (100%) rename cpp/examples/string_transforms/{http_log_performance.md => http_logs/performance.md} (95%) rename cpp/examples/string_transforms/{http_log_transforms.cpp => http_logs/transforms.cpp} (98%) rename cpp/examples/string_transforms/{http_log_udf.cuh => http_logs/udf.cuh} (100%) diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index f1a3aa7e139b..4cefc7b3e79a 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -80,29 +80,29 @@ add_string_transforms_example(localize_phone_jit localize_phone_jit.cpp) add_string_transforms_example(localize_phone_precompiled localize_phone_precompiled.cpp) rtcx_add_embed(http_log_fragments) -add_fragment(http_log_fragments FRAGMENT http_request_line_sizes SOURCE - fragments/http_request_line_sizes.cu) -add_fragment(http_log_fragments FRAGMENT http_request_line_output SOURCE - fragments/http_request_line_output.cu) -add_fragment(http_log_fragments FRAGMENT http_combined_log_sizes SOURCE - fragments/http_combined_log_sizes.cu) -add_fragment(http_log_fragments FRAGMENT http_combined_log_output SOURCE - fragments/http_combined_log_output.cu) +add_fragment(http_log_fragments FRAGMENT request_line_sizes SOURCE + http_logs/fragments/request_line_sizes.cu) +add_fragment(http_log_fragments FRAGMENT request_line_output SOURCE + http_logs/fragments/request_line_output.cu) +add_fragment(http_log_fragments FRAGMENT combined_log_sizes SOURCE + http_logs/fragments/combined_log_sizes.cu) +add_fragment(http_log_fragments FRAGMENT combined_log_output SOURCE + http_logs/fragments/combined_log_output.cu) foreach( fragment_target - http_log_fragments_http_request_line_sizes - http_log_fragments_http_request_line_output - http_log_fragments_http_combined_log_sizes - http_log_fragments_http_combined_log_output) + http_log_fragments_request_line_sizes + http_log_fragments_request_line_output + http_log_fragments_combined_log_sizes + http_log_fragments_combined_log_output) target_include_directories(${fragment_target} - PRIVATE ${CMAKE_CURRENT_LIST_DIR}) + PRIVATE ${CMAKE_CURRENT_LIST_DIR}/http_logs) endforeach() rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed") -add_string_transforms_example(http_log_transforms http_log_transforms.cpp) +add_string_transforms_example(http_log_transforms http_logs/transforms.cpp) target_sources( http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}/http_log_fragments.s ) @@ -111,5 +111,5 @@ target_include_directories(http_log_transforms add_dependencies(http_log_transforms http_log_fragments) install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv - ${CMAKE_CURRENT_LIST_DIR}/http_logs.csv + ${CMAKE_CURRENT_LIST_DIR}/http_logs/logs.csv DESTINATION bin/examples/libcudf/string_transformers) diff --git a/cpp/examples/string_transforms/fragments/http_combined_log_output.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu similarity index 97% rename from cpp/examples/string_transforms/fragments/http_combined_log_output.cu rename to cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu index 084ad6b89bc7..29e739e580f9 100644 --- a/cpp/examples/string_transforms/fragments/http_combined_log_output.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "http_log_udf.cuh" +#include "udf.cuh" extern "C" __device__ int transform(cuda::std::span* client_ip, cuda::std::span* timestamp, diff --git a/cpp/examples/string_transforms/fragments/http_combined_log_sizes.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu similarity index 97% rename from cpp/examples/string_transforms/fragments/http_combined_log_sizes.cu rename to cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu index fbc5c1b92712..f8dd5b9b143e 100644 --- a/cpp/examples/string_transforms/fragments/http_combined_log_sizes.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "http_log_udf.cuh" +#include "udf.cuh" extern "C" __device__ int transform(int32_t* client_ip_size, int32_t* timestamp_size, diff --git a/cpp/examples/string_transforms/fragments/http_request_line_output.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu similarity index 95% rename from cpp/examples/string_transforms/fragments/http_request_line_output.cu rename to cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu index 273485feff54..cd4953abc491 100644 --- a/cpp/examples/string_transforms/fragments/http_request_line_output.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "http_log_udf.cuh" +#include "udf.cuh" extern "C" __device__ int transform(cuda::std::span* method, cuda::std::span* path, diff --git a/cpp/examples/string_transforms/fragments/http_request_line_sizes.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu similarity index 95% rename from cpp/examples/string_transforms/fragments/http_request_line_sizes.cu rename to cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu index 43272f7813e8..302b3487054f 100644 --- a/cpp/examples/string_transforms/fragments/http_request_line_sizes.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "http_log_udf.cuh" +#include "udf.cuh" extern "C" __device__ int transform(int32_t* method_size, int32_t* path_size, diff --git a/cpp/examples/string_transforms/http_logs.csv b/cpp/examples/string_transforms/http_logs/logs.csv similarity index 100% rename from cpp/examples/string_transforms/http_logs.csv rename to cpp/examples/string_transforms/http_logs/logs.csv diff --git a/cpp/examples/string_transforms/http_log_performance.csv b/cpp/examples/string_transforms/http_logs/performance.csv similarity index 100% rename from cpp/examples/string_transforms/http_log_performance.csv rename to cpp/examples/string_transforms/http_logs/performance.csv diff --git a/cpp/examples/string_transforms/http_log_performance.md b/cpp/examples/string_transforms/http_logs/performance.md similarity index 95% rename from cpp/examples/string_transforms/http_log_performance.md rename to cpp/examples/string_transforms/http_logs/performance.md index af047b7f900f..50b6641b0efb 100644 --- a/cpp/examples/string_transforms/http_log_performance.md +++ b/cpp/examples/string_transforms/http_logs/performance.md @@ -15,4 +15,4 @@ Each result processes 1,000,000 rows. It is the mean of five independent process Effective bandwidth is `(input bytes + output bytes) / warm time`. Peak allocation and allocated-per-call are reported by the example's tracking memory resource; they are allocation metrics, not total device-resident memory. The precompiled implementation intentionally uses public non-JIT cuDF string and regex functions, while the JIT variants fuse parsing, sizing, and output construction. -The machine-readable comparison is in [`http_log_performance.csv`](http_log_performance.csv). +The machine-readable comparison is in [`performance.csv`](performance.csv). diff --git a/cpp/examples/string_transforms/http_log_transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp similarity index 98% rename from cpp/examples/string_transforms/http_log_transforms.cpp rename to cpp/examples/string_transforms/http_logs/transforms.cpp index 822630cc2a9c..d2c1fed131be 100644 --- a/cpp/examples/string_transforms/http_log_transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -286,8 +286,8 @@ constexpr std::string_view usage = mr); } else { auto const id = selected_operation == operation::REQUEST_LINE - ? http_log_fragments::http_request_line_sizes - : http_log_fragments::http_combined_log_sizes; + ? http_log_fragments::request_line_sizes + : http_log_fragments::combined_log_sizes; sizes = cudf::transform_lto(fragment(id), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, @@ -323,8 +323,8 @@ constexpr std::string_view usage = } auto const id = selected_operation == operation::REQUEST_LINE - ? http_log_fragments::http_request_line_output - : http_log_fragments::http_combined_log_output; + ? http_log_fragments::request_line_output + : http_log_fragments::combined_log_output; return cudf::transform_lto(fragment(id), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, diff --git a/cpp/examples/string_transforms/http_log_udf.cuh b/cpp/examples/string_transforms/http_logs/udf.cuh similarity index 100% rename from cpp/examples/string_transforms/http_log_udf.cuh rename to cpp/examples/string_transforms/http_logs/udf.cuh From f142f830ba5ce737dc307725047a3ae96b53a986 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Sun, 5 Jul 2026 20:54:10 +0100 Subject: [PATCH 05/27] Rename regex HTTP log implementation --- .../string_transforms/http_logs/transforms.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index d2c1fed131be..33d3906a0b65 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -241,10 +241,10 @@ constexpr std::string_view usage = return http_log_fragments::files.subspan(range[0], range[1]); } -[[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, - operation selected_operation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +[[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, + operation selected_operation, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { if (selected_operation == operation::REQUEST_LINE) { static auto const program = @@ -344,7 +344,7 @@ constexpr std::string_view usage = rmm::device_async_resource_ref mr) { return implementation == variant::PRECOMPILED - ? run_precompiled(input, selected_operation, stream, mr) + ? run_regex(input, selected_operation, stream, mr) : run_two_pass(input, selected_operation, implementation, stream, mr); } From 7cc2f4602ee0bdf51fa8eddde21417601d50f61d Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 6 Jul 2026 18:45:35 +0100 Subject: [PATCH 06/27] Improve HTTP log example readability --- .../fragments/combined_log_output.cu | 14 +- .../fragments/request_line_output.cu | 6 +- .../http_logs/transforms.cpp | 512 +++++++++++------- .../string_transforms/http_logs/udf.cuh | 66 +-- 4 files changed, 354 insertions(+), 244 deletions(-) diff --git a/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu index 29e739e580f9..256b7f41984d 100644 --- a/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu @@ -15,12 +15,12 @@ extern "C" __device__ int transform(cuda::std::span* client_ip, cudf::string_view input) { auto const fields = http_log_udf::parse_combined_log(input); - http_log_udf::copy_range(*client_ip, input, fields.client_ip); - http_log_udf::copy_range(*timestamp, input, fields.timestamp); - http_log_udf::copy_range(*method, input, fields.method); - http_log_udf::copy_range(*path, input, fields.path); - http_log_udf::copy_range(*status, input, fields.status); - http_log_udf::copy_range(*referer, input, fields.referer); - http_log_udf::copy_range(*user_agent, input, fields.user_agent); + http_log_udf::copy_field(*client_ip, input, fields.client_ip); + http_log_udf::copy_field(*timestamp, input, fields.timestamp); + http_log_udf::copy_field(*method, input, fields.method); + http_log_udf::copy_field(*path, input, fields.path); + http_log_udf::copy_field(*status, input, fields.status); + http_log_udf::copy_field(*referer, input, fields.referer); + http_log_udf::copy_field(*user_agent, input, fields.user_agent); return 0; } diff --git a/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu index cd4953abc491..6d56c7187c5c 100644 --- a/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu @@ -11,8 +11,8 @@ extern "C" __device__ int transform(cuda::std::span* method, cudf::string_view input) { auto const fields = http_log_udf::parse_request_line(input); - http_log_udf::copy_range(*method, input, fields.method); - http_log_udf::copy_range(*path, input, fields.path); - http_log_udf::copy_range(*version, input, fields.version); + http_log_udf::copy_field(*method, input, fields.method); + http_log_udf::copy_field(*path, input, fields.path); + http_log_udf::copy_field(*version, input, fields.version); return 0; } diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index 33d3906a0b65..602ab78e7ac4 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -36,6 +36,14 @@ namespace { enum class variant { PRECOMPILED, JIT, LTO }; enum class operation { REQUEST_LINE, COMBINED_LOG }; +constexpr auto request_line_output_count = std::size_t{3}; +constexpr auto combined_log_output_count = std::size_t{7}; + +constexpr std::string_view request_line_pattern = + R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"; +constexpr std::string_view combined_log_pattern = + R"regex(^([^ ]+) - [^ ]+ \[([^]]+)\] "([A-Z]+) ([^ ?]+)[^ ]* HTTP/[0-9.]+" ([0-9]{3}) [0-9]+ "([^"]*)" "([^"]*)"$)regex"; + struct options { std::string input_path; std::string output_path; @@ -45,38 +53,54 @@ struct options { int iterations; }; +// Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes +// exact output sizes and another that writes into the resulting character buffers. constexpr char request_line_sizes_udf[] = R"***( -__device__ void size_http_request(int32_t* method_size, int32_t* path_size, - int32_t* version_size, cudf::string_view input) { - auto find_char = [&](char needle, int32_t begin) { - for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; +__device__ void compute_request_line_sizes(int32_t* method_size, + int32_t* path_size, + int32_t* version_size, + cudf::string_view input) { + auto find_character = [&](char needle, int32_t begin) { + for (auto index = begin; index < input.size_bytes(); ++index) { + if (input.data()[index] == needle) { return index; } + } return input.size_bytes(); }; - auto method_end = find_char(' ', 0); - auto target_end = find_char(' ', method_end + 1); - auto query_begin = find_char('?', method_end + 1); - auto path_end = query_begin < target_end ? query_begin : target_end; - *method_size = method_end; - *path_size = path_end - method_end - 1; + + auto const method_end = find_character(' ', 0); + auto const target_end = find_character(' ', method_end + 1); + auto const query_begin = find_character('?', method_end + 1); + auto const path_end = query_begin < target_end ? query_begin : target_end; + + *method_size = method_end; + *path_size = path_end - method_end - 1; *version_size = input.size_bytes() - target_end - 6; } )***"; constexpr char request_line_output_udf[] = R"***( -__device__ void extract_http_request(cuda::std::span* method, cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) { - auto find_char = [&](char needle, int32_t begin) { - for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; +__device__ void write_request_line(cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) { + auto find_character = [&](char needle, int32_t begin) { + for (auto index = begin; index < input.size_bytes(); ++index) { + if (input.data()[index] == needle) { return index; } + } return input.size_bytes(); }; + auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { - for (int32_t i = begin; i < end; ++i) out[i - begin] = input.data()[i]; + for (auto index = begin; index < end; ++index) { + out[index - begin] = input.data()[index]; + } }; - auto method_end = find_char(' ', 0); - auto target_end = find_char(' ', method_end + 1); - auto query_begin = find_char('?', method_end + 1); - auto path_end = query_begin < target_end ? query_begin : target_end; + + auto const method_end = find_character(' ', 0); + auto const target_end = find_character(' ', method_end + 1); + auto const query_begin = find_character('?', method_end + 1); + auto const path_end = query_begin < target_end ? query_begin : target_end; + copy_field(*method, 0, method_end); copy_field(*path, method_end + 1, path_end); copy_field(*version, target_end + 6, input.size_bytes()); @@ -84,93 +108,129 @@ __device__ void extract_http_request(cuda::std::span* method, cuda::std::s )***"; constexpr char combined_log_sizes_udf[] = R"***( -__device__ void size_combined_log(int32_t* ip, int32_t* timestamp, int32_t* method, - int32_t* path, int32_t* status, int32_t* referer, - int32_t* user_agent, cudf::string_view input) { - auto find_char = [&](char needle, int32_t begin) { - for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; +__device__ void compute_combined_log_sizes(int32_t* client_ip_size, + int32_t* timestamp_size, + int32_t* method_size, + int32_t* path_size, + int32_t* status_size, + int32_t* referer_size, + int32_t* user_agent_size, + cudf::string_view input) { + auto find_character = [&](char needle, int32_t begin) { + for (auto index = begin; index < input.size_bytes(); ++index) { + if (input.data()[index] == needle) { return index; } + } return input.size_bytes(); }; - auto ip_end = find_char(' ', 0); - auto timestamp_begin = find_char('[', ip_end) + 1; - auto timestamp_end = find_char(']', timestamp_begin); - auto request_begin = find_char('\"', timestamp_end) + 1; - auto method_end = find_char(' ', request_begin); - auto target_end = find_char(' ', method_end + 1); - auto query_begin = find_char('?', method_end + 1); - auto path_end = query_begin < target_end ? query_begin : target_end; - auto request_end = find_char('\"', target_end); - auto status_begin = request_end + 2; - auto status_end = find_char(' ', status_begin); - auto bytes_end = find_char(' ', status_end + 1); - auto referer_begin = find_char('\"', bytes_end) + 1; - auto referer_end = find_char('\"', referer_begin); - auto user_agent_begin = find_char('\"', referer_end + 1) + 1; - auto user_agent_end = find_char('\"', user_agent_begin); - *ip = ip_end; *timestamp = timestamp_end - timestamp_begin; - *method = method_end - request_begin; *path = path_end - method_end - 1; - *status = status_end - status_begin; *referer = referer_end - referer_begin; - *user_agent = user_agent_end - user_agent_begin; + + auto const client_ip_end = find_character(' ', 0); + auto const timestamp_begin = find_character('[', client_ip_end) + 1; + auto const timestamp_end = find_character(']', timestamp_begin); + auto const request_begin = find_character('\"', timestamp_end) + 1; + auto const method_end = find_character(' ', request_begin); + auto const target_end = find_character(' ', method_end + 1); + auto const query_begin = find_character('?', method_end + 1); + auto const path_end = query_begin < target_end ? query_begin : target_end; + auto const request_end = find_character('\"', target_end); + auto const status_begin = request_end + 2; + auto const status_end = find_character(' ', status_begin); + auto const bytes_end = find_character(' ', status_end + 1); + auto const referer_begin = find_character('\"', bytes_end) + 1; + auto const referer_end = find_character('\"', referer_begin); + auto const user_agent_begin = find_character('\"', referer_end + 1) + 1; + auto const user_agent_end = find_character('\"', user_agent_begin); + + *client_ip_size = client_ip_end; + *timestamp_size = timestamp_end - timestamp_begin; + *method_size = method_end - request_begin; + *path_size = path_end - method_end - 1; + *status_size = status_end - status_begin; + *referer_size = referer_end - referer_begin; + *user_agent_size = user_agent_end - user_agent_begin; } )***"; constexpr char combined_log_output_udf[] = R"***( -__device__ void extract_combined_log(cuda::std::span* ip, - cuda::std::span* timestamp, - cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* status, - cuda::std::span* referer, - cuda::std::span* user_agent, - cudf::string_view input) { - auto find_char = [&](char needle, int32_t begin) { - for (auto i = begin; i < input.size_bytes(); ++i) if (input.data()[i] == needle) return i; +__device__ void write_combined_log(cuda::std::span* client_ip, + cuda::std::span* timestamp, + cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* status, + cuda::std::span* referer, + cuda::std::span* user_agent, + cudf::string_view input) { + auto find_character = [&](char needle, int32_t begin) { + for (auto index = begin; index < input.size_bytes(); ++index) { + if (input.data()[index] == needle) { return index; } + } return input.size_bytes(); }; + auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { - for (int32_t i = begin; i < end; ++i) out[i - begin] = input.data()[i]; + for (auto index = begin; index < end; ++index) { + out[index - begin] = input.data()[index]; + } }; - auto ip_end = find_char(' ', 0); - auto timestamp_begin = find_char('[', ip_end) + 1; - auto timestamp_end = find_char(']', timestamp_begin); - auto request_begin = find_char('\"', timestamp_end) + 1; - auto method_end = find_char(' ', request_begin); - auto target_end = find_char(' ', method_end + 1); - auto query_begin = find_char('?', method_end + 1); - auto path_end = query_begin < target_end ? query_begin : target_end; - auto request_end = find_char('\"', target_end); - auto status_begin = request_end + 2; - auto status_end = find_char(' ', status_begin); - auto bytes_end = find_char(' ', status_end + 1); - auto referer_begin = find_char('\"', bytes_end) + 1; - auto referer_end = find_char('\"', referer_begin); - auto user_agent_begin = find_char('\"', referer_end + 1) + 1; - auto user_agent_end = find_char('\"', user_agent_begin); - copy_field(*ip, 0, ip_end); copy_field(*timestamp, timestamp_begin, timestamp_end); - copy_field(*method, request_begin, method_end); copy_field(*path, method_end + 1, path_end); - copy_field(*status, status_begin, status_end); copy_field(*referer, referer_begin, referer_end); + + auto const client_ip_end = find_character(' ', 0); + auto const timestamp_begin = find_character('[', client_ip_end) + 1; + auto const timestamp_end = find_character(']', timestamp_begin); + auto const request_begin = find_character('\"', timestamp_end) + 1; + auto const method_end = find_character(' ', request_begin); + auto const target_end = find_character(' ', method_end + 1); + auto const query_begin = find_character('?', method_end + 1); + auto const path_end = query_begin < target_end ? query_begin : target_end; + auto const request_end = find_character('\"', target_end); + auto const status_begin = request_end + 2; + auto const status_end = find_character(' ', status_begin); + auto const bytes_end = find_character(' ', status_end + 1); + auto const referer_begin = find_character('\"', bytes_end) + 1; + auto const referer_end = find_character('\"', referer_begin); + auto const user_agent_begin = find_character('\"', referer_end + 1) + 1; + auto const user_agent_end = find_character('\"', user_agent_begin); + + copy_field(*client_ip, 0, client_ip_end); + copy_field(*timestamp, timestamp_begin, timestamp_end); + copy_field(*method, request_begin, method_end); + copy_field(*path, method_end + 1, path_end); + copy_field(*status, status_begin, status_end); + copy_field(*referer, referer_begin, referer_end); copy_field(*user_agent, user_agent_begin, user_agent_end); } )***"; -[[nodiscard]] std::string const& to_string(variant value) +[[nodiscard]] constexpr std::string_view to_string(variant value) { - static std::string const precompiled{"precompiled"}; - static std::string const jit{"jit"}; - static std::string const lto{"lto"}; switch (value) { - case variant::PRECOMPILED: return precompiled; - case variant::JIT: return jit; - case variant::LTO: return lto; + case variant::PRECOMPILED: return "precompiled"; + case variant::JIT: return "jit"; + case variant::LTO: return "lto"; } throw std::logic_error("Unknown variant"); } -[[nodiscard]] std::string const& to_string(operation value) +[[nodiscard]] constexpr std::string_view to_string(operation value) +{ + switch (value) { + case operation::REQUEST_LINE: return "request-line"; + case operation::COMBINED_LOG: return "combined-log"; + } + throw std::logic_error("Unknown operation"); +} + +[[nodiscard]] variant parse_variant(std::string_view name) +{ + if (name == "precompiled") { return variant::PRECOMPILED; } + if (name == "jit") { return variant::JIT; } + if (name == "lto") { return variant::LTO; } + throw std::invalid_argument("variant must be precompiled, jit, or lto"); +} + +[[nodiscard]] operation parse_operation(std::string_view name) { - static std::string const request_line{"request-line"}; - static std::string const combined_log{"combined-log"}; - return value == operation::REQUEST_LINE ? request_line : combined_log; + if (name == "request-line") { return operation::REQUEST_LINE; } + if (name == "combined-log") { return operation::COMBINED_LOG; } + throw std::invalid_argument("operation must be request-line or combined-log"); } constexpr std::string_view usage = @@ -184,19 +244,8 @@ constexpr std::string_view usage = throw std::invalid_argument("invalid arguments; run http_log_transforms --help for usage"); } - auto const implementation = std::string_view{argv[3]} == "precompiled" ? variant::PRECOMPILED - : std::string_view{argv[3]} == "jit" ? variant::JIT - : variant::LTO; - if (std::string_view{argv[3]} != "precompiled" && std::string_view{argv[3]} != "jit" && - std::string_view{argv[3]} != "lto") { - throw std::invalid_argument("variant must be precompiled, jit, or lto"); - } - - auto const selected_operation = - std::string_view{argv[4]} == "request-line" ? operation::REQUEST_LINE : operation::COMBINED_LOG; - if (std::string_view{argv[4]} != "request-line" && std::string_view{argv[4]} != "combined-log") { - throw std::invalid_argument("operation must be request-line or combined-log"); - } + auto const implementation = parse_variant(argv[3]); + auto const selected_operation = parse_operation(argv[4]); auto const rows = std::stoll(argv[5]); auto const iterations = std::stoi(argv[6]); @@ -212,157 +261,225 @@ constexpr std::string_view usage = iterations}; } -[[nodiscard]] std::unique_ptr make_offsets(cudf::column_view const sizes, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +[[nodiscard]] constexpr std::size_t output_count(operation selected_operation) { - auto inclusive = cudf::scan(sizes, - *cudf::make_sum_aggregation(), - cudf::scan_type::INCLUSIVE, - cudf::null_policy::EXCLUDE, - stream, - mr); - auto const zero = cudf::numeric_scalar{0, true, stream, mr}; - auto first = cudf::make_column_from_scalar(zero, 1, stream, mr); - return cudf::concatenate( - std::vector{first->view(), inclusive->view()}, stream, mr); + return selected_operation == operation::REQUEST_LINE ? request_line_output_count + : combined_log_output_count; +} + +[[nodiscard]] constexpr cudf::size_type input_column_index(operation selected_operation) +{ + return selected_operation == operation::REQUEST_LINE ? 0 : 1; +} + +[[nodiscard]] char const* sizing_udf(operation selected_operation) +{ + return selected_operation == operation::REQUEST_LINE ? request_line_sizes_udf + : combined_log_sizes_udf; +} + +[[nodiscard]] char const* output_udf(operation selected_operation) +{ + return selected_operation == operation::REQUEST_LINE ? request_line_output_udf + : combined_log_output_udf; +} + +[[nodiscard]] std::size_t sizing_fragment(operation selected_operation) +{ + return selected_operation == operation::REQUEST_LINE ? http_log_fragments::request_line_sizes + : http_log_fragments::combined_log_sizes; +} + +[[nodiscard]] std::size_t output_fragment(operation selected_operation) +{ + return selected_operation == operation::REQUEST_LINE ? http_log_fragments::request_line_output + : http_log_fragments::combined_log_output; } -[[nodiscard]] std::vector output_specs(std::size_t count, - cudf::type_id type) +[[nodiscard]] std::vector make_output_specs(std::size_t count, + cudf::type_id type) { - return std::vector( - count, cudf::transform_output{cudf::data_type{type}, cudf::output_nullability::ALL_VALID}); + auto const spec = + cudf::transform_output{cudf::data_type{type}, cudf::output_nullability::ALL_VALID}; + return std::vector(count, spec); } -[[nodiscard]] std::span fragment(std::size_t id) +[[nodiscard]] std::span get_fragment(std::size_t id) { auto const range = http_log_fragments::file_ranges[id]; return http_log_fragments::files.subspan(range[0], range[1]); } +[[nodiscard]] std::unique_ptr make_string_offsets( + cudf::column_view const string_sizes, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // A strings column uses N+1 offsets. Scanning the N string sizes produces every run end; adding + // a leading zero supplies the first offset. + auto run_ends = cudf::scan(string_sizes, + *cudf::make_sum_aggregation(), + cudf::scan_type::INCLUSIVE, + cudf::null_policy::EXCLUDE, + stream, + mr); + auto const zero_offset = cudf::numeric_scalar{0, true, stream, mr}; + auto first_offset = cudf::make_column_from_scalar(zero_offset, 1, stream, mr); + return cudf::concatenate( + std::vector{first_offset->view(), run_ends->view()}, stream, mr); +} + [[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, operation selected_operation, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { + // This public cuDF regex implementation is the non-JIT comparison baseline. if (selected_operation == operation::REQUEST_LINE) { - static auto const program = - cudf::strings::regex_program::create(R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"); + static auto const program = cudf::strings::regex_program::create(request_line_pattern); return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); } - static auto const program = cudf::strings::regex_program::create( - R"regex(^([^ ]+) - [^ ]+ \[([^]]+)\] "([A-Z]+) ([^ ?]+)[^ ]* HTTP/[0-9.]+" ([0-9]{3}) [0-9]+ "([^"]*)" "([^"]*)"$)regex"); + static auto const program = cudf::strings::regex_program::create(combined_log_pattern); return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); } -[[nodiscard]] std::unique_ptr run_two_pass(cudf::column_view input, - operation selected_operation, - variant implementation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +[[nodiscard]] std::unique_ptr compute_string_sizes(cudf::column_view input, + operation selected_operation, + variant implementation, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { - // Pass 1 emits the exact byte count for every output string and row. Scanning each size column - // produces run-end offsets, so multi_transform can allocate each chars child once. Pass 2 then - // receives a cuda::std::span for every row/output and writes directly into final storage. - auto const count = - selected_operation == operation::REQUEST_LINE ? std::size_t{3} : std::size_t{7}; - auto sizes_out = output_specs(count, cudf::type_id::INT32); + // Pass 1: produce one INT32 byte-count column for each eventual string output. + auto const outputs = make_output_specs(output_count(selected_operation), cudf::type_id::INT32); cudf::transform_input inputs[] = {input}; - std::unique_ptr sizes; if (implementation == variant::JIT) { - auto const source = selected_operation == operation::REQUEST_LINE ? request_line_sizes_udf - : combined_log_sizes_udf; - sizes = cudf::multi_transform(source, - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - sizes_out, - std::vector>{}, - std::nullopt, - stream, - mr); - } else { - auto const id = selected_operation == operation::REQUEST_LINE - ? http_log_fragments::request_line_sizes - : http_log_fragments::combined_log_sizes; - sizes = cudf::transform_lto(fragment(id), - cudf::lto_binary_type::FATBIN, - cudf::null_aware::NO, - std::nullopt, - inputs, - sizes_out, - std::vector>{}, - std::nullopt, - stream, - mr); + return cudf::multi_transform(sizing_udf(selected_operation), + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + {}, + std::nullopt, + stream, + mr); } + return cudf::transform_lto(get_fragment(sizing_fragment(selected_operation)), + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + {}, + std::nullopt, + stream, + mr); +} + +[[nodiscard]] std::vector> make_all_string_offsets( + cudf::table_view const size_columns, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ std::vector> offsets; - offsets.reserve(count); - for (auto const& size_column : sizes->view()) { - offsets.push_back(make_offsets(size_column, stream, mr)); + offsets.reserve(size_columns.num_columns()); + for (auto const& string_sizes : size_columns) { + offsets.push_back(make_string_offsets(string_sizes, stream, mr)); } + return offsets; +} + +[[nodiscard]] std::unique_ptr write_strings( + cudf::column_view input, + operation selected_operation, + variant implementation, + std::vector>&& string_offsets, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // Pass 2: use the precomputed offsets to write directly into final strings columns. + auto const outputs = make_output_specs(output_count(selected_operation), cudf::type_id::STRING); + cudf::transform_input inputs[] = {input}; - auto strings_out = output_specs(count, cudf::type_id::STRING); if (implementation == variant::JIT) { - auto const source = selected_operation == operation::REQUEST_LINE ? request_line_output_udf - : combined_log_output_udf; - return cudf::multi_transform(source, + return cudf::multi_transform(output_udf(selected_operation), cudf::udf_source_type::CUDA, cudf::null_aware::NO, std::nullopt, inputs, - strings_out, - std::move(offsets), + outputs, + std::move(string_offsets), std::nullopt, stream, mr); } - auto const id = selected_operation == operation::REQUEST_LINE - ? http_log_fragments::request_line_output - : http_log_fragments::combined_log_output; - return cudf::transform_lto(fragment(id), + return cudf::transform_lto(get_fragment(output_fragment(selected_operation)), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, std::nullopt, inputs, - strings_out, - std::move(offsets), + outputs, + std::move(string_offsets), std::nullopt, stream, mr); } -[[nodiscard]] std::unique_ptr run(cudf::column_view input, - operation selected_operation, - variant implementation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +[[nodiscard]] std::unique_ptr run_two_pass(cudf::column_view input, + operation selected_operation, + variant implementation, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // Pass 1 computes the exact byte count of every output string. Inclusive scans turn those sizes + // into run-end offsets, allowing pass 2 to write directly into the final character buffers. + auto sizes = compute_string_sizes(input, selected_operation, implementation, stream, mr); + auto offsets = make_all_string_offsets(sizes->view(), stream, mr); + return write_strings(input, selected_operation, implementation, std::move(offsets), stream, mr); +} + +[[nodiscard]] std::unique_ptr run_transform(cudf::column_view input, + operation selected_operation, + variant implementation, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + if (implementation == variant::PRECOMPILED) { + return run_regex(input, selected_operation, stream, mr); + } + return run_two_pass(input, selected_operation, implementation, stream, mr); +} + +[[nodiscard]] std::vector output_column_names(operation selected_operation) { - return implementation == variant::PRECOMPILED - ? run_regex(input, selected_operation, stream, mr) - : run_two_pass(input, selected_operation, implementation, stream, mr); + if (selected_operation == operation::REQUEST_LINE) { return {"method", "path", "http_version"}; } + return {"client_ip", "timestamp", "method", "path", "status", "referer", "user_agent"}; } void write_output(cudf::table_view const result, operation selected_operation, std::string const& output_path) { - auto names = selected_operation == operation::REQUEST_LINE - ? std::vector{"method", "path", "http_version"} - : std::vector{ - "client_ip", "timestamp", "method", "path", "status", "referer", "user_agent"}; auto options = cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result) .include_header(true) - .names(names) + .names(output_column_names(selected_operation)) .build(); cudf::io::write_csv(options); } +[[nodiscard]] std::unique_ptr read_input(options const& opts) +{ + auto read_options = + cudf::io::csv_reader_options::builder(cudf::io::source_info{opts.input_path}).header(0).build(); + auto input = cudf::io::read_csv(read_options).tbl; + + if (opts.rows == input->num_rows()) { return input; } + return cudf::sample(input->view(), opts.rows, cudf::sample_with_replacement::TRUE); +} + } // namespace int main(int argc, char const** argv) @@ -378,18 +495,10 @@ int main(int argc, char const** argv) auto const stream = cudf::get_default_stream(); auto const mr = cudf::get_current_device_resource_ref(); - auto read_options = - cudf::io::csv_reader_options::builder(cudf::io::source_info{opts.input_path}) - .header(0) - .build(); - auto input_data = cudf::io::read_csv(read_options); - auto input = - opts.rows == input_data.tbl->num_rows() - ? std::move(input_data.tbl) - : cudf::sample(input_data.tbl->view(), opts.rows, cudf::sample_with_replacement::TRUE); - auto const input_index = opts.selected_operation == operation::REQUEST_LINE ? 0 : 1; - auto const input_bytes = input->get_column(input_index).alloc_size(); - auto const input_column = input->get_column(input_index).view(); + auto input = read_input(opts); + auto const input_index = input_column_index(opts.selected_operation); + auto const input_bytes = input->get_column(input_index).alloc_size(); + auto const input_view = input->get_column(input_index).view(); rmm::mr::statistics_resource_adaptor stats{mr}; auto const stats_mr = rmm::device_async_resource_ref{stats}; @@ -398,7 +507,7 @@ int main(int argc, char const** argv) auto const cold_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_cold"); auto cold_result = - run(input_column, opts.selected_operation, opts.implementation, stream, stats_mr); + run_transform(input_view, opts.selected_operation, opts.implementation, stream, stats_mr); stream.synchronize(); nvtxRangePop(); auto const cold_seconds = @@ -410,7 +519,8 @@ int main(int argc, char const** argv) nvtxRangePush("http_log_warm"); for (auto i = 0; i < opts.iterations; ++i) { result.reset(); - result = run(input_column, opts.selected_operation, opts.implementation, stream, stats_mr); + result = + run_transform(input_view, opts.selected_operation, opts.implementation, stream, stats_mr); } stream.synchronize(); nvtxRangePop(); diff --git a/cpp/examples/string_transforms/http_logs/udf.cuh b/cpp/examples/string_transforms/http_logs/udf.cuh index d49447b7453a..086dbb3034a3 100644 --- a/cpp/examples/string_transforms/http_logs/udf.cuh +++ b/cpp/examples/string_transforms/http_logs/udf.cuh @@ -12,7 +12,7 @@ namespace http_log_udf { -struct range { +struct field_range { int32_t begin{}; int32_t end{}; @@ -20,24 +20,24 @@ struct range { }; struct request_line_fields { - range method; - range path; - range version; + field_range method; + field_range path; + field_range version; }; struct combined_log_fields { - range client_ip; - range timestamp; - range method; - range path; - range status; - range referer; - range user_agent; + field_range client_ip; + field_range timestamp; + field_range method; + field_range path; + field_range status; + field_range referer; + field_range user_agent; }; -[[nodiscard]] __device__ int32_t find(cudf::string_view const input, - char const needle, - int32_t begin) +[[nodiscard]] __device__ int32_t find_character(cudf::string_view const input, + char const needle, + int32_t begin) { for (auto i = begin; i < input.size_bytes(); ++i) { if (input.data()[i] == needle) { return i; } @@ -47,9 +47,9 @@ struct combined_log_fields { [[nodiscard]] __device__ request_line_fields parse_request_line(cudf::string_view const input) { - auto const method_end = find(input, ' ', 0); - auto const target_end = find(input, ' ', method_end + 1); - auto const query_begin = find(input, '?', method_end + 1); + auto const method_end = find_character(input, ' ', 0); + auto const target_end = find_character(input, ' ', method_end + 1); + auto const query_begin = find_character(input, '?', method_end + 1); auto const path_end = query_begin < target_end ? query_begin : target_end; constexpr int32_t http_prefix_size = 6; // " HTTP/" @@ -60,22 +60,22 @@ struct combined_log_fields { [[nodiscard]] __device__ combined_log_fields parse_combined_log(cudf::string_view const input) { - auto const ip_end = find(input, ' ', 0); - auto const timestamp_begin = find(input, '[', ip_end) + 1; - auto const timestamp_end = find(input, ']', timestamp_begin); - auto const request_begin = find(input, '"', timestamp_end) + 1; - auto const method_end = find(input, ' ', request_begin); - auto const target_end = find(input, ' ', method_end + 1); - auto const query_begin = find(input, '?', method_end + 1); + auto const ip_end = find_character(input, ' ', 0); + auto const timestamp_begin = find_character(input, '[', ip_end) + 1; + auto const timestamp_end = find_character(input, ']', timestamp_begin); + auto const request_begin = find_character(input, '"', timestamp_end) + 1; + auto const method_end = find_character(input, ' ', request_begin); + auto const target_end = find_character(input, ' ', method_end + 1); + auto const query_begin = find_character(input, '?', method_end + 1); auto const path_end = query_begin < target_end ? query_begin : target_end; - auto const request_end = find(input, '"', target_end); + auto const request_end = find_character(input, '"', target_end); auto const status_begin = request_end + 2; - auto const status_end = find(input, ' ', status_begin); - auto const bytes_end = find(input, ' ', status_end + 1); - auto const referer_begin = find(input, '"', bytes_end) + 1; - auto const referer_end = find(input, '"', referer_begin); - auto const user_agent_begin = find(input, '"', referer_end + 1) + 1; - auto const user_agent_end = find(input, '"', user_agent_begin); + auto const status_end = find_character(input, ' ', status_begin); + auto const bytes_end = find_character(input, ' ', status_end + 1); + auto const referer_begin = find_character(input, '"', bytes_end) + 1; + auto const referer_end = find_character(input, '"', referer_begin); + auto const user_agent_begin = find_character(input, '"', referer_end + 1) + 1; + auto const user_agent_end = find_character(input, '"', user_agent_begin); return {{0, ip_end}, {timestamp_begin, timestamp_end}, @@ -86,9 +86,9 @@ struct combined_log_fields { {user_agent_begin, user_agent_end}}; } -__device__ void copy_range(cuda::std::span output, +__device__ void copy_field(cuda::std::span output, cudf::string_view const input, - range const field) + field_range const field) { for (auto i = int32_t{0}; i < field.size(); ++i) { output[i] = input.data()[field.begin + i]; From 2e91087571469bc92e89073f98b96c7fefccd99e Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 8 Jul 2026 02:47:59 +0100 Subject: [PATCH 07/27] Document HTTP log transform example --- .../fragments/combined_log_output.cu | 2 ++ .../http_logs/fragments/combined_log_sizes.cu | 2 ++ .../fragments/request_line_output.cu | 2 ++ .../http_logs/fragments/request_line_sizes.cu | 2 ++ .../http_logs/transforms.cpp | 27 +++++++++++++++++++ .../string_transforms/http_logs/udf.cuh | 8 ++++++ 6 files changed, 43 insertions(+) diff --git a/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu index 256b7f41984d..5d3cc9c990d7 100644 --- a/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu @@ -5,6 +5,8 @@ #include "udf.cuh" +// The output spans point directly into final allocations, so this fragment only parses delimiters +// and copies bytes—there is no temporary string representation or compaction pass. extern "C" __device__ int transform(cuda::std::span* client_ip, cuda::std::span* timestamp, cuda::std::span* method, diff --git a/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu index f8dd5b9b143e..72d14464a774 100644 --- a/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu @@ -5,6 +5,8 @@ #include "udf.cuh" +// One output pointer is provided for each eventual string column. Each row writes only byte counts; +// the host scans these columns to build string offsets. extern "C" __device__ int transform(int32_t* client_ip_size, int32_t* timestamp_size, int32_t* method_size, diff --git a/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu index 6d56c7187c5c..1d5f03c45caf 100644 --- a/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu @@ -5,6 +5,8 @@ #include "udf.cuh" +// The host supplies spans backed by the final strings-column character buffers. Their lengths were +// computed by the matching sizing fragment. extern "C" __device__ int transform(cuda::std::span* method, cuda::std::span* path, cuda::std::span* version, diff --git a/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu index 302b3487054f..eccafc6b77bc 100644 --- a/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu +++ b/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu @@ -5,6 +5,8 @@ #include "udf.cuh" +// AOT fragments expose an unmangled `transform` symbol so transform_lto can link them with the +// precompiled libcudf kernel. Output pointers precede the per-row input in the transform ABI. extern "C" __device__ int transform(int32_t* method_size, int32_t* path_size, int32_t* version_size, diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index 602ab78e7ac4..6741a9557248 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -33,12 +33,15 @@ namespace { +// Every implementation below produces the same output schema. PRECOMPILED uses public regex APIs, +// JIT compiles CUDA source at runtime, and LTO links CUDA fragments compiled during the build. enum class variant { PRECOMPILED, JIT, LTO }; enum class operation { REQUEST_LINE, COMBINED_LOG }; constexpr auto request_line_output_count = std::size_t{3}; constexpr auto combined_log_output_count = std::size_t{7}; +// Each capture group becomes one output column in the public-regex baseline. constexpr std::string_view request_line_pattern = R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"; constexpr std::string_view combined_log_pattern = @@ -56,6 +59,8 @@ struct options { // Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes // exact output sizes and another that writes into the resulting character buffers. constexpr char request_line_sizes_udf[] = R"***( +// multi_transform calls this function once per input row. Pointer parameters are output columns; +// writing a byte count to each one lets the host create exact string offsets before allocation. __device__ void compute_request_line_sizes(int32_t* method_size, int32_t* path_size, int32_t* version_size, @@ -70,6 +75,7 @@ __device__ void compute_request_line_sizes(int32_t* method_size, auto const method_end = find_character(' ', 0); auto const target_end = find_character(' ', method_end + 1); auto const query_begin = find_character('?', method_end + 1); + // Strip the query string so the path matches the first regex capture workload. auto const path_end = query_begin < target_end ? query_begin : target_end; *method_size = method_end; @@ -79,6 +85,8 @@ __device__ void compute_request_line_sizes(int32_t* method_size, )***"; constexpr char request_line_output_udf[] = R"***( +// Each span points at the final character buffer for one output string in this row. Its size came +// from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. __device__ void write_request_line(cuda::std::span* method, cuda::std::span* path, cuda::std::span* version, @@ -108,6 +116,8 @@ __device__ void write_request_line(cuda::std::span* method, )***"; constexpr char combined_log_sizes_udf[] = R"***( +// The combined-log operation has seven outputs, so the UDF receives seven output pointers followed +// by the input string. The parameter order defines the output-column order. __device__ void compute_combined_log_sizes(int32_t* client_ip_size, int32_t* timestamp_size, int32_t* method_size, @@ -140,6 +150,7 @@ __device__ void compute_combined_log_sizes(int32_t* client_ip_size, auto const user_agent_begin = find_character('\"', referer_end + 1) + 1; auto const user_agent_end = find_character('\"', user_agent_begin); + // Report exact byte counts; the scan on the host turns these into run-end offsets. *client_ip_size = client_ip_end; *timestamp_size = timestamp_end - timestamp_begin; *method_size = method_end - request_begin; @@ -151,6 +162,8 @@ __device__ void compute_combined_log_sizes(int32_t* client_ip_size, )***"; constexpr char combined_log_output_udf[] = R"***( +// This second-pass UDF repeats the inexpensive delimiter search, then copies each parsed field into +// the exact final allocation described by the sizing pass. __device__ void write_combined_log(cuda::std::span* client_ip, cuda::std::span* timestamp, cuda::std::span* method, @@ -299,6 +312,8 @@ constexpr std::string_view usage = [[nodiscard]] std::vector make_output_specs(std::size_t count, cudf::type_id type) { + // The example data is well-formed, and both parsers always write every field, so no output needs + // a null mask. auto const spec = cudf::transform_output{cudf::data_type{type}, cudf::output_nullability::ALL_VALID}; return std::vector(count, spec); @@ -306,6 +321,8 @@ constexpr std::string_view usage = [[nodiscard]] std::span get_fragment(std::size_t id) { + // rtcx_embed concatenates all AOT fatbins into one byte array. file_ranges identifies the slice + // belonging to the requested sizing or output fragment. auto const range = http_log_fragments::file_ranges[id]; return http_log_fragments::files.subspan(range[0], range[1]); } @@ -354,6 +371,7 @@ constexpr std::string_view usage = cudf::transform_input inputs[] = {input}; if (implementation == variant::JIT) { + // Compile the human-readable CUDA source on first use, then reuse the cached kernel. return cudf::multi_transform(sizing_udf(selected_operation), cudf::udf_source_type::CUDA, cudf::null_aware::NO, @@ -366,6 +384,7 @@ constexpr std::string_view usage = mr); } + // Link the build-time-compiled fatbin with libcudf's transform kernel at runtime. return cudf::transform_lto(get_fragment(sizing_fragment(selected_operation)), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, @@ -404,6 +423,7 @@ constexpr std::string_view usage = cudf::transform_input inputs[] = {input}; if (implementation == variant::JIT) { + // Supplying offsets avoids the usual temporary string_view output and compaction step. return cudf::multi_transform(output_udf(selected_operation), cudf::udf_source_type::CUDA, cudf::null_aware::NO, @@ -416,6 +436,7 @@ constexpr std::string_view usage = mr); } + // The AOT path uses the same offsets and output ABI; only the UDF representation differs. return cudf::transform_lto(get_fragment(output_fragment(selected_operation)), cudf::lto_binary_type::FATBIN, cudf::null_aware::NO, @@ -477,6 +498,7 @@ void write_output(cudf::table_view const result, auto input = cudf::io::read_csv(read_options).tbl; if (opts.rows == input->num_rows()) { return input; } + // Sampling with replacement scales the small checked-in dataset to the requested benchmark size. return cudf::sample(input->view(), opts.rows, cudf::sample_with_replacement::TRUE); } @@ -500,10 +522,13 @@ int main(int argc, char const** argv) auto const input_bytes = input->get_column(input_index).alloc_size(); auto const input_view = input->get_column(input_index).view(); + // Track allocations made by the transforms without changing the application's upstream memory + // resource. rmm::mr::statistics_resource_adaptor stats{mr}; auto const stats_mr = rmm::device_async_resource_ref{stats}; stream.synchronize(); + // The cold measurement includes regex setup or JIT compilation/linking performed on first use. auto const cold_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_cold"); auto cold_result = @@ -515,6 +540,7 @@ int main(int argc, char const** argv) cold_result.reset(); std::unique_ptr result; + // Subsequent calls exercise the cached kernel and represent steady-state throughput. auto const warm_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_warm"); for (auto i = 0; i < opts.iterations; ++i) { @@ -528,6 +554,7 @@ int main(int argc, char const** argv) std::chrono::duration{std::chrono::steady_clock::now() - warm_start}.count() / opts.iterations; + // A dash suppresses CSV output so file I/O does not affect benchmark runs. if (opts.output_path != "-") { write_output(result->view(), opts.selected_operation, opts.output_path); } diff --git a/cpp/examples/string_transforms/http_logs/udf.cuh b/cpp/examples/string_transforms/http_logs/udf.cuh index 086dbb3034a3..15c49f2f01a1 100644 --- a/cpp/examples/string_transforms/http_logs/udf.cuh +++ b/cpp/examples/string_transforms/http_logs/udf.cuh @@ -12,6 +12,8 @@ namespace http_log_udf { +// A field is represented as a half-open byte range into the original log line. Keeping ranges +// avoids copying data while the parser discovers all output fields. struct field_range { int32_t begin{}; int32_t end{}; @@ -39,6 +41,7 @@ struct combined_log_fields { char const needle, int32_t begin) { + // These example formats are ASCII-delimited, so byte offsets are also valid character offsets. for (auto i = begin; i < input.size_bytes(); ++i) { if (input.data()[i] == needle) { return i; } } @@ -47,6 +50,7 @@ struct combined_log_fields { [[nodiscard]] __device__ request_line_fields parse_request_line(cudf::string_view const input) { + // Expected form: METHOD /path?optional-query HTTP/version auto const method_end = find_character(input, ' ', 0); auto const target_end = find_character(input, ' ', method_end + 1); auto const query_begin = find_character(input, '?', method_end + 1); @@ -60,6 +64,9 @@ struct combined_log_fields { [[nodiscard]] __device__ combined_log_fields parse_combined_log(cudf::string_view const input) { + // Walk the delimiters once and retain only ranges for the seven fields emitted by the example. + // The checked-in input is well-formed, so validation and malformed-row handling are intentionally + // outside the scope of this transform demonstration. auto const ip_end = find_character(input, ' ', 0); auto const timestamp_begin = find_character(input, '[', ip_end) + 1; auto const timestamp_end = find_character(input, ']', timestamp_begin); @@ -90,6 +97,7 @@ __device__ void copy_field(cuda::std::span output, cudf::string_view const input, field_range const field) { + // output is a view into the final chars child allocated from the sizing pass offsets. for (auto i = int32_t{0}; i < field.size(); ++i) { output[i] = input.data()[field.begin + i]; } From a67feb4b68cd3581336db50b4555007259d3cddc Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 14 Jul 2026 16:31:27 +0100 Subject: [PATCH 08/27] update examples --- cpp/cmake/Modules/AddFragment.cmake | 5 +- cpp/examples/string_transforms/CMakeLists.txt | 23 +- cpp/examples/string_transforms/README.md | 4 - .../string_transforms/http_logs/fragments.cu | 77 +++ .../fragments/combined_log_output.cu | 28 - .../http_logs/fragments/combined_log_sizes.cu | 28 - .../fragments/request_line_output.cu | 20 - .../http_logs/fragments/request_line_sizes.cu | 20 - .../http_logs/performance.csv | 7 - .../http_logs/performance.md | 18 - .../http_logs/transforms.cpp | 587 +++++------------- .../string_transforms/http_logs/udf.cuh | 106 ---- 12 files changed, 244 insertions(+), 679 deletions(-) create mode 100644 cpp/examples/string_transforms/http_logs/fragments.cu delete mode 100644 cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu delete mode 100644 cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu delete mode 100644 cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu delete mode 100644 cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu delete mode 100644 cpp/examples/string_transforms/http_logs/performance.csv delete mode 100644 cpp/examples/string_transforms/http_logs/performance.md delete mode 100644 cpp/examples/string_transforms/http_logs/udf.cuh diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index 187d9c36d594..4f1e8ebaff1d 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -14,7 +14,7 @@ include_guard(GLOBAL) macro(add_fragment) set(TARGET ${ARGV0}) set(ONE_VALUE_ARGS FRAGMENT SOURCE KERNEL_ONLY KERNEL_INSTANCE UDF_TYPE) - set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES) + set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES INCLUDE_DIRS) cmake_parse_arguments(ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) if(NOT ARG_FRAGMENT) @@ -54,6 +54,9 @@ macro(add_fragment) endif() target_compile_definitions(${OBJECT_ID} PRIVATE CUDF_DISABLE_EXPORTS ${ARG_DEFINITIONS}) + if(ARG_INCLUDE_DIRS) + target_include_directories(${OBJECT_ID} PRIVATE ${ARG_INCLUDE_DIRS}) + endif() set_target_properties( ${OBJECT_ID} PROPERTIES CUDA_SEPARABLE_COMPILATION ON diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 4cefc7b3e79a..5ec9167ecad3 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -81,24 +81,13 @@ add_string_transforms_example(localize_phone_precompiled localize_phone_precompi rtcx_add_embed(http_log_fragments) add_fragment(http_log_fragments FRAGMENT request_line_sizes SOURCE - http_logs/fragments/request_line_sizes.cu) + http_logs/fragments.cu + DEFINITIONS UDF_COMPUTE_SIZES + INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs) add_fragment(http_log_fragments FRAGMENT request_line_output SOURCE - http_logs/fragments/request_line_output.cu) -add_fragment(http_log_fragments FRAGMENT combined_log_sizes SOURCE - http_logs/fragments/combined_log_sizes.cu) -add_fragment(http_log_fragments FRAGMENT combined_log_output SOURCE - http_logs/fragments/combined_log_output.cu) - -foreach( - fragment_target - http_log_fragments_request_line_sizes - http_log_fragments_request_line_output - http_log_fragments_combined_log_sizes - http_log_fragments_combined_log_output) - target_include_directories(${fragment_target} - PRIVATE ${CMAKE_CURRENT_LIST_DIR}/http_logs) -endforeach() - + http_logs/fragments.cu + DEFINITIONS UDF_WRITE_OUTPUT + INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs) rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed") diff --git a/cpp/examples/string_transforms/README.md b/cpp/examples/string_transforms/README.md index b7e55f9a3963..3b68e94e8568 100644 --- a/cpp/examples/string_transforms/README.md +++ b/cpp/examples/string_transforms/README.md @@ -23,10 +23,6 @@ The following examples are included: - `lto`: the same sizing and output transform ABI, AOT-compiled to embedded fatbins and JIT-linked with libcudf's precompiled transform kernels. -The `request-line` operation extracts method, path, and HTTP version. The `combined-log` operation -extracts client IP, timestamp, method, path, status, referer, and user agent. Both implement the same -extraction groups as their comparative regex variant. - ## Compile and execute ```bash diff --git a/cpp/examples/string_transforms/http_logs/fragments.cu b/cpp/examples/string_transforms/http_logs/fragments.cu new file mode 100644 index 000000000000..cc59f05315b4 --- /dev/null +++ b/cpp/examples/string_transforms/http_logs/fragments.cu @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + + +// Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes +// exact output sizes and another that writes into the resulting character buffers. +__device__ void compute_request_line_sizes(int32_t* method_size, + int32_t* path_size, + int32_t* version_size, + cudf::string_view input) { + auto find_character = [&](char needle, int32_t begin) { + for (auto index = begin; index < input.size_bytes(); ++index) { + if (input.data()[index] == needle) { return index; } + } + return input.size_bytes(); + }; + + auto method_end = find_character(' ', 0); + auto target_end = find_character(' ', method_end + 1); + auto query_begin = find_character('?', method_end + 1); + // Strip the query string so the path matches the first regex capture workload. + auto path_end = query_begin < target_end ? query_begin : target_end; + + *method_size = method_end; + *path_size = path_end - method_end - 1; + *version_size = input.size_bytes() - target_end - 6; +} + + +// Each span points at the final character buffer for one output string in this row. Its size came +// from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. +__device__ void write_request_line(cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) { + auto find_character = [&](char needle, int32_t begin) { + for (auto index = begin; index < input.size_bytes(); ++index) { + if (input.data()[index] == needle) { return index; } + } + return input.size_bytes(); + }; + + auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { + for (auto index = begin; index < end; ++index) { + out[index - begin] = input.data()[index]; + } + }; + + auto method_end = find_character(' ', 0); + auto target_end = find_character(' ', method_end + 1); + auto query_begin = find_character('?', method_end + 1); + auto path_end = query_begin < target_end ? query_begin : target_end; + + copy_field(*method, 0, method_end); + copy_field(*path, method_end + 1, path_end); + copy_field(*version, target_end + 6, input.size_bytes()); +} + +#ifdef UDF_COMPUTE_SIZES +extern "C" __device__ auto * transform = & compute_request_line_sizes; +#else +#ifdef UDF_WRITE_OUTPUT +extern "C" __device__ auto * transform =& write_request_line; +#else +#error "Must define either UDF_COMPUTE_SIZES or UDF_WRITE_OUTPUT" +#endif +#endif + diff --git a/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu deleted file mode 100644 index 5d3cc9c990d7..000000000000 --- a/cpp/examples/string_transforms/http_logs/fragments/combined_log_output.cu +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "udf.cuh" - -// The output spans point directly into final allocations, so this fragment only parses delimiters -// and copies bytes—there is no temporary string representation or compaction pass. -extern "C" __device__ int transform(cuda::std::span* client_ip, - cuda::std::span* timestamp, - cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* status, - cuda::std::span* referer, - cuda::std::span* user_agent, - cudf::string_view input) -{ - auto const fields = http_log_udf::parse_combined_log(input); - http_log_udf::copy_field(*client_ip, input, fields.client_ip); - http_log_udf::copy_field(*timestamp, input, fields.timestamp); - http_log_udf::copy_field(*method, input, fields.method); - http_log_udf::copy_field(*path, input, fields.path); - http_log_udf::copy_field(*status, input, fields.status); - http_log_udf::copy_field(*referer, input, fields.referer); - http_log_udf::copy_field(*user_agent, input, fields.user_agent); - return 0; -} diff --git a/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu b/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu deleted file mode 100644 index 72d14464a774..000000000000 --- a/cpp/examples/string_transforms/http_logs/fragments/combined_log_sizes.cu +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "udf.cuh" - -// One output pointer is provided for each eventual string column. Each row writes only byte counts; -// the host scans these columns to build string offsets. -extern "C" __device__ int transform(int32_t* client_ip_size, - int32_t* timestamp_size, - int32_t* method_size, - int32_t* path_size, - int32_t* status_size, - int32_t* referer_size, - int32_t* user_agent_size, - cudf::string_view input) -{ - auto const fields = http_log_udf::parse_combined_log(input); - *client_ip_size = fields.client_ip.size(); - *timestamp_size = fields.timestamp.size(); - *method_size = fields.method.size(); - *path_size = fields.path.size(); - *status_size = fields.status.size(); - *referer_size = fields.referer.size(); - *user_agent_size = fields.user_agent.size(); - return 0; -} diff --git a/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu deleted file mode 100644 index 1d5f03c45caf..000000000000 --- a/cpp/examples/string_transforms/http_logs/fragments/request_line_output.cu +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "udf.cuh" - -// The host supplies spans backed by the final strings-column character buffers. Their lengths were -// computed by the matching sizing fragment. -extern "C" __device__ int transform(cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) -{ - auto const fields = http_log_udf::parse_request_line(input); - http_log_udf::copy_field(*method, input, fields.method); - http_log_udf::copy_field(*path, input, fields.path); - http_log_udf::copy_field(*version, input, fields.version); - return 0; -} diff --git a/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu b/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu deleted file mode 100644 index eccafc6b77bc..000000000000 --- a/cpp/examples/string_transforms/http_logs/fragments/request_line_sizes.cu +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "udf.cuh" - -// AOT fragments expose an unmangled `transform` symbol so transform_lto can link them with the -// precompiled libcudf kernel. Output pointers precede the per-row input in the transform ABI. -extern "C" __device__ int transform(int32_t* method_size, - int32_t* path_size, - int32_t* version_size, - cudf::string_view input) -{ - auto const fields = http_log_udf::parse_request_line(input); - *method_size = fields.method.size(); - *path_size = fields.path.size(); - *version_size = fields.version.size(); - return 0; -} diff --git a/cpp/examples/string_transforms/http_logs/performance.csv b/cpp/examples/string_transforms/http_logs/performance.csv deleted file mode 100644 index c4c8a6860005..000000000000 --- a/cpp/examples/string_transforms/http_logs/performance.csv +++ /dev/null @@ -1,7 +0,0 @@ -operation,variant,cold_mean_s,cold_sd_s,warm_mean_ms,warm_sd_ms,throughput_mean_mrows_s,throughput_sd_mrows_s,effective_bandwidth_mean_gib_s,effective_bandwidth_sd_gib_s,peak_memory_mib,allocated_per_call_mib,warm_speedup_vs_precompiled -request-line,precompiled,0.066313,0.000504,68.416,0.167,14.617,0.035,0.975,0.002,32.00,32.23,1.00 -request-line,jit,1.398848,0.008472,2.574,0.017,388.528,2.554,25.910,0.170,43.32,54.77,26.58 -request-line,lto,1.417205,0.008892,2.618,0.041,381.982,5.925,25.473,0.395,43.32,54.77,26.13 -combined-log,precompiled,0.601166,0.011457,600.868,3.285,1.664,0.009,0.495,0.003,145.13,145.85,1.00 -combined-log,jit,1.554661,0.013899,25.394,0.209,39.382,0.323,11.711,0.096,171.72,198.42,23.66 -combined-log,lto,1.620733,0.055994,25.465,0.166,39.270,0.254,11.677,0.076,171.72,198.42,23.60 diff --git a/cpp/examples/string_transforms/http_logs/performance.md b/cpp/examples/string_transforms/http_logs/performance.md deleted file mode 100644 index 50b6641b0efb..000000000000 --- a/cpp/examples/string_transforms/http_logs/performance.md +++ /dev/null @@ -1,18 +0,0 @@ -# HTTP log transform performance - -Measured on 2026-07-05 with GPU 1 of a dual NVIDIA RTX A6000 system. The GPU was idle before the run. The executable was built in Release mode with CUDA 13.2 and run against the current `multi-string-output` working tree at `2e2a13526f` plus the standalone-build fixes in that tree. - -Each result processes 1,000,000 rows. It is the mean of five independent process runs; each process records one cold call and the mean of ten warm calls. JIT calls use a fresh kernel-cache directory in every process. The `±` values are sample standard deviations. - -| Operation | Variant | Cold (s) | Warm (ms) | Throughput (M rows/s) | Effective bandwidth (GiB/s) | Peak allocation (MiB) | Allocated/call (MiB) | Warm speedup | -|---|---|---:|---:|---:|---:|---:|---:|---:| -| `request-line` | precompiled | 0.066313 ± 0.000504 | 68.416 ± 0.167 | 14.617 ± 0.035 | 0.975 ± 0.002 | 32.00 | 32.23 | 1.00× | -| `request-line` | runtime JIT | 1.398848 ± 0.008472 | 2.574 ± 0.017 | 388.528 ± 2.554 | 25.910 ± 0.170 | 43.32 | 54.77 | 26.58× | -| `request-line` | AOT LTO JIT-linked | 1.417205 ± 0.008892 | 2.618 ± 0.041 | 381.982 ± 5.925 | 25.473 ± 0.395 | 43.32 | 54.77 | 26.13× | -| `combined-log` | precompiled | 0.601166 ± 0.011457 | 600.868 ± 3.285 | 1.664 ± 0.009 | 0.495 ± 0.003 | 145.13 | 145.85 | 1.00× | -| `combined-log` | runtime JIT | 1.554661 ± 0.013899 | 25.394 ± 0.209 | 39.382 ± 0.323 | 11.711 ± 0.096 | 171.72 | 198.42 | 23.66× | -| `combined-log` | AOT LTO JIT-linked | 1.620733 ± 0.055994 | 25.465 ± 0.166 | 39.270 ± 0.254 | 11.677 ± 0.076 | 171.72 | 198.42 | 23.60× | - -Effective bandwidth is `(input bytes + output bytes) / warm time`. Peak allocation and allocated-per-call are reported by the example's tracking memory resource; they are allocation metrics, not total device-resident memory. The precompiled implementation intentionally uses public non-JIT cuDF string and regex functions, while the JIT variants fuse parsing, sizing, and output construction. - -The machine-readable comparison is in [`performance.csv`](performance.csv). diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index 6741a9557248..4a258b371747 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -33,31 +32,9 @@ namespace { -// Every implementation below produces the same output schema. PRECOMPILED uses public regex APIs, -// JIT compiles CUDA source at runtime, and LTO links CUDA fragments compiled during the build. -enum class variant { PRECOMPILED, JIT, LTO }; -enum class operation { REQUEST_LINE, COMBINED_LOG }; - -constexpr auto request_line_output_count = std::size_t{3}; -constexpr auto combined_log_output_count = std::size_t{7}; - -// Each capture group becomes one output column in the public-regex baseline. -constexpr std::string_view request_line_pattern = - R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"; -constexpr std::string_view combined_log_pattern = - R"regex(^([^ ]+) - [^ ]+ \[([^]]+)\] "([A-Z]+) ([^ ?]+)[^ ]* HTTP/[0-9.]+" ([0-9]{3}) [0-9]+ "([^"]*)" "([^"]*)"$)regex"; - -struct options { - std::string input_path; - std::string output_path; - variant implementation; - operation selected_operation; - cudf::size_type rows; - int iterations; -}; - -// Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes -// exact output sizes and another that writes into the resulting character buffers. +constexpr auto output_count = std::size_t{3}; + +// Runtime JIT compilation consumes one CUDA source string for each pass. constexpr char request_line_sizes_udf[] = R"***( // multi_transform calls this function once per input row. Pointer parameters are output columns; // writing a byte count to each one lets the host create exact string offsets before allocation. @@ -72,11 +49,11 @@ __device__ void compute_request_line_sizes(int32_t* method_size, return input.size_bytes(); }; - auto const method_end = find_character(' ', 0); - auto const target_end = find_character(' ', method_end + 1); - auto const query_begin = find_character('?', method_end + 1); + auto method_end = find_character(' ', 0); + auto target_end = find_character(' ', method_end + 1); + auto query_begin = find_character('?', method_end + 1); // Strip the query string so the path matches the first regex capture workload. - auto const path_end = query_begin < target_end ? query_begin : target_end; + auto path_end = query_begin < target_end ? query_begin : target_end; *method_size = method_end; *path_size = path_end - method_end - 1; @@ -104,10 +81,10 @@ __device__ void write_request_line(cuda::std::span* method, } }; - auto const method_end = find_character(' ', 0); - auto const target_end = find_character(' ', method_end + 1); - auto const query_begin = find_character('?', method_end + 1); - auto const path_end = query_begin < target_end ? query_begin : target_end; + auto method_end = find_character(' ', 0); + auto target_end = find_character(' ', method_end + 1); + auto query_begin = find_character('?', method_end + 1); + auto path_end = query_begin < target_end ? query_begin : target_end; copy_field(*method, 0, method_end); copy_field(*path, method_end + 1, path_end); @@ -115,391 +92,108 @@ __device__ void write_request_line(cuda::std::span* method, } )***"; -constexpr char combined_log_sizes_udf[] = R"***( -// The combined-log operation has seven outputs, so the UDF receives seven output pointers followed -// by the input string. The parameter order defines the output-column order. -__device__ void compute_combined_log_sizes(int32_t* client_ip_size, - int32_t* timestamp_size, - int32_t* method_size, - int32_t* path_size, - int32_t* status_size, - int32_t* referer_size, - int32_t* user_agent_size, - cudf::string_view input) { - auto find_character = [&](char needle, int32_t begin) { - for (auto index = begin; index < input.size_bytes(); ++index) { - if (input.data()[index] == needle) { return index; } - } - return input.size_bytes(); - }; - - auto const client_ip_end = find_character(' ', 0); - auto const timestamp_begin = find_character('[', client_ip_end) + 1; - auto const timestamp_end = find_character(']', timestamp_begin); - auto const request_begin = find_character('\"', timestamp_end) + 1; - auto const method_end = find_character(' ', request_begin); - auto const target_end = find_character(' ', method_end + 1); - auto const query_begin = find_character('?', method_end + 1); - auto const path_end = query_begin < target_end ? query_begin : target_end; - auto const request_end = find_character('\"', target_end); - auto const status_begin = request_end + 2; - auto const status_end = find_character(' ', status_begin); - auto const bytes_end = find_character(' ', status_end + 1); - auto const referer_begin = find_character('\"', bytes_end) + 1; - auto const referer_end = find_character('\"', referer_begin); - auto const user_agent_begin = find_character('\"', referer_end + 1) + 1; - auto const user_agent_end = find_character('\"', user_agent_begin); - - // Report exact byte counts; the scan on the host turns these into run-end offsets. - *client_ip_size = client_ip_end; - *timestamp_size = timestamp_end - timestamp_begin; - *method_size = method_end - request_begin; - *path_size = path_end - method_end - 1; - *status_size = status_end - status_begin; - *referer_size = referer_end - referer_begin; - *user_agent_size = user_agent_end - user_agent_begin; -} -)***"; - -constexpr char combined_log_output_udf[] = R"***( -// This second-pass UDF repeats the inexpensive delimiter search, then copies each parsed field into -// the exact final allocation described by the sizing pass. -__device__ void write_combined_log(cuda::std::span* client_ip, - cuda::std::span* timestamp, - cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* status, - cuda::std::span* referer, - cuda::std::span* user_agent, - cudf::string_view input) { - auto find_character = [&](char needle, int32_t begin) { - for (auto index = begin; index < input.size_bytes(); ++index) { - if (input.data()[index] == needle) { return index; } - } - return input.size_bytes(); - }; - - auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { - for (auto index = begin; index < end; ++index) { - out[index - begin] = input.data()[index]; - } - }; - - auto const client_ip_end = find_character(' ', 0); - auto const timestamp_begin = find_character('[', client_ip_end) + 1; - auto const timestamp_end = find_character(']', timestamp_begin); - auto const request_begin = find_character('\"', timestamp_end) + 1; - auto const method_end = find_character(' ', request_begin); - auto const target_end = find_character(' ', method_end + 1); - auto const query_begin = find_character('?', method_end + 1); - auto const path_end = query_begin < target_end ? query_begin : target_end; - auto const request_end = find_character('\"', target_end); - auto const status_begin = request_end + 2; - auto const status_end = find_character(' ', status_begin); - auto const bytes_end = find_character(' ', status_end + 1); - auto const referer_begin = find_character('\"', bytes_end) + 1; - auto const referer_end = find_character('\"', referer_begin); - auto const user_agent_begin = find_character('\"', referer_end + 1) + 1; - auto const user_agent_end = find_character('\"', user_agent_begin); - - copy_field(*client_ip, 0, client_ip_end); - copy_field(*timestamp, timestamp_begin, timestamp_end); - copy_field(*method, request_begin, method_end); - copy_field(*path, method_end + 1, path_end); - copy_field(*status, status_begin, status_end); - copy_field(*referer, referer_begin, referer_end); - copy_field(*user_agent, user_agent_begin, user_agent_end); -} -)***"; - -[[nodiscard]] constexpr std::string_view to_string(variant value) -{ - switch (value) { - case variant::PRECOMPILED: return "precompiled"; - case variant::JIT: return "jit"; - case variant::LTO: return "lto"; - } - throw std::logic_error("Unknown variant"); -} - -[[nodiscard]] constexpr std::string_view to_string(operation value) -{ - switch (value) { - case operation::REQUEST_LINE: return "request-line"; - case operation::COMBINED_LOG: return "combined-log"; - } - throw std::logic_error("Unknown operation"); -} - -[[nodiscard]] variant parse_variant(std::string_view name) -{ - if (name == "precompiled") { return variant::PRECOMPILED; } - if (name == "jit") { return variant::JIT; } - if (name == "lto") { return variant::LTO; } - throw std::invalid_argument("variant must be precompiled, jit, or lto"); -} - -[[nodiscard]] operation parse_operation(std::string_view name) -{ - if (name == "request-line") { return operation::REQUEST_LINE; } - if (name == "combined-log") { return operation::COMBINED_LOG; } - throw std::invalid_argument("operation must be request-line or combined-log"); -} - constexpr std::string_view usage = "usage: http_log_transforms INPUT.csv OUTPUT.csv " - " ROWS ITERATIONS\n" + " ROWS ITERATIONS\n" " http_log_transforms \n"; -[[nodiscard]] options parse_options(int argc, char const** argv) -{ - if (argc != 7) { - throw std::invalid_argument("invalid arguments; run http_log_transforms --help for usage"); - } - - auto const implementation = parse_variant(argv[3]); - auto const selected_operation = parse_operation(argv[4]); - - auto const rows = std::stoll(argv[5]); - auto const iterations = std::stoi(argv[6]); - if (rows < 0 || rows > std::numeric_limits::max()) { - throw std::invalid_argument("ROWS is outside the cudf::size_type range"); - } - if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } - return {argv[1], - argv[2], - implementation, - selected_operation, - static_cast(rows), - iterations}; -} - -[[nodiscard]] constexpr std::size_t output_count(operation selected_operation) -{ - return selected_operation == operation::REQUEST_LINE ? request_line_output_count - : combined_log_output_count; -} - -[[nodiscard]] constexpr cudf::size_type input_column_index(operation selected_operation) +[[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { - return selected_operation == operation::REQUEST_LINE ? 0 : 1; -} - -[[nodiscard]] char const* sizing_udf(operation selected_operation) -{ - return selected_operation == operation::REQUEST_LINE ? request_line_sizes_udf - : combined_log_sizes_udf; -} - -[[nodiscard]] char const* output_udf(operation selected_operation) -{ - return selected_operation == operation::REQUEST_LINE ? request_line_output_udf - : combined_log_output_udf; -} - -[[nodiscard]] std::size_t sizing_fragment(operation selected_operation) -{ - return selected_operation == operation::REQUEST_LINE ? http_log_fragments::request_line_sizes - : http_log_fragments::combined_log_sizes; -} - -[[nodiscard]] std::size_t output_fragment(operation selected_operation) -{ - return selected_operation == operation::REQUEST_LINE ? http_log_fragments::request_line_output - : http_log_fragments::combined_log_output; -} - -[[nodiscard]] std::vector make_output_specs(std::size_t count, - cudf::type_id type) -{ - // The example data is well-formed, and both parsers always write every field, so no output needs - // a null mask. - auto const spec = - cudf::transform_output{cudf::data_type{type}, cudf::output_nullability::ALL_VALID}; - return std::vector(count, spec); -} - -[[nodiscard]] std::span get_fragment(std::size_t id) -{ - // rtcx_embed concatenates all AOT fatbins into one byte array. file_ranges identifies the slice - // belonging to the requested sizing or output fragment. - auto const range = http_log_fragments::file_ranges[id]; - return http_log_fragments::files.subspan(range[0], range[1]); -} - -[[nodiscard]] std::unique_ptr make_string_offsets( - cudf::column_view const string_sizes, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // A strings column uses N+1 offsets. Scanning the N string sizes produces every run end; adding - // a leading zero supplies the first offset. - auto run_ends = cudf::scan(string_sizes, - *cudf::make_sum_aggregation(), - cudf::scan_type::INCLUSIVE, - cudf::null_policy::EXCLUDE, - stream, - mr); - auto const zero_offset = cudf::numeric_scalar{0, true, stream, mr}; - auto first_offset = cudf::make_column_from_scalar(zero_offset, 1, stream, mr); - return cudf::concatenate( - std::vector{first_offset->view(), run_ends->view()}, stream, mr); -} - -[[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, - operation selected_operation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // This public cuDF regex implementation is the non-JIT comparison baseline. - if (selected_operation == operation::REQUEST_LINE) { - static auto const program = cudf::strings::regex_program::create(request_line_pattern); - return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); - } - static auto const program = cudf::strings::regex_program::create(combined_log_pattern); + static auto program = + cudf::strings::regex_program::create(R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"); return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); } -[[nodiscard]] std::unique_ptr compute_string_sizes(cudf::column_view input, - operation selected_operation, - variant implementation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) +[[nodiscard]] std::unique_ptr run_jit(cudf::column_view input, + bool use_lto, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) { - // Pass 1: produce one INT32 byte-count column for each eventual string output. - auto const outputs = make_output_specs(output_count(selected_operation), cudf::type_id::INT32); + // Pass 1 produces one byte-count column for each eventual string output. + cudf::transform_output const size_spec{cudf::data_type{cudf::type_id::INT32}, + cudf::output_nullability::ALL_VALID}; + std::vector const size_outputs(output_count, size_spec); cudf::transform_input inputs[] = {input}; - if (implementation == variant::JIT) { - // Compile the human-readable CUDA source on first use, then reuse the cached kernel. - return cudf::multi_transform(sizing_udf(selected_operation), - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - {}, - std::nullopt, - stream, - mr); + std::unique_ptr sizes; + + if (use_lto) { + auto range = http_log_fragments::file_ranges[http_log_fragments::request_line_sizes]; + auto fragment = http_log_fragments::files.subspan(range[0], range[1]); + + sizes = cudf::transform_lto(fragment, + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + size_outputs, + {}, + std::nullopt, + stream, + mr); + } else { + sizes = cudf::multi_transform(request_line_sizes_udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + size_outputs, + {}, + std::nullopt, + stream, + mr); } - // Link the build-time-compiled fatbin with libcudf's transform kernel at runtime. - return cudf::transform_lto(get_fragment(sizing_fragment(selected_operation)), - cudf::lto_binary_type::FATBIN, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - {}, - std::nullopt, - stream, - mr); -} - -[[nodiscard]] std::vector> make_all_string_offsets( - cudf::table_view const size_columns, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ + // Inclusive scans turn the sizes into the offsets needed for the final strings columns. std::vector> offsets; - offsets.reserve(size_columns.num_columns()); - for (auto const& string_sizes : size_columns) { - offsets.push_back(make_string_offsets(string_sizes, stream, mr)); + offsets.reserve(output_count); + + for (auto& string_sizes : sizes->view()) { + auto run_ends = cudf::scan(string_sizes, + *cudf::make_sum_aggregation(), + cudf::scan_type::INCLUSIVE, + cudf::null_policy::EXCLUDE, + stream, + mr); + + auto zero = cudf::numeric_scalar{0, true, stream, mr}; + auto first = cudf::make_column_from_scalar(zero, 1, stream, mr); + offsets.push_back(cudf::concatenate( + std::vector{first->view(), run_ends->view()}, stream, mr)); } - return offsets; -} - -[[nodiscard]] std::unique_ptr write_strings( - cudf::column_view input, - operation selected_operation, - variant implementation, - std::vector>&& string_offsets, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // Pass 2: use the precomputed offsets to write directly into final strings columns. - auto const outputs = make_output_specs(output_count(selected_operation), cudf::type_id::STRING); - cudf::transform_input inputs[] = {input}; - - if (implementation == variant::JIT) { - // Supplying offsets avoids the usual temporary string_view output and compaction step. - return cudf::multi_transform(output_udf(selected_operation), - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - std::move(string_offsets), - std::nullopt, - stream, - mr); - } - - // The AOT path uses the same offsets and output ABI; only the UDF representation differs. - return cudf::transform_lto(get_fragment(output_fragment(selected_operation)), - cudf::lto_binary_type::FATBIN, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - std::move(string_offsets), - std::nullopt, - stream, - mr); -} - -[[nodiscard]] std::unique_ptr run_two_pass(cudf::column_view input, - operation selected_operation, - variant implementation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // Pass 1 computes the exact byte count of every output string. Inclusive scans turn those sizes - // into run-end offsets, allowing pass 2 to write directly into the final character buffers. - auto sizes = compute_string_sizes(input, selected_operation, implementation, stream, mr); - auto offsets = make_all_string_offsets(sizes->view(), stream, mr); - return write_strings(input, selected_operation, implementation, std::move(offsets), stream, mr); -} -[[nodiscard]] std::unique_ptr run_transform(cudf::column_view input, - operation selected_operation, - variant implementation, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - if (implementation == variant::PRECOMPILED) { - return run_regex(input, selected_operation, stream, mr); + // Pass 2 writes directly into final character buffers described by those offsets. + cudf::transform_output const output_spec{cudf::data_type{cudf::type_id::STRING}, + cudf::output_nullability::ALL_VALID}; + std::vector const outputs(output_count, output_spec); + + if (use_lto) { + auto range = http_log_fragments::file_ranges[http_log_fragments::request_line_output]; + auto fragment = http_log_fragments::files.subspan(range[0], range[1]); + return cudf::transform_lto(fragment, + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + std::move(offsets), + std::nullopt, + stream, + mr); } - return run_two_pass(input, selected_operation, implementation, stream, mr); -} - -[[nodiscard]] std::vector output_column_names(operation selected_operation) -{ - if (selected_operation == operation::REQUEST_LINE) { return {"method", "path", "http_version"}; } - return {"client_ip", "timestamp", "method", "path", "status", "referer", "user_agent"}; -} - -void write_output(cudf::table_view const result, - operation selected_operation, - std::string const& output_path) -{ - auto options = cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result) - .include_header(true) - .names(output_column_names(selected_operation)) - .build(); - cudf::io::write_csv(options); -} - -[[nodiscard]] std::unique_ptr read_input(options const& opts) -{ - auto read_options = - cudf::io::csv_reader_options::builder(cudf::io::source_info{opts.input_path}).header(0).build(); - auto input = cudf::io::read_csv(read_options).tbl; - if (opts.rows == input->num_rows()) { return input; } - // Sampling with replacement scales the small checked-in dataset to the requested benchmark size. - return cudf::sample(input->view(), opts.rows, cudf::sample_with_replacement::TRUE); + return cudf::multi_transform(request_line_output_udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + std::move(offsets), + std::nullopt, + stream, + mr); } } // namespace @@ -513,66 +207,99 @@ int main(int argc, char const** argv) return EXIT_SUCCESS; } - auto const opts = parse_options(argc, argv); - auto const stream = cudf::get_default_stream(); - auto const mr = cudf::get_current_device_resource_ref(); + if (argc != 6) { + throw std::invalid_argument("invalid arguments; run http_log_transforms --help for usage"); + } + + auto input_path = std::string{argv[1]}; + auto output_path = std::string{argv[2]}; + auto implementation = std::string_view{argv[3]}; + if (implementation != "precompiled" && implementation != "jit" && implementation != "lto") { + throw std::invalid_argument("variant must be precompiled, jit, or lto"); + } + + auto requested_rows = std::stoll(argv[4]); + auto iterations = std::stoi(argv[5]); + if (requested_rows < 0 || requested_rows > std::numeric_limits::max()) { + throw std::invalid_argument("ROWS is outside the cudf::size_type range"); + } + + if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } + + auto rows = static_cast(requested_rows); + auto is_precompiled = implementation == "precompiled"; + auto use_lto = implementation == "lto"; + auto stream = cudf::get_default_stream(); + auto mr = cudf::get_current_device_resource_ref(); + + auto read_options = cudf::io::csv_reader_options::builder(cudf::io::source_info{input_path}) + .header(0) + .use_cols_names({"RequestLine"}) + .build(); + auto input = cudf::io::read_csv(read_options).tbl; + if (rows != input->num_rows()) { + // Sampling with replacement scales the small checked-in dataset to the requested size. + input = cudf::sample(input->view(), rows, cudf::sample_with_replacement::TRUE); + } - auto input = read_input(opts); - auto const input_index = input_column_index(opts.selected_operation); - auto const input_bytes = input->get_column(input_index).alloc_size(); - auto const input_view = input->get_column(input_index).view(); + auto input_bytes = input->get_column(0).alloc_size(); + auto input_view = input->get_column(0).view(); // Track allocations made by the transforms without changing the application's upstream memory // resource. rmm::mr::statistics_resource_adaptor stats{mr}; - auto const stats_mr = rmm::device_async_resource_ref{stats}; + auto stats_mr = rmm::device_async_resource_ref{stats}; stream.synchronize(); // The cold measurement includes regex setup or JIT compilation/linking performed on first use. - auto const cold_start = std::chrono::steady_clock::now(); + auto cold_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_cold"); - auto cold_result = - run_transform(input_view, opts.selected_operation, opts.implementation, stream, stats_mr); + auto cold_result = is_precompiled ? run_precompiled(input_view, stream, stats_mr) + : run_jit(input_view, use_lto, stream, stats_mr); stream.synchronize(); nvtxRangePop(); - auto const cold_seconds = + auto cold_seconds = std::chrono::duration{std::chrono::steady_clock::now() - cold_start}.count(); cold_result.reset(); std::unique_ptr result; // Subsequent calls exercise the cached kernel and represent steady-state throughput. - auto const warm_start = std::chrono::steady_clock::now(); + auto warm_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_warm"); - for (auto i = 0; i < opts.iterations; ++i) { + for (auto i = 0; i < iterations; ++i) { result.reset(); - result = - run_transform(input_view, opts.selected_operation, opts.implementation, stream, stats_mr); + result = is_precompiled ? run_precompiled(input_view, stream, stats_mr) + : run_jit(input_view, use_lto, stream, stats_mr); } stream.synchronize(); nvtxRangePop(); - auto const warm_seconds = + auto warm_seconds = std::chrono::duration{std::chrono::steady_clock::now() - warm_start}.count() / - opts.iterations; + iterations; // A dash suppresses CSV output so file I/O does not affect benchmark runs. - if (opts.output_path != "-") { - write_output(result->view(), opts.selected_operation, opts.output_path); + if (output_path != "-") { + auto write_options = + cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result->view()) + .include_header(true) + .names({"method", "path", "http_version"}) + .build(); + cudf::io::write_csv(write_options); } - auto const bytes = stats.get_bytes_counter(); - auto const output_bytes = result->alloc_size(); - auto const gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); + auto bytes = stats.get_bytes_counter(); + auto output_bytes = result->alloc_size(); + auto gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); - std::cout << std::fixed << std::setprecision(9) - << "RESULT variant=" << to_string(opts.implementation) - << " operation=" << to_string(opts.selected_operation) << " rows=" << opts.rows - << " cold_seconds=" << cold_seconds << " warm_seconds=" << warm_seconds - << " rows_per_second=" << static_cast(opts.rows) / warm_seconds + std::cout << std::fixed << std::setprecision(9) << "RESULT variant=" << implementation + << " rows=" << rows << " cold_seconds=" << cold_seconds + << " warm_seconds=" << warm_seconds + << " rows_per_second=" << static_cast(rows) / warm_seconds << " effective_gib_per_second=" << gib / warm_seconds << " input_bytes=" << input_bytes << " output_bytes=" << output_bytes << " peak_memory_bytes=" << bytes.peak << " total_allocated_bytes=" << bytes.total << " allocated_bytes_per_call=" - << bytes.total / static_cast(opts.iterations + 1) << '\n'; + << bytes.total / static_cast(iterations + 1) << '\n'; return EXIT_SUCCESS; } catch (std::exception const& error) { std::cerr << error.what() << '\n'; diff --git a/cpp/examples/string_transforms/http_logs/udf.cuh b/cpp/examples/string_transforms/http_logs/udf.cuh deleted file mode 100644 index 15c49f2f01a1..000000000000 --- a/cpp/examples/string_transforms/http_logs/udf.cuh +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#include -#include - -namespace http_log_udf { - -// A field is represented as a half-open byte range into the original log line. Keeping ranges -// avoids copying data while the parser discovers all output fields. -struct field_range { - int32_t begin{}; - int32_t end{}; - - [[nodiscard]] __device__ int32_t size() const { return end - begin; } -}; - -struct request_line_fields { - field_range method; - field_range path; - field_range version; -}; - -struct combined_log_fields { - field_range client_ip; - field_range timestamp; - field_range method; - field_range path; - field_range status; - field_range referer; - field_range user_agent; -}; - -[[nodiscard]] __device__ int32_t find_character(cudf::string_view const input, - char const needle, - int32_t begin) -{ - // These example formats are ASCII-delimited, so byte offsets are also valid character offsets. - for (auto i = begin; i < input.size_bytes(); ++i) { - if (input.data()[i] == needle) { return i; } - } - return input.size_bytes(); -} - -[[nodiscard]] __device__ request_line_fields parse_request_line(cudf::string_view const input) -{ - // Expected form: METHOD /path?optional-query HTTP/version - auto const method_end = find_character(input, ' ', 0); - auto const target_end = find_character(input, ' ', method_end + 1); - auto const query_begin = find_character(input, '?', method_end + 1); - auto const path_end = query_begin < target_end ? query_begin : target_end; - constexpr int32_t http_prefix_size = 6; // " HTTP/" - - return {{0, method_end}, - {method_end + 1, path_end}, - {target_end + http_prefix_size, input.size_bytes()}}; -} - -[[nodiscard]] __device__ combined_log_fields parse_combined_log(cudf::string_view const input) -{ - // Walk the delimiters once and retain only ranges for the seven fields emitted by the example. - // The checked-in input is well-formed, so validation and malformed-row handling are intentionally - // outside the scope of this transform demonstration. - auto const ip_end = find_character(input, ' ', 0); - auto const timestamp_begin = find_character(input, '[', ip_end) + 1; - auto const timestamp_end = find_character(input, ']', timestamp_begin); - auto const request_begin = find_character(input, '"', timestamp_end) + 1; - auto const method_end = find_character(input, ' ', request_begin); - auto const target_end = find_character(input, ' ', method_end + 1); - auto const query_begin = find_character(input, '?', method_end + 1); - auto const path_end = query_begin < target_end ? query_begin : target_end; - auto const request_end = find_character(input, '"', target_end); - auto const status_begin = request_end + 2; - auto const status_end = find_character(input, ' ', status_begin); - auto const bytes_end = find_character(input, ' ', status_end + 1); - auto const referer_begin = find_character(input, '"', bytes_end) + 1; - auto const referer_end = find_character(input, '"', referer_begin); - auto const user_agent_begin = find_character(input, '"', referer_end + 1) + 1; - auto const user_agent_end = find_character(input, '"', user_agent_begin); - - return {{0, ip_end}, - {timestamp_begin, timestamp_end}, - {request_begin, method_end}, - {method_end + 1, path_end}, - {status_begin, status_end}, - {referer_begin, referer_end}, - {user_agent_begin, user_agent_end}}; -} - -__device__ void copy_field(cuda::std::span output, - cudf::string_view const input, - field_range const field) -{ - // output is a view into the final chars child allocated from the sizing pass offsets. - for (auto i = int32_t{0}; i < field.size(); ++i) { - output[i] = input.data()[field.begin + i]; - } -} - -} // namespace http_log_udf From fbf3a6775bdc281f1f9eef6039ab18a2e06fa55f Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 14 Jul 2026 16:44:43 +0000 Subject: [PATCH 09/27] pre-commit --- cpp/examples/string_transforms/CMakeLists.txt | 41 +++++++++---------- .../string_transforms/http_logs/fragments.cu | 17 ++++---- .../http_logs/transforms.cpp | 2 +- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 5ec9167ecad3..931a639cd148 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -1,5 +1,5 @@ # cmake-format: off -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # cmake-format: on @@ -43,8 +43,9 @@ if(NOT TARGET xxhash) find_path(XXHASH_INCLUDE_DIR NAMES xxhash.h REQUIRED) add_library(xxhash UNKNOWN IMPORTED) set_target_properties( - xxhash PROPERTIES IMPORTED_LOCATION "${XXHASH_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${XXHASH_INCLUDE_DIR}") + xxhash PROPERTIES IMPORTED_LOCATION "${XXHASH_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES + "${XXHASH_INCLUDE_DIR}" + ) endif() include(${CMAKE_CURRENT_LIST_DIR}/../../librtcx/embed.cmake) @@ -80,25 +81,23 @@ add_string_transforms_example(localize_phone_jit localize_phone_jit.cpp) add_string_transforms_example(localize_phone_precompiled localize_phone_precompiled.cpp) rtcx_add_embed(http_log_fragments) -add_fragment(http_log_fragments FRAGMENT request_line_sizes SOURCE - http_logs/fragments.cu - DEFINITIONS UDF_COMPUTE_SIZES - INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs) -add_fragment(http_log_fragments FRAGMENT request_line_output SOURCE - http_logs/fragments.cu - DEFINITIONS UDF_WRITE_OUTPUT - INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs) -rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY - "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed") +add_fragment( + http_log_fragments FRAGMENT request_line_sizes SOURCE http_logs/fragments.cu DEFINITIONS + UDF_COMPUTE_SIZES INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs +) +add_fragment( + http_log_fragments FRAGMENT request_line_output SOURCE http_logs/fragments.cu DEFINITIONS + UDF_WRITE_OUTPUT INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs +) +rtcx_embed( + http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" +) add_string_transforms_example(http_log_transforms http_logs/transforms.cpp) -target_sources( - http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}/http_log_fragments.s -) -target_include_directories(http_log_transforms - PRIVATE ${http_log_fragments_SOURCE_DIR}) +target_sources(http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}/http_log_fragments.s) +target_include_directories(http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}) add_dependencies(http_log_transforms http_log_fragments) -install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv - ${CMAKE_CURRENT_LIST_DIR}/http_logs/logs.csv - DESTINATION bin/examples/libcudf/string_transformers) +install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv ${CMAKE_CURRENT_LIST_DIR}/http_logs/logs.csv + DESTINATION bin/examples/libcudf/string_transformers +) diff --git a/cpp/examples/string_transforms/http_logs/fragments.cu b/cpp/examples/string_transforms/http_logs/fragments.cu index cc59f05315b4..0dadbde7998c 100644 --- a/cpp/examples/string_transforms/http_logs/fragments.cu +++ b/cpp/examples/string_transforms/http_logs/fragments.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -10,13 +10,13 @@ #include #include - // Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes // exact output sizes and another that writes into the resulting character buffers. __device__ void compute_request_line_sizes(int32_t* method_size, int32_t* path_size, int32_t* version_size, - cudf::string_view input) { + cudf::string_view input) +{ auto find_character = [&](char needle, int32_t begin) { for (auto index = begin; index < input.size_bytes(); ++index) { if (input.data()[index] == needle) { return index; } @@ -28,20 +28,20 @@ __device__ void compute_request_line_sizes(int32_t* method_size, auto target_end = find_character(' ', method_end + 1); auto query_begin = find_character('?', method_end + 1); // Strip the query string so the path matches the first regex capture workload. - auto path_end = query_begin < target_end ? query_begin : target_end; + auto path_end = query_begin < target_end ? query_begin : target_end; *method_size = method_end; *path_size = path_end - method_end - 1; *version_size = input.size_bytes() - target_end - 6; } - // Each span points at the final character buffer for one output string in this row. Its size came // from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. __device__ void write_request_line(cuda::std::span* method, cuda::std::span* path, cuda::std::span* version, - cudf::string_view input) { + cudf::string_view input) +{ auto find_character = [&](char needle, int32_t begin) { for (auto index = begin; index < input.size_bytes(); ++index) { if (input.data()[index] == needle) { return index; } @@ -66,12 +66,11 @@ __device__ void write_request_line(cuda::std::span* method, } #ifdef UDF_COMPUTE_SIZES -extern "C" __device__ auto * transform = & compute_request_line_sizes; +extern "C" __device__ auto* transform = &compute_request_line_sizes; #else #ifdef UDF_WRITE_OUTPUT -extern "C" __device__ auto * transform =& write_request_line; +extern "C" __device__ auto* transform = &write_request_line; #else #error "Must define either UDF_COMPUTE_SIZES or UDF_WRITE_OUTPUT" #endif #endif - diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index 4a258b371747..b01d71c7c392 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ From 82cd2440983b4d9e317126faff51643b4f4f9e3e Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 14 Jul 2026 17:50:19 +0000 Subject: [PATCH 10/27] finish up --- cpp/cmake/Modules/AddFragment.cmake | 19 +++++---- cpp/examples/string_transforms/CMakeLists.txt | 36 +++-------------- .../string_transforms/http_logs/fragments.cu | 39 ++++++++++++++----- .../http_logs/transforms.cpp | 24 +++++++----- 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index 4f1e8ebaff1d..9d21574c518b 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -13,7 +13,7 @@ include_guard(GLOBAL) # final library with metadata that allows it to be looked up at runtime. macro(add_fragment) set(TARGET ${ARGV0}) - set(ONE_VALUE_ARGS FRAGMENT SOURCE KERNEL_ONLY KERNEL_INSTANCE UDF_TYPE) + set(ONE_VALUE_ARGS FRAGMENT SOURCE KERNEL_ONLY KERNEL_INSTANCE UDF_TYPE LINK_CUDF_DEPS) set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES INCLUDE_DIRS) cmake_parse_arguments(ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) @@ -71,12 +71,17 @@ macro(add_fragment) CUDA_STANDARD_REQUIRED ON CUDA_VISIBILITY_PRESET hidden ) - target_link_libraries( - ${OBJECT_ID} - PUBLIC CCCL::CCCL rapids_logger::rapids_logger rmm::rmm $ - PRIVATE $ $ - ZLIB::ZLIB nvcomp::nvcomp kvikio::kvikio nanoarrow::nanoarrow zstd - ) + + if(ARG_LINK_CUDF_DEPS) + target_link_libraries( + ${OBJECT_ID} + PUBLIC CCCL::CCCL rapids_logger::rapids_logger rmm::rmm + $ + PRIVATE $ $ + ZLIB::ZLIB nvcomp::nvcomp kvikio::kvikio nanoarrow::nanoarrow zstd + ) + endif() + target_include_directories( ${OBJECT_ID} PRIVATE "$" "$" diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 931a639cd148..1eb7a6cdd128 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -21,34 +21,10 @@ include(../fetch_dependencies.cmake) include(rapids-cmake) rapids_cmake_build_type("Release") -# Build and embed the AOT CUDA fragments consumed by cudf::transform_lto. -find_package(zstd CONFIG REQUIRED) -find_package(bs_thread_pool CONFIG REQUIRED) -find_package(ZLIB REQUIRED) -find_package(nvcomp CONFIG REQUIRED) -find_package(kvikio CONFIG REQUIRED) -if(NOT TARGET cuco::cuco) - add_library(cuco::cuco INTERFACE IMPORTED) -endif() -if(NOT TARGET nanoarrow::nanoarrow) - add_library(nanoarrow::nanoarrow INTERFACE IMPORTED) -endif() -if(NOT TARGET zstd) - add_library(zstd INTERFACE) - target_link_libraries(zstd INTERFACE zstd::libzstd) -endif() - -if(NOT TARGET xxhash) - find_library(XXHASH_LIBRARY NAMES xxhash REQUIRED) - find_path(XXHASH_INCLUDE_DIR NAMES xxhash.h REQUIRED) - add_library(xxhash UNKNOWN IMPORTED) - set_target_properties( - xxhash PROPERTIES IMPORTED_LOCATION "${XXHASH_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES - "${XXHASH_INCLUDE_DIR}" - ) -endif() - -include(${CMAKE_CURRENT_LIST_DIR}/../../librtcx/embed.cmake) +set(CUDF_EXCLUDE_DEPS_FROM_ALL OFF) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_zstd.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_xxhash.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_rtcx.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/Modules/AddFragment.cmake) set(CUDF_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") set(CUDF_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") @@ -83,11 +59,11 @@ add_string_transforms_example(localize_phone_precompiled localize_phone_precompi rtcx_add_embed(http_log_fragments) add_fragment( http_log_fragments FRAGMENT request_line_sizes SOURCE http_logs/fragments.cu DEFINITIONS - UDF_COMPUTE_SIZES INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs + UDF_COMPUTE_SIZES INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs LINK_CUDF_DEPS OFF ) add_fragment( http_log_fragments FRAGMENT request_line_output SOURCE http_logs/fragments.cu DEFINITIONS - UDF_WRITE_OUTPUT INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs + UDF_WRITE_OUTPUT INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs LINK_CUDF_DEPS OFF ) rtcx_embed( http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" diff --git a/cpp/examples/string_transforms/http_logs/fragments.cu b/cpp/examples/string_transforms/http_logs/fragments.cu index 0dadbde7998c..9f261ddc22c9 100644 --- a/cpp/examples/string_transforms/http_logs/fragments.cu +++ b/cpp/examples/string_transforms/http_logs/fragments.cu @@ -12,10 +12,10 @@ // Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes // exact output sizes and another that writes into the resulting character buffers. -__device__ void compute_request_line_sizes(int32_t* method_size, - int32_t* path_size, - int32_t* version_size, - cudf::string_view input) +__device__ int compute_request_line_sizes(int32_t* method_size, + int32_t* path_size, + int32_t* version_size, + cudf::string_view input) { auto find_character = [&](char needle, int32_t begin) { for (auto index = begin; index < input.size_bytes(); ++index) { @@ -33,14 +33,17 @@ __device__ void compute_request_line_sizes(int32_t* method_size, *method_size = method_end; *path_size = path_end - method_end - 1; *version_size = input.size_bytes() - target_end - 6; + + // return 0 to indicate success + return 0; } // Each span points at the final character buffer for one output string in this row. Its size came // from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. -__device__ void write_request_line(cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) +__device__ int write_request_line(cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) { auto find_character = [&](char needle, int32_t begin) { for (auto index = begin; index < input.size_bytes(); ++index) { @@ -63,13 +66,29 @@ __device__ void write_request_line(cuda::std::span* method, copy_field(*method, 0, method_end); copy_field(*path, method_end + 1, path_end); copy_field(*version, target_end + 6, input.size_bytes()); + + // return 0 to indicate success + return 0; } +// The symbol `transform` is the entry point for cudf::transform_lto. #ifdef UDF_COMPUTE_SIZES -extern "C" __device__ auto* transform = &compute_request_line_sizes; +extern "C" __device__ int transform(int32_t* method_size, + int32_t* path_size, + int32_t* version_size, + cudf::string_view input) +{ + return compute_request_line_sizes(method_size, path_size, version_size, input); +} #else #ifdef UDF_WRITE_OUTPUT -extern "C" __device__ auto* transform = &write_request_line; +extern "C" __device__ int transform(cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) +{ + return write_request_line(method, path, version, input); +} #else #error "Must define either UDF_COMPUTE_SIZES or UDF_WRITE_OUTPUT" #endif diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index b01d71c7c392..b5a5081430fb 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -291,15 +291,21 @@ int main(int argc, char const** argv) auto output_bytes = result->alloc_size(); auto gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); - std::cout << std::fixed << std::setprecision(9) << "RESULT variant=" << implementation - << " rows=" << rows << " cold_seconds=" << cold_seconds - << " warm_seconds=" << warm_seconds - << " rows_per_second=" << static_cast(rows) / warm_seconds - << " effective_gib_per_second=" << gib / warm_seconds - << " input_bytes=" << input_bytes << " output_bytes=" << output_bytes - << " peak_memory_bytes=" << bytes.peak << " total_allocated_bytes=" << bytes.total - << " allocated_bytes_per_call=" - << bytes.total / static_cast(iterations + 1) << '\n'; + std::cout << std::format( + "variant={}\nrows={}\ncold_seconds={}\nwarm_seconds={}\nrows_per_second={}\neffective_gib_" + "per_second={}\ninput_bytes={}\noutput_bytes={}\npeak_memory_bytes={}\ntotal_allocated_bytes=" + "{}\nallocated_bytes_per_call={}\n", + implementation, + rows, + cold_seconds, + warm_seconds, + static_cast(rows) / warm_seconds, + gib / warm_seconds, + input_bytes, + output_bytes, + bytes.peak, + bytes.total, + bytes.total / static_cast(iterations + 1)); return EXIT_SUCCESS; } catch (std::exception const& error) { std::cerr << error.what() << '\n'; From 9f364ecff2c2a6ba9017c0bb4dc980c7b5336819 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 14 Jul 2026 20:29:28 +0100 Subject: [PATCH 11/27] update --- cpp/cmake/Modules/AddFragment.cmake | 4 ++++ cpp/src/strings/extract/extract.cu | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index 9d21574c518b..67c49f30609b 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -17,6 +17,10 @@ macro(add_fragment) set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES INCLUDE_DIRS) cmake_parse_arguments(ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) + if(NOT DEFINED ARG_LINK_CUDF_DEPS) + set(ARG_LINK_CUDF_DEPS ON) + endif() + if(NOT ARG_FRAGMENT) message(FATAL_ERROR "add_fragment requires FRAGMENT argument") endif() diff --git a/cpp/src/strings/extract/extract.cu b/cpp/src/strings/extract/extract.cu index 5f03ca912095..b643968c6bb2 100644 --- a/cpp/src/strings/extract/extract.cu +++ b/cpp/src/strings/extract/extract.cu @@ -85,7 +85,7 @@ std::unique_ptr extract(strings_column_view const& input, auto const groups = d_prog->group_counts(); CUDF_EXPECTS(groups > 0, "Group indicators not found in regex pattern"); - auto indices = rmm::device_uvector(input.size() * groups, stream); + auto indices = rmm::device_uvector(input.size() * groups, stream, mr); auto d_indices = cudf::detail::device_2dspan(indices, groups); auto const d_strings = column_device_view::create(input.parent(), stream); From dc401dc4cdd9fcaf7560e01f1fd4f1fb4d9c68e5 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 14 Jul 2026 20:00:05 +0000 Subject: [PATCH 12/27] finish up --- cpp/src/strings/extract/extract.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/strings/extract/extract.cu b/cpp/src/strings/extract/extract.cu index b643968c6bb2..1a7573b2a8a1 100644 --- a/cpp/src/strings/extract/extract.cu +++ b/cpp/src/strings/extract/extract.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ From 97b478627b9ca9f58b388f1061dd9eb239976150 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 14 Jul 2026 21:58:25 +0100 Subject: [PATCH 13/27] update --- .../string_transforms/http_logs/fragments.cu | 66 ++++++++++---- .../http_logs/transforms.cpp | 90 +++++++++++++------ 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/cpp/examples/string_transforms/http_logs/fragments.cu b/cpp/examples/string_transforms/http_logs/fragments.cu index 9f261ddc22c9..860f3c805dce 100644 --- a/cpp/examples/string_transforms/http_logs/fragments.cu +++ b/cpp/examples/string_transforms/http_logs/fragments.cu @@ -17,22 +17,39 @@ __device__ int compute_request_line_sizes(int32_t* method_size, int32_t* version_size, cudf::string_view input) { - auto find_character = [&](char needle, int32_t begin) { - for (auto index = begin; index < input.size_bytes(); ++index) { - if (input.data()[index] == needle) { return index; } + // Initialize output sizes to zero in case of early return. + *method_size = 0; + *path_size = 0; + *version_size = 0; + + auto n = input.size_bytes(); + + auto find_character = [&](int32_t begin, char needle) { + for (auto i = begin; i < n; ++i) { + if (input.data()[i] == needle) { return i; } } - return input.size_bytes(); + return n; }; - auto method_end = find_character(' ', 0); - auto target_end = find_character(' ', method_end + 1); - auto query_begin = find_character('?', method_end + 1); - // Strip the query string so the path matches the first regex capture workload. + auto method_end = find_character(0, ' '); + if (method_end == n) { return 0; } + + auto target_end = find_character(method_end + 1, ' '); + if (target_end == n || n - target_end < 6) { return 0; } + if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || + input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || + input.data()[target_end + 5] != '/') { + return 0; + } + + auto query_begin = find_character(method_end + 1, '?'); + + // The path ends at the query or the target, whichever comes first. auto path_end = query_begin < target_end ? query_begin : target_end; *method_size = method_end; *path_size = path_end - method_end - 1; - *version_size = input.size_bytes() - target_end - 6; + *version_size = n - target_end - 6; // return 0 to indicate success return 0; @@ -45,11 +62,13 @@ __device__ int write_request_line(cuda::std::span* method, cuda::std::span* version, cudf::string_view input) { - auto find_character = [&](char needle, int32_t begin) { - for (auto index = begin; index < input.size_bytes(); ++index) { - if (input.data()[index] == needle) { return index; } + auto n = input.size_bytes(); + + auto find_character = [&](int32_t begin, char needle) { + for (auto i = begin; i < n; ++i) { + if (input.data()[i] == needle) { return i; } } - return input.size_bytes(); + return n; }; auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { @@ -58,14 +77,25 @@ __device__ int write_request_line(cuda::std::span* method, } }; - auto method_end = find_character(' ', 0); - auto target_end = find_character(' ', method_end + 1); - auto query_begin = find_character('?', method_end + 1); - auto path_end = query_begin < target_end ? query_begin : target_end; + auto method_end = find_character(0, ' '); + if (method_end == n) { return 0; } + + auto target_end = find_character(method_end + 1, ' '); + if (target_end == n || n - target_end < 6) { return 0; } + if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || + input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || + input.data()[target_end + 5] != '/') { + return 0; + } + + auto query_begin = find_character(method_end + 1, '?'); + + // The path ends at the query or the target, whichever comes first. + auto path_end = query_begin < target_end ? query_begin : target_end; copy_field(*method, 0, method_end); copy_field(*path, method_end + 1, path_end); - copy_field(*version, target_end + 6, input.size_bytes()); + copy_field(*version, target_end + 6, n); // return 0 to indicate success return 0; diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index b5a5081430fb..d9518fd5d32d 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -38,41 +38,63 @@ constexpr auto output_count = std::size_t{3}; constexpr char request_line_sizes_udf[] = R"***( // multi_transform calls this function once per input row. Pointer parameters are output columns; // writing a byte count to each one lets the host create exact string offsets before allocation. -__device__ void compute_request_line_sizes(int32_t* method_size, - int32_t* path_size, - int32_t* version_size, - cudf::string_view input) { - auto find_character = [&](char needle, int32_t begin) { - for (auto index = begin; index < input.size_bytes(); ++index) { - if (input.data()[index] == needle) { return index; } +__device__ int compute_request_line_sizes(int32_t* method_size, + int32_t* path_size, + int32_t* version_size, + cudf::string_view input) { + // Initialize output sizes to zero in case of early return. + *method_size = 0; + *path_size = 0; + *version_size = 0; + + auto n = input.size_bytes(); + + auto find_character = [&](int32_t begin, char needle) { + for (auto i = begin; i < n; ++i) { + if (input.data()[i] == needle) { return i; } } - return input.size_bytes(); + return n; }; - auto method_end = find_character(' ', 0); - auto target_end = find_character(' ', method_end + 1); - auto query_begin = find_character('?', method_end + 1); - // Strip the query string so the path matches the first regex capture workload. - auto path_end = query_begin < target_end ? query_begin : target_end; + auto method_end = find_character(0, ' '); + if (method_end == n) { return 0; } + + auto target_end = find_character(method_end + 1, ' '); + if (target_end == n || n - target_end < 6) { return 0; } + if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || + input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || + input.data()[target_end + 5] != '/') { + return 0; + } + + auto query_begin = find_character(method_end + 1, '?'); + + // The path ends at the query or the target, whichever comes first. + auto path_end = query_begin < target_end ? query_begin : target_end; *method_size = method_end; *path_size = path_end - method_end - 1; - *version_size = input.size_bytes() - target_end - 6; + *version_size = n - target_end - 6; + + // return 0 to indicate success + return 0; } )***"; constexpr char request_line_output_udf[] = R"***( // Each span points at the final character buffer for one output string in this row. Its size came // from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. -__device__ void write_request_line(cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) { - auto find_character = [&](char needle, int32_t begin) { - for (auto index = begin; index < input.size_bytes(); ++index) { - if (input.data()[index] == needle) { return index; } +__device__ int write_request_line(cuda::std::span* method, + cuda::std::span* path, + cuda::std::span* version, + cudf::string_view input) { + auto n = input.size_bytes(); + + auto find_character = [&](int32_t begin, char needle) { + for (auto i = begin; i < n; ++i) { + if (input.data()[i] == needle) { return i; } } - return input.size_bytes(); + return n; }; auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { @@ -81,14 +103,28 @@ __device__ void write_request_line(cuda::std::span* method, } }; - auto method_end = find_character(' ', 0); - auto target_end = find_character(' ', method_end + 1); - auto query_begin = find_character('?', method_end + 1); - auto path_end = query_begin < target_end ? query_begin : target_end; + auto method_end = find_character(0, ' '); + if (method_end == n) { return 0; } + + auto target_end = find_character(method_end + 1, ' '); + if (target_end == n || n - target_end < 6) { return 0; } + if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || + input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || + input.data()[target_end + 5] != '/') { + return 0; + } + + auto query_begin = find_character(method_end + 1, '?'); + + // The path ends at the query or the target, whichever comes first. + auto path_end = query_begin < target_end ? query_begin : target_end; copy_field(*method, 0, method_end); copy_field(*path, method_end + 1, path_end); - copy_field(*version, target_end + 6, input.size_bytes()); + copy_field(*version, target_end + 6, n); + + // return 0 to indicate success + return 0; } )***"; From f9411a9bfed56197f5a56ea836df74e92bccbf7b Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 21 Jul 2026 18:50:05 +0000 Subject: [PATCH 14/27] update --- cpp/CMakeLists.txt | 160 +++++++++++++++++++++------- cpp/cmake/Modules/AddFragment.cmake | 21 ++-- cpp/tests/CMakeLists.txt | 22 ++-- 3 files changed, 145 insertions(+), 58 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6bfea599ee1f..6a73b32384dd 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -496,44 +496,15 @@ rtcx_embed( rtcx_add_embed(cudf_fragments) -list(APPEND CUDF_PRECOMPILE_PHYSICAL_TYPES uint8_t uint16_t uint32_t uint64_t numeric::decimal32 - numeric::decimal64 numeric::decimal128 -) - -foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) - set(FRAGMENT_NAME transform_kernel) - get_property( - FILE_INDEX - TARGET cudf_fragments__embed_props - PROPERTY EMBED_FILE_INDEX - ) - set(VARIANT_NAME transform_kernel_${FILE_INDEX}) - set(INSTANCE - "cudf::jit::transform_kernel>, cudf::jit::type_list>>" - ) - add_fragment( - cudf_fragments - FRAGMENT - ${VARIANT_NAME} - SOURCE - src/transform/jit/kernel.cu - KERNEL_INSTANCE - ${INSTANCE} - UDF_TYPE - "int(${TYPE} *, ${TYPE})" - DEFINITIONS - CUDF_LTO_MODE - ARRAY_IDS - ${FRAGMENT_NAME}_FILE_INDEX - ${FRAGMENT_NAME}_INSTANCE - ARRAY_VALUES - ${FILE_INDEX} - "${INSTANCE}" +# This function precompiles the unary and binary transform kernel fragments for all fixed-width +# types. This helps amortize the cost of JIT compilation for kernels that have matching signatures +# and reduces the amount of time spent on source-based runtime JIT compilation for LTO-based JIT. +function(precompile_fixed_width_kernel_fragments) + list(APPEND CUDF_PRECOMPILE_PHYSICAL_TYPES uint8_t uint16_t uint32_t uint64_t numeric::decimal32 + numeric::decimal64 numeric::decimal128 ) -endforeach() - -foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) - foreach(RHS_IS_SCALAR IN ITEMS "false" "true") + # Pre-compile unary-op kernel fragments for fixed-width types. + foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) set(FRAGMENT_NAME transform_kernel) get_property( FILE_INDEX @@ -542,10 +513,11 @@ foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) ) set(VARIANT_NAME transform_kernel_${FILE_INDEX}) set(INSTANCE - "cudf::jit::transform_kernel, cudf::jit::column_accessor<1ULL, cudf::column_device_view_core, ${TYPE}, ${RHS_IS_SCALAR}, 0>>, cudf::jit::type_list>>" + "cudf::jit::transform_kernel>, cudf::jit::type_list>>" ) add_fragment( cudf_fragments + LINK_CUDF_DEPS FRAGMENT ${VARIANT_NAME} SOURCE @@ -553,7 +525,7 @@ foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) KERNEL_INSTANCE ${INSTANCE} UDF_TYPE - "int(${TYPE} *, ${TYPE}, ${TYPE})" + "int(${TYPE} *, ${TYPE})" DEFINITIONS CUDF_LTO_MODE ARRAY_IDS @@ -564,7 +536,115 @@ foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) "${INSTANCE}" ) endforeach() -endforeach() + + # Precompile binary-op kernel fragments for fixed-width types. To minimize binary size impact, + # only the RHS-scalar variants are precompiled. + foreach(TYPE IN ITEMS ${CUDF_PRECOMPILE_PHYSICAL_TYPES}) + foreach(RHS_IS_SCALAR IN ITEMS "false" "true") + set(FRAGMENT_NAME transform_kernel) + get_property( + FILE_INDEX + TARGET cudf_fragments__embed_props + PROPERTY EMBED_FILE_INDEX + ) + set(VARIANT_NAME transform_kernel_${FILE_INDEX}) + set(INSTANCE + "cudf::jit::transform_kernel, cudf::jit::column_accessor<1ULL, cudf::column_device_view_core, ${TYPE}, ${RHS_IS_SCALAR}, 0>>, cudf::jit::type_list>>" + ) + add_fragment( + cudf_fragments + LINK_CUDF_DEPS + FRAGMENT + ${VARIANT_NAME} + SOURCE + src/transform/jit/kernel.cu + KERNEL_INSTANCE + ${INSTANCE} + UDF_TYPE + "int(${TYPE} *, ${TYPE}, ${TYPE})" + DEFINITIONS + CUDF_LTO_MODE + ARRAY_IDS + ${FRAGMENT_NAME}_FILE_INDEX + ${FRAGMENT_NAME}_INSTANCE + ARRAY_VALUES + ${FILE_INDEX} + "${INSTANCE}" + ) + endforeach() + endforeach() +endfunction() + +# This function precompiles kernel fragments for string transforms. It precompiles kernel fragments +# for common string transform operations: multi-extract, multi-split, string-parsing. +# This helps amortize the cost of JIT compilation for kernels that have matching signatures +function(precompile_string_kernel_fragments) + list(APPEND CUDF_PRECOMPILE_STRING_OUTPUT_PHYSICAL_TYPES uint8_t uint16_t uint32_t uint64_t + cuda::std::span + ) + list( + APPEND + CUDF_PRECOMPILE_STRING_OUTPUT_COLUMN_TYPES + cudf::mutable_column_device_view_core + cudf::mutable_column_device_view_core + cudf::mutable_column_device_view_core + cudf::mutable_column_device_view_core + cudf::jit::mutable_strings_column_device_view + ) + foreach(OUTPUT_TYPE OUTPUT_COLUMN_TYPE IN ZIP_LISTS CUDF_PRECOMPILE_STRING_OUTPUT_PHYSICAL_TYPES + CUDF_PRECOMPILE_STRING_OUTPUT_COLUMN_TYPES + ) + foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3" "0;1;2;3;4" "0;1;2;3;4;5" + "0;1;2;3;4;5;6" "0;1;2;3;4;5;6;7" + ) + set(FRAGMENT_NAME transform_kernel) + get_property( + FILE_INDEX + TARGET cudf_fragments__embed_props + PROPERTY EMBED_FILE_INDEX + ) + set(VARIANT_NAME transform_kernel_${FILE_INDEX}) + set(OUTPUT_ACCESSORS "") + set(OUTPUT_POINTERS "") + foreach(OUTPUT_INDEX IN LISTS OUTPUT_INDICES) + list( + APPEND + OUTPUT_ACCESSORS + "cudf::jit::column_accessor<${OUTPUT_INDEX}ULL, ${OUTPUT_COLUMN_TYPE}, ${OUTPUT_TYPE}, false, 0>" + ) + list(APPEND OUTPUT_POINTERS "${OUTPUT_TYPE} *") + endforeach() + list(JOIN OUTPUT_ACCESSORS "," OUTPUT_ACCESSORS_STR) + list(JOIN OUTPUT_POINTERS "," OUTPUT_POINTERS_STR) + set(INSTANCE + "cudf::jit::transform_kernel>, cudf::jit::type_list<${OUTPUT_ACCESSORS_STR}>>" + ) + add_fragment( + cudf_fragments + LINK_CUDF_DEPS + FRAGMENT + ${VARIANT_NAME} + SOURCE + src/transform/jit/kernel.cu + KERNEL_INSTANCE + ${INSTANCE} + UDF_TYPE + "int(${OUTPUT_POINTERS_STR}, cudf::string_view)" + DEFINITIONS + CUDF_LTO_MODE + ARRAY_IDS + ${FRAGMENT_NAME}_FILE_INDEX + ${FRAGMENT_NAME}_INSTANCE + ARRAY_VALUES + ${FILE_INDEX} + "${INSTANCE}" + ) + endforeach() + endforeach() +endfunction() + +precompile_fixed_width_kernel_fragments() +precompile_string_kernel_fragments() rtcx_embed( cudf_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index 67c49f30609b..0c55a2a281b2 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -13,14 +13,11 @@ include_guard(GLOBAL) # final library with metadata that allows it to be looked up at runtime. macro(add_fragment) set(TARGET ${ARGV0}) - set(ONE_VALUE_ARGS FRAGMENT SOURCE KERNEL_ONLY KERNEL_INSTANCE UDF_TYPE LINK_CUDF_DEPS) + set(OPTIONS LINK_CUDF_DEPS) + set(ONE_VALUE_ARGS FRAGMENT SOURCE KERNEL_ONLY KERNEL_INSTANCE UDF_TYPE) set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES INCLUDE_DIRS) cmake_parse_arguments(ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) - if(NOT DEFINED ARG_LINK_CUDF_DEPS) - set(ARG_LINK_CUDF_DEPS ON) - endif() - if(NOT ARG_FRAGMENT) message(FATAL_ERROR "add_fragment requires FRAGMENT argument") endif() @@ -76,7 +73,7 @@ macro(add_fragment) CUDA_VISIBILITY_PRESET hidden ) - if(ARG_LINK_CUDF_DEPS) + if(DEFINED ARG_LINK_CUDF_DEPS AND ARG_LINK_CUDF_DEPS) target_link_libraries( ${OBJECT_ID} PUBLIC CCCL::CCCL rapids_logger::rapids_logger rmm::rmm @@ -84,13 +81,13 @@ macro(add_fragment) PRIVATE $ $ ZLIB::ZLIB nvcomp::nvcomp kvikio::kvikio nanoarrow::nanoarrow zstd ) - endif() - target_include_directories( - ${OBJECT_ID} PRIVATE "$" - "$" - ) - target_compile_options(${OBJECT_ID} PRIVATE "$<$:${CUDF_CUDA_FLAGS}>") + target_include_directories( + ${OBJECT_ID} PRIVATE "$" + "$" + ) + target_compile_options(${OBJECT_ID} PRIVATE "$<$:${CUDF_CUDA_FLAGS}>") + endif() rtcx_embed_blob( ${TARGET} FILE $ DEST fragments/${ARG_FRAGMENT}.fatbin ID diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index ff214d35562a..4480512b00e5 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -697,20 +697,30 @@ ConfigureTest(AST_TEST ast/transform_tests.cpp ast/ast_tree_tests.cpp ast/jit_ex rtcx_add_embed(cudf_test_fragments) add_fragment( - cudf_test_fragments FRAGMENT bankers_rounding SOURCE transform/fragments/bankers_rounding.cu + cudf_test_fragments LINK_CUDF_DEPS FRAGMENT bankers_rounding SOURCE + transform/fragments/bankers_rounding.cu ) -add_fragment(cudf_test_fragments FRAGMENT distance SOURCE transform/fragments/distance.cu) +add_fragment( + cudf_test_fragments LINK_CUDF_DEPS FRAGMENT distance SOURCE transform/fragments/distance.cu +) -add_fragment(cudf_test_fragments FRAGMENT invsqrt SOURCE transform/fragments/invsqrt.cu) +add_fragment( + cudf_test_fragments LINK_CUDF_DEPS FRAGMENT invsqrt SOURCE transform/fragments/invsqrt.cu +) -add_fragment(cudf_test_fragments FRAGMENT lehmer_mean SOURCE transform/fragments/lehmer_mean.cu) +add_fragment( + cudf_test_fragments LINK_CUDF_DEPS FRAGMENT lehmer_mean SOURCE transform/fragments/lehmer_mean.cu +) add_fragment( - cudf_test_fragments FRAGMENT sum_of_squares SOURCE transform/fragments/sum_of_squares.cu + cudf_test_fragments LINK_CUDF_DEPS FRAGMENT sum_of_squares SOURCE + transform/fragments/sum_of_squares.cu ) -add_fragment(cudf_test_fragments FRAGMENT to_upper SOURCE transform/fragments/to_upper.cu) +add_fragment( + cudf_test_fragments LINK_CUDF_DEPS FRAGMENT to_upper SOURCE transform/fragments/to_upper.cu +) rtcx_embed( cudf_test_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" From 3327295ef9f984a090b5c939eee49abf98f0f10b Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 21 Jul 2026 21:14:24 +0000 Subject: [PATCH 15/27] refactoring --- cpp/CMakeLists.txt | 48 ++++++++++-- cpp/benchmarks/CMakeLists.txt | 28 ++++++- cpp/cmake/Modules/AddFragment.cmake | 35 ++++----- cpp/examples/string_transforms/CMakeLists.txt | 6 +- .../http_logs/transforms.cpp | 2 +- cpp/src/jit/cache.hpp | 2 +- cpp/src/transform/transform.cu | 17 +++-- cpp/tests/CMakeLists.txt | 76 +++++++++++++++++-- 8 files changed, 166 insertions(+), 48 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 530815bd32e0..12f8e2396dbb 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -442,6 +442,24 @@ if(NOT BUILD_SHARED_LIBS) endif() endif() +set(LIBCUDF_FRAGMENT_LINK_LIBRARIES + CCCL::CCCL + rapids_logger::rapids_logger + rmm::rmm + $ + $ + $ + ZLIB::ZLIB + nvcomp::nvcomp + kvikio::kvikio + nanoarrow::nanoarrow + zstd +) +set(LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES "$" + "$" +) +set(LIBCUDF_FRAGMENT_COMPILE_OPTIONS "$<$:${CUDF_CUDA_FLAGS}>") + rtcx_add_embed(cudf_cuda_embed) rtcx_embed_includes( @@ -517,7 +535,6 @@ function(precompile_fixed_width_kernel_fragments) ) add_fragment( cudf_fragments - LINK_CUDF_DEPS FRAGMENT ${VARIANT_NAME} SOURCE @@ -534,7 +551,14 @@ function(precompile_fixed_width_kernel_fragments) ARRAY_VALUES ${FILE_INDEX} "${INSTANCE}" + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} ) + endforeach() # Precompile binary-op kernel fragments for fixed-width types. To minimize binary size impact, @@ -553,7 +577,6 @@ function(precompile_fixed_width_kernel_fragments) ) add_fragment( cudf_fragments - LINK_CUDF_DEPS FRAGMENT ${VARIANT_NAME} SOURCE @@ -570,14 +593,20 @@ function(precompile_fixed_width_kernel_fragments) ARRAY_VALUES ${FILE_INDEX} "${INSTANCE}" + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} ) endforeach() endforeach() endfunction() # This function precompiles kernel fragments for string transforms. It precompiles kernel fragments -# for common string transform operations: multi-extract, multi-split, string-parsing. -# This helps amortize the cost of JIT compilation for kernels that have matching signatures +# for common string transform operations: multi-extract, multi-split, string-parsing. This helps +# amortize the cost of JIT compilation for kernels that have matching signatures function(precompile_string_kernel_fragments) list(APPEND CUDF_PRECOMPILE_STRING_OUTPUT_PHYSICAL_TYPES uint8_t uint16_t uint32_t uint64_t cuda::std::span @@ -614,14 +643,13 @@ function(precompile_string_kernel_fragments) ) list(APPEND OUTPUT_POINTERS "${OUTPUT_TYPE} *") endforeach() - list(JOIN OUTPUT_ACCESSORS "," OUTPUT_ACCESSORS_STR) - list(JOIN OUTPUT_POINTERS "," OUTPUT_POINTERS_STR) + list(JOIN OUTPUT_ACCESSORS " ," OUTPUT_ACCESSORS_STR) + list(JOIN OUTPUT_POINTERS " ," OUTPUT_POINTERS_STR) set(INSTANCE "cudf::jit::transform_kernel>, cudf::jit::type_list<${OUTPUT_ACCESSORS_STR}>>" ) add_fragment( cudf_fragments - LINK_CUDF_DEPS FRAGMENT ${VARIANT_NAME} SOURCE @@ -638,6 +666,12 @@ function(precompile_string_kernel_fragments) ARRAY_VALUES ${FILE_INDEX} "${INSTANCE}" + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} ) endforeach() endforeach() diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 1b57e3b23666..4fd17b54a7c1 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -394,8 +394,32 @@ ConfigureNVBench(AST_NVBENCH ast/polynomials.cpp ast/transform.cpp) # ################################################################################################## # * LTO Fragments ---------------------------------------------------------------------------- rtcx_add_embed(cudf_benchmark_fragments) -add_fragment(cudf_benchmark_fragments FRAGMENT add_f32 SOURCE binaryop/fragments/add_f32.cu) -add_fragment(cudf_benchmark_fragments FRAGMENT mul_f32 SOURCE binaryop/fragments/mul_f32.cu) +add_fragment( + cudf_benchmark_fragments + FRAGMENT + add_f32 + SOURCE + binaryop/fragments/add_f32.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} +) +add_fragment( + cudf_benchmark_fragments + FRAGMENT + mul_f32 + SOURCE + binaryop/fragments/mul_f32.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} +) rtcx_embed( cudf_benchmark_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index 0c55a2a281b2..e6ff042d3f9f 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -13,9 +13,11 @@ include_guard(GLOBAL) # final library with metadata that allows it to be looked up at runtime. macro(add_fragment) set(TARGET ${ARGV0}) - set(OPTIONS LINK_CUDF_DEPS) + set(OPTIONS) set(ONE_VALUE_ARGS FRAGMENT SOURCE KERNEL_ONLY KERNEL_INSTANCE UDF_TYPE) - set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES INCLUDE_DIRS) + set(MULTI_VALUE_ARGS DEFINITIONS ARRAY_IDS ARRAY_VALUES INCLUDE_DIRECTORIES LINK_LIBRARIES + COMPILE_OPTIONS + ) cmake_parse_arguments(ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) if(NOT ARG_FRAGMENT) @@ -55,9 +57,18 @@ macro(add_fragment) endif() target_compile_definitions(${OBJECT_ID} PRIVATE CUDF_DISABLE_EXPORTS ${ARG_DEFINITIONS}) - if(ARG_INCLUDE_DIRS) - target_include_directories(${OBJECT_ID} PRIVATE ${ARG_INCLUDE_DIRS}) + if(ARG_INCLUDE_DIRECTORIES) + target_include_directories(${OBJECT_ID} PRIVATE ${ARG_INCLUDE_DIRECTORIES}) + endif() + + if(ARG_LINK_LIBRARIES) + target_link_libraries(${OBJECT_ID} PRIVATE ${ARG_LINK_LIBRARIES}) endif() + + if(ARG_COMPILE_OPTIONS) + target_compile_options(${OBJECT_ID} PRIVATE ${ARG_COMPILE_OPTIONS}) + endif() + set_target_properties( ${OBJECT_ID} PROPERTIES CUDA_SEPARABLE_COMPILATION ON @@ -73,22 +84,6 @@ macro(add_fragment) CUDA_VISIBILITY_PRESET hidden ) - if(DEFINED ARG_LINK_CUDF_DEPS AND ARG_LINK_CUDF_DEPS) - target_link_libraries( - ${OBJECT_ID} - PUBLIC CCCL::CCCL rapids_logger::rapids_logger rmm::rmm - $ - PRIVATE $ $ - ZLIB::ZLIB nvcomp::nvcomp kvikio::kvikio nanoarrow::nanoarrow zstd - ) - - target_include_directories( - ${OBJECT_ID} PRIVATE "$" - "$" - ) - target_compile_options(${OBJECT_ID} PRIVATE "$<$:${CUDF_CUDA_FLAGS}>") - endif() - rtcx_embed_blob( ${TARGET} FILE $ DEST fragments/${ARG_FRAGMENT}.fatbin ID ${ARG_FRAGMENT} ARRAY_IDS ${ARG_ARRAY_IDS} ARRAY_VALUES ${ARG_ARRAY_VALUES} diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 1eb7a6cdd128..615f174a5973 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -21,7 +21,6 @@ include(../fetch_dependencies.cmake) include(rapids-cmake) rapids_cmake_build_type("Release") -set(CUDF_EXCLUDE_DEPS_FROM_ALL OFF) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_zstd.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_xxhash.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_rtcx.cmake) @@ -59,11 +58,12 @@ add_string_transforms_example(localize_phone_precompiled localize_phone_precompi rtcx_add_embed(http_log_fragments) add_fragment( http_log_fragments FRAGMENT request_line_sizes SOURCE http_logs/fragments.cu DEFINITIONS - UDF_COMPUTE_SIZES INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs LINK_CUDF_DEPS OFF + UDF_COMPUTE_SIZES + INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/http_logs ) add_fragment( http_log_fragments FRAGMENT request_line_output SOURCE http_logs/fragments.cu DEFINITIONS - UDF_WRITE_OUTPUT INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/http_logs LINK_CUDF_DEPS OFF + UDF_WRITE_OUTPUT INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/http_logs ) rtcx_embed( http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index d9518fd5d32d..9d5c991872ae 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include #include diff --git a/cpp/src/jit/cache.hpp b/cpp/src/jit/cache.hpp index 16696f3d0a71..7a4125a8975c 100644 --- a/cpp/src/jit/cache.hpp +++ b/cpp/src/jit/cache.hpp @@ -64,7 +64,7 @@ struct [[nodiscard]] kernel { rtcx::cuda_dim3 block_dim, uint32_t shared_mem_bytes, rmm::cuda_stream_view stream, - Args&&... args) + Args&&... args) const requires(sizeof...(Args) > 0) { void const* params[] = {&args...}; // NOLINT(modernize-avoid-c-arrays) diff --git a/cpp/src/transform/transform.cu b/cpp/src/transform/transform.cu index a34b8389a774..612f2567c99b 100644 --- a/cpp/src/transform/transform.cu +++ b/cpp/src/transform/transform.cu @@ -201,12 +201,11 @@ void launch(cudf::kernel const& kernel, rmm::cuda_stream_view stream) { CUDF_FUNC_RANGE(); - void* args[] = {&row_size, &stencil, &user_data, &input_cols, &output_cols, &max_error}; auto cfg = kernel.max_occupancy_config(0, 0); CUDF_EXPECTS(cfg.block_size % cudf::detail::warp_size == 0, "Expected block size to be a multiple of warp size", std::runtime_error); - kernel.launch({cfg.min_grid_size}, {cfg.block_size}, 0, stream, args); + kernel.launch_with({cfg.min_grid_size}, {cfg.block_size}, 0, stream, row_size, stencil, user_data, input_cols, output_cols, max_error); } std::string get_element_type_name(column_view const& view, bool use_physical_type); @@ -389,8 +388,7 @@ std::string reflect_udf_signature(bool is_null_aware, { std::vector in_types; - for (size_t i = 0; i < inputs.size(); i++) { - auto& in = inputs[i]; + for (auto& in : inputs) { auto element = std::visit([&](auto& c) { return reflect_input_element(c, use_physical_types); }, in); in_types.push_back(is_null_aware ? std::format("cuda::std::optional<{}>", element) : element); @@ -398,8 +396,7 @@ std::string reflect_udf_signature(bool is_null_aware, std::vector out_types; - for (size_t i = 0; i < outputs.size(); i++) { - auto& out = outputs[i]; + for (auto& out : outputs) { auto element = std::visit([&](auto& c) { return reflect_output_element(c, use_physical_types); }, out); out_types.push_back(is_null_aware ? std::format("cuda::std::optional<{}> *", element) @@ -407,14 +404,18 @@ std::string reflect_udf_signature(bool is_null_aware, } std::vector params; - if (has_user_data) { params.push_back("void*"); } + if (has_user_data) { + params.emplace_back("void*"); + params.emplace_back("cudf::size_type"); + } params.insert(params.end(), out_types.begin(), out_types.end()); params.insert(params.end(), in_types.begin(), in_types.end()); auto joined = params.empty() ? "" - : std::accumulate(std::next(params.begin()), params.end(), params[0], [](auto a, auto b) { + : std::accumulate( + std::next(params.begin()), params.end(), params[0], [](auto const& a, auto const& b) { return std::format("{}, {}", a, b); }); diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index cbaa276def4a..2c8774a5eeba 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -698,29 +698,93 @@ ConfigureTest(AST_TEST ast/transform_tests.cpp ast/ast_tree_tests.cpp ast/jit_ex rtcx_add_embed(cudf_test_fragments) add_fragment( - cudf_test_fragments LINK_CUDF_DEPS FRAGMENT bankers_rounding SOURCE + cudf_test_fragments + LINK_CUDF_DEPS + FRAGMENT + bankers_rounding + SOURCE transform/fragments/bankers_rounding.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) add_fragment( - cudf_test_fragments LINK_CUDF_DEPS FRAGMENT distance SOURCE transform/fragments/distance.cu + cudf_test_fragments + LINK_CUDF_DEPS + FRAGMENT + distance + SOURCE + transform/fragments/distance.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) add_fragment( - cudf_test_fragments LINK_CUDF_DEPS FRAGMENT invsqrt SOURCE transform/fragments/invsqrt.cu + cudf_test_fragments + LINK_CUDF_DEPS + FRAGMENT + invsqrt + SOURCE + transform/fragments/invsqrt.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) add_fragment( - cudf_test_fragments LINK_CUDF_DEPS FRAGMENT lehmer_mean SOURCE transform/fragments/lehmer_mean.cu + cudf_test_fragments + LINK_CUDF_DEPS + FRAGMENT + lehmer_mean + SOURCE + transform/fragments/lehmer_mean.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) add_fragment( - cudf_test_fragments LINK_CUDF_DEPS FRAGMENT sum_of_squares SOURCE + cudf_test_fragments + LINK_CUDF_DEPS + FRAGMENT + sum_of_squares + SOURCE transform/fragments/sum_of_squares.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) add_fragment( - cudf_test_fragments LINK_CUDF_DEPS FRAGMENT to_upper SOURCE transform/fragments/to_upper.cu + cudf_test_fragments + LINK_CUDF_DEPS + FRAGMENT + to_upper + SOURCE + transform/fragments/to_upper.cu + LINK_LIBRARIES + ${LIBCUDF_FRAGMENT_LINK_LIBRARIES} + INCLUDE_DIRECTORIES + ${LIBCUDF_FRAGMENT_INCLUDE_DIRECTORIES} + COMPILE_OPTIONS + ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) rtcx_embed( From d2961b3931b2a87c36ee26027937bc363cc44c1e Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Tue, 21 Jul 2026 23:53:46 +0000 Subject: [PATCH 16/27] Refactoring + code review changes --- cpp/CMakeLists.txt | 18 +--- cpp/benchmarks/CMakeLists.txt | 3 +- cpp/cmake/Modules/AddFragment.cmake | 2 +- cpp/examples/string_transforms/CMakeLists.txt | 37 +++++-- cpp/examples/string_transforms/README.md | 5 +- .../string_transforms/http_logs/fragments.cu | 51 ++++------ .../http_logs/transforms.cpp | 97 ++++++++++++------- cpp/src/strings/extract/extract.cu | 2 +- cpp/src/transform/transform.cu | 7 +- cpp/tests/CMakeLists.txt | 2 +- 10 files changed, 123 insertions(+), 101 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 12f8e2396dbb..49ac9400747d 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -209,10 +209,6 @@ set_property( ) message(VERBOSE "CUDF: LIBCUDF_LOGGING_LEVEL = '${LIBCUDF_LOGGING_LEVEL}'.") -if(NOT CUDF_GENERATED_INCLUDE_DIR) - set(CUDF_GENERATED_INCLUDE_DIR ${CUDF_BINARY_DIR}) -endif() - # ################################################################################################## # * linter configuration --------------------------------------------------------------------------- if(CUDF_CLANG_TIDY) @@ -508,9 +504,7 @@ foreach(INC_DIR IN LISTS LIBCUDACXX_RAW_INCLUDE_DIRS) ) endforeach() -rtcx_embed( - cudf_cuda_embed COMPRESSION zstd OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" -) +rtcx_embed(cudf_cuda_embed COMPRESSION zstd OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed") rtcx_add_embed(cudf_fragments) @@ -623,9 +617,8 @@ function(precompile_string_kernel_fragments) foreach(OUTPUT_TYPE OUTPUT_COLUMN_TYPE IN ZIP_LISTS CUDF_PRECOMPILE_STRING_OUTPUT_PHYSICAL_TYPES CUDF_PRECOMPILE_STRING_OUTPUT_COLUMN_TYPES ) - foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3" "0;1;2;3;4" "0;1;2;3;4;5" - "0;1;2;3;4;5;6" "0;1;2;3;4;5;6;7" - ) + # Pre-compile for up to 4 output columns + foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3") set(FRAGMENT_NAME transform_kernel) get_property( FILE_INDEX @@ -680,9 +673,7 @@ endfunction() precompile_fixed_width_kernel_fragments() precompile_string_kernel_fragments() -rtcx_embed( - cudf_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" -) +rtcx_embed(cudf_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed") # ################################################################################################## # * library targets ------------------------------------------------------------------------------- @@ -1318,7 +1309,6 @@ target_compile_options( target_include_directories( cudf PUBLIC "$" "$" - "$" PRIVATE "$" "$" "$" diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index 4fd17b54a7c1..f05d0c560226 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -421,8 +421,7 @@ add_fragment( ${LIBCUDF_FRAGMENT_COMPILE_OPTIONS} ) rtcx_embed( - cudf_benchmark_fragments COMPRESSION none OUTPUT_DIRECTORY - "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" + cudf_benchmark_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed" ) # ################################################################################################## diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index e6ff042d3f9f..d41669b91a44 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -37,7 +37,7 @@ macro(add_fragment) target_compile_options(${OBJECT_ID} PRIVATE -Xnvlink=--kernels-used=cudf_kernel_entry) endif() - set(INSTANTIATION_DIR "${CUDF_GENERATED_INCLUDE_DIR}/${TARGET}/instantiations/${ARG_FRAGMENT}") + set(INSTANTIATION_DIR "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}/instantiations/${ARG_FRAGMENT}") target_include_directories(${OBJECT_ID} PRIVATE ${INSTANTIATION_DIR}) if(ARG_KERNEL_INSTANCE) diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 615f174a5973..909e0278b5f3 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -21,12 +21,13 @@ include(../fetch_dependencies.cmake) include(rapids-cmake) rapids_cmake_build_type("Release") +# Fetch librtcx and its dependencies include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_zstd.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_xxhash.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_rtcx.cmake) + +# Include the helper function for embedding FATBINs of fragments include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/Modules/AddFragment.cmake) -set(CUDF_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../..") -set(CUDF_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") # For now, disable CMake's automatic module scanning for C++ files. There is an sccache bug in the # version RAPIDS uses in CI that causes it to handle the resulting -M* flags incorrectly with @@ -55,19 +56,35 @@ add_string_transforms_example(format_phone_precompiled format_phone_precompiled. add_string_transforms_example(localize_phone_jit localize_phone_jit.cpp) add_string_transforms_example(localize_phone_precompiled localize_phone_precompiled.cpp) +# Compile and embed the UDFs so we can link them at runtime rtcx_add_embed(http_log_fragments) add_fragment( - http_log_fragments FRAGMENT request_line_sizes SOURCE http_logs/fragments.cu DEFINITIONS - UDF_COMPUTE_SIZES - INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/http_logs + http_log_fragments + FRAGMENT + request_line_sizes + SOURCE + http_logs/fragments.cu + DEFINITIONS + UDF_COMPUTE_SIZES + INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_LIST_DIR}/http_logs + LINK_LIBRARIES + cudf::cudf ) add_fragment( - http_log_fragments FRAGMENT request_line_output SOURCE http_logs/fragments.cu DEFINITIONS - UDF_WRITE_OUTPUT INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/http_logs -) -rtcx_embed( - http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" + http_log_fragments + FRAGMENT + request_line_output + SOURCE + http_logs/fragments.cu + DEFINITIONS + UDF_WRITE_OUTPUT + INCLUDE_DIRECTORIES + ${CMAKE_CURRENT_LIST_DIR}/http_logs + LINK_LIBRARIES + cudf::cudf ) +rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed") add_string_transforms_example(http_log_transforms http_logs/transforms.cpp) target_sources(http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}/http_log_fragments.s) diff --git a/cpp/examples/string_transforms/README.md b/cpp/examples/string_transforms/README.md index 3b68e94e8568..68503a01887f 100644 --- a/cpp/examples/string_transforms/README.md +++ b/cpp/examples/string_transforms/README.md @@ -15,8 +15,9 @@ The following examples are included: 5. `extract_email_precompiled` - Performs same transformation on the table as `output` but uses precompiled public APIs 6. `format_phone_jit` - Using a transform kernel to output a string to a pre-allocated buffer 7. `format_phone_precompiled` - Performs same transformation on the table as `preallocated` but uses precompiled public APIs -8. `http_log_transforms` - Compares three multi-output HTTP log extractors: - - `precompiled`: `cudf::strings::extract` with a public regex program. +8. `http_log_transforms` - Compares four multi-output HTTP log extractors: + - `regex`: `cudf::strings::extract` with a regex program. + - `precompiled`: public string partition and slice APIs, without regular expressions. - `jit`: two CUDA source transforms compiled at runtime. The first produces exact per-row string sizes; inclusive scans turn those sizes into run-end offsets, and the second writes directly to the resulting string character buffers. diff --git a/cpp/examples/string_transforms/http_logs/fragments.cu b/cpp/examples/string_transforms/http_logs/fragments.cu index 860f3c805dce..18d6fa74e636 100644 --- a/cpp/examples/string_transforms/http_logs/fragments.cu +++ b/cpp/examples/string_transforms/http_logs/fragments.cu @@ -3,15 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -#pragma once - #include +#include #include #include -// Runtime JIT compilation consumes CUDA source strings. Each operation has one UDF that computes -// exact output sizes and another that writes into the resulting character buffers. +__device__ int32_t find_character(cudf::string_view input, int32_t begin, char needle) +{ + return cuda::std::find(input.data() + begin, input.data() + input.size_bytes(), needle) - + input.data(); +} + __device__ int compute_request_line_sizes(int32_t* method_size, int32_t* path_size, int32_t* version_size, @@ -24,17 +27,10 @@ __device__ int compute_request_line_sizes(int32_t* method_size, auto n = input.size_bytes(); - auto find_character = [&](int32_t begin, char needle) { - for (auto i = begin; i < n; ++i) { - if (input.data()[i] == needle) { return i; } - } - return n; - }; - - auto method_end = find_character(0, ' '); + auto method_end = find_character(input, 0, ' '); if (method_end == n) { return 0; } - auto target_end = find_character(method_end + 1, ' '); + auto target_end = find_character(input, method_end + 1, ' '); if (target_end == n || n - target_end < 6) { return 0; } if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || @@ -42,7 +38,7 @@ __device__ int compute_request_line_sizes(int32_t* method_size, return 0; } - auto query_begin = find_character(method_end + 1, '?'); + auto query_begin = find_character(input, method_end + 1, '?'); // The path ends at the query or the target, whichever comes first. auto path_end = query_begin < target_end ? query_begin : target_end; @@ -64,23 +60,10 @@ __device__ int write_request_line(cuda::std::span* method, { auto n = input.size_bytes(); - auto find_character = [&](int32_t begin, char needle) { - for (auto i = begin; i < n; ++i) { - if (input.data()[i] == needle) { return i; } - } - return n; - }; - - auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { - for (auto index = begin; index < end; ++index) { - out[index - begin] = input.data()[index]; - } - }; - - auto method_end = find_character(0, ' '); + auto method_end = find_character(input, 0, ' '); if (method_end == n) { return 0; } - auto target_end = find_character(method_end + 1, ' '); + auto target_end = find_character(input, method_end + 1, ' '); if (target_end == n || n - target_end < 6) { return 0; } if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || @@ -88,20 +71,20 @@ __device__ int write_request_line(cuda::std::span* method, return 0; } - auto query_begin = find_character(method_end + 1, '?'); + auto query_begin = find_character(input, method_end + 1, '?'); // The path ends at the query or the target, whichever comes first. auto path_end = query_begin < target_end ? query_begin : target_end; - copy_field(*method, 0, method_end); - copy_field(*path, method_end + 1, path_end); - copy_field(*version, target_end + 6, n); + memcpy(method->data(), input.data(), method_end); + memcpy(path->data(), input.data() + method_end + 1, path_end - (method_end + 1)); + memcpy(version->data(), input.data() + target_end + 6, n - (target_end + 6)); // return 0 to indicate success return 0; } -// The symbol `transform` is the entry point for cudf::transform_lto. +// The symbol `transform` is the UDF entry point for `cudf::transform_lto`. #ifdef UDF_COMPUTE_SIZES extern "C" __device__ int transform(int32_t* method_size, int32_t* path_size, diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp index 9d5c991872ae..0c050807a1e0 100644 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ b/cpp/examples/string_transforms/http_logs/transforms.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include @@ -50,10 +52,8 @@ __device__ int compute_request_line_sizes(int32_t* method_size, auto n = input.size_bytes(); auto find_character = [&](int32_t begin, char needle) { - for (auto i = begin; i < n; ++i) { - if (input.data()[i] == needle) { return i; } - } - return n; + return cuda::std::find(input.data() + begin, input.data() + input.size_bytes(), needle) - + input.data(); }; auto method_end = find_character(0, ' '); @@ -91,16 +91,8 @@ __device__ int write_request_line(cuda::std::span* method, auto n = input.size_bytes(); auto find_character = [&](int32_t begin, char needle) { - for (auto i = begin; i < n; ++i) { - if (input.data()[i] == needle) { return i; } - } - return n; - }; - - auto copy_field = [&](cuda::std::span out, int32_t begin, int32_t end) { - for (auto index = begin; index < end; ++index) { - out[index - begin] = input.data()[index]; - } + return cuda::std::find(input.data() + begin, input.data() + input.size_bytes(), needle) - + input.data(); }; auto method_end = find_character(0, ' '); @@ -119,9 +111,9 @@ __device__ int write_request_line(cuda::std::span* method, // The path ends at the query or the target, whichever comes first. auto path_end = query_begin < target_end ? query_begin : target_end; - copy_field(*method, 0, method_end); - copy_field(*path, method_end + 1, path_end); - copy_field(*version, target_end + 6, n); + memcpy(method->data(), input.data(), method_end); + memcpy(path->data(), input.data() + method_end + 1, path_end - (method_end + 1)); + memcpy(version->data(), input.data() + target_end + 6, n - (target_end + 6)); // return 0 to indicate success return 0; @@ -130,16 +122,52 @@ __device__ int write_request_line(cuda::std::span* method, constexpr std::string_view usage = "usage: http_log_transforms INPUT.csv OUTPUT.csv " - " ROWS ITERATIONS\n" + " ROWS ITERATIONS\n" " http_log_transforms \n"; +[[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // Match the CUDA parsers: accept any method and HTTP version, extract the path before an optional + // query, and require only the delimiters and literal "HTTP/" prefix that they validate. + static auto program = + cudf::strings::regex_program::create(R"(^([^ ]*) ([^ ?]*)[^ ]* HTTP/(.*)$)"); + return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); +} + [[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - static auto program = - cudf::strings::regex_program::create(R"(^([A-Z]+) ([^ ?]+)[^ ]* HTTP/([0-9]+[.][0-9]+)$)"); - return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); + auto space = cudf::string_scalar{" ", true, stream, mr}; + auto query = cudf::string_scalar{"?", true, stream, mr}; + + // split "METHOD target HTTP/version" into its three logical fields using public string APIs. + auto request = cudf::strings::partition(cudf::strings_column_view{input}, space, stream, mr); + auto request_columns = request->release(); + auto target_and_protocol = cudf::strings::rpartition( + cudf::strings_column_view{request_columns[2]->view()}, space, stream, mr); + auto target_and_protocol_columns = target_and_protocol->release(); + + // remove the optional query from the request target and the "HTTP/" protocol prefix. + auto path_and_query = cudf::strings::partition( + cudf::strings_column_view{target_and_protocol_columns[0]->view()}, query, stream, mr); + auto path_and_query_columns = path_and_query->release(); + auto version = + cudf::strings::slice_strings(cudf::strings_column_view{target_and_protocol_columns[2]->view()}, + 5, + std::nullopt, + std::nullopt, + stream, + mr); + + std::vector> result; + result.reserve(output_count); + result.push_back(std::move(request_columns[0])); + result.push_back(std::move(path_and_query_columns[0])); + result.push_back(std::move(version)); + return std::make_unique(std::move(result)); } [[nodiscard]] std::unique_ptr run_jit(cudf::column_view input, @@ -250,8 +278,9 @@ int main(int argc, char const** argv) auto input_path = std::string{argv[1]}; auto output_path = std::string{argv[2]}; auto implementation = std::string_view{argv[3]}; - if (implementation != "precompiled" && implementation != "jit" && implementation != "lto") { - throw std::invalid_argument("variant must be precompiled, jit, or lto"); + if (implementation != "regex" && implementation != "precompiled" && implementation != "jit" && + implementation != "lto") { + throw std::invalid_argument("variant must be regex, precompiled, jit, or lto"); } auto requested_rows = std::stoll(argv[4]); @@ -262,11 +291,10 @@ int main(int argc, char const** argv) if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } - auto rows = static_cast(requested_rows); - auto is_precompiled = implementation == "precompiled"; - auto use_lto = implementation == "lto"; - auto stream = cudf::get_default_stream(); - auto mr = cudf::get_current_device_resource_ref(); + auto rows = static_cast(requested_rows); + auto use_lto = implementation == "lto"; + auto stream = cudf::get_default_stream(); + auto mr = cudf::get_current_device_resource_ref(); auto read_options = cudf::io::csv_reader_options::builder(cudf::io::source_info{input_path}) .header(0) @@ -284,14 +312,18 @@ int main(int argc, char const** argv) // Track allocations made by the transforms without changing the application's upstream memory // resource. rmm::mr::statistics_resource_adaptor stats{mr}; - auto stats_mr = rmm::device_async_resource_ref{stats}; + auto stats_mr = rmm::device_async_resource_ref{stats}; + auto run_transform = [&]() { + if (implementation == "regex") { return run_regex(input_view, stream, stats_mr); } + if (implementation == "precompiled") { return run_precompiled(input_view, stream, stats_mr); } + return run_jit(input_view, use_lto, stream, stats_mr); + }; stream.synchronize(); // The cold measurement includes regex setup or JIT compilation/linking performed on first use. auto cold_start = std::chrono::steady_clock::now(); nvtxRangePush("http_log_cold"); - auto cold_result = is_precompiled ? run_precompiled(input_view, stream, stats_mr) - : run_jit(input_view, use_lto, stream, stats_mr); + auto cold_result = run_transform(); stream.synchronize(); nvtxRangePop(); auto cold_seconds = @@ -304,8 +336,7 @@ int main(int argc, char const** argv) nvtxRangePush("http_log_warm"); for (auto i = 0; i < iterations; ++i) { result.reset(); - result = is_precompiled ? run_precompiled(input_view, stream, stats_mr) - : run_jit(input_view, use_lto, stream, stats_mr); + result = run_transform(); } stream.synchronize(); nvtxRangePop(); diff --git a/cpp/src/strings/extract/extract.cu b/cpp/src/strings/extract/extract.cu index 1a7573b2a8a1..19c2a07b9143 100644 --- a/cpp/src/strings/extract/extract.cu +++ b/cpp/src/strings/extract/extract.cu @@ -160,7 +160,7 @@ std::unique_ptr extract_single(strings_column_view const& input, "group parameter outside the range of capture groups found in the regex pattern", std::invalid_argument); - auto indices = rmm::device_uvector(input.size(), stream); + auto indices = rmm::device_uvector(input.size(), stream, mr); auto const d_strings = column_device_view::create(input.parent(), stream); diff --git a/cpp/src/transform/transform.cu b/cpp/src/transform/transform.cu index 612f2567c99b..9ea30dfe75dc 100644 --- a/cpp/src/transform/transform.cu +++ b/cpp/src/transform/transform.cu @@ -201,11 +201,12 @@ void launch(cudf::kernel const& kernel, rmm::cuda_stream_view stream) { CUDF_FUNC_RANGE(); + void* args[] = {&row_size, &stencil, &user_data, &input_cols, &output_cols, &max_error}; auto cfg = kernel.max_occupancy_config(0, 0); CUDF_EXPECTS(cfg.block_size % cudf::detail::warp_size == 0, "Expected block size to be a multiple of warp size", std::runtime_error); - kernel.launch_with({cfg.min_grid_size}, {cfg.block_size}, 0, stream, row_size, stencil, user_data, input_cols, output_cols, max_error); + kernel.launch({cfg.min_grid_size}, {cfg.block_size}, 0, stream, args); } std::string get_element_type_name(column_view const& view, bool use_physical_type); @@ -416,8 +417,8 @@ std::string reflect_udf_signature(bool is_null_aware, ? "" : std::accumulate( std::next(params.begin()), params.end(), params[0], [](auto const& a, auto const& b) { - return std::format("{}, {}", a, b); - }); + return std::format("{}, {}", a, b); + }); return std::format("int({})", joined); } diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 2c8774a5eeba..09c60e0dbbda 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -788,7 +788,7 @@ add_fragment( ) rtcx_embed( - cudf_test_fragments COMPRESSION none OUTPUT_DIRECTORY "${CUDF_GENERATED_INCLUDE_DIR}/rtcx_embed" + cudf_test_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed" ) ConfigureTest( From 41c09fd3310654e36938c99193d54eeeb9210d24 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 22 Jul 2026 00:07:37 +0000 Subject: [PATCH 17/27] remove redundant CUDF_DISABLE_EXPORTS --- cpp/cmake/Modules/AddFragment.cmake | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index d41669b91a44..7a30df538192 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -56,7 +56,6 @@ macro(add_fragment) ) endif() - target_compile_definitions(${OBJECT_ID} PRIVATE CUDF_DISABLE_EXPORTS ${ARG_DEFINITIONS}) if(ARG_INCLUDE_DIRECTORIES) target_include_directories(${OBJECT_ID} PRIVATE ${ARG_INCLUDE_DIRECTORIES}) endif() From d757b11e4c26c400a78bfd4f2c586024620ff5af Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 22 Jul 2026 12:04:48 +0000 Subject: [PATCH 18/27] fix compilation error --- cpp/cmake/Modules/AddFragment.cmake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cpp/cmake/Modules/AddFragment.cmake b/cpp/cmake/Modules/AddFragment.cmake index 7a30df538192..d9cbe3538de9 100644 --- a/cpp/cmake/Modules/AddFragment.cmake +++ b/cpp/cmake/Modules/AddFragment.cmake @@ -68,6 +68,10 @@ macro(add_fragment) target_compile_options(${OBJECT_ID} PRIVATE ${ARG_COMPILE_OPTIONS}) endif() + if(ARG_DEFINITIONS) + target_compile_definitions(${OBJECT_ID} PRIVATE ${ARG_DEFINITIONS}) + endif() + set_target_properties( ${OBJECT_ID} PROPERTIES CUDA_SEPARABLE_COMPILATION ON From 8f52275432ab95eee4703a5c57be8f2ec26b9499 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 22 Jul 2026 21:03:09 +0000 Subject: [PATCH 19/27] CI compile error fix --- cpp/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index dc20e53e7bc2..864afb231771 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1305,6 +1305,7 @@ target_compile_options( target_include_directories( cudf PUBLIC "$" "$" + "$" PRIVATE "$" "$" "$" From 1b03b2765bc123a606b664e4c5e46d652add64ce Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 22 Jul 2026 21:42:59 +0000 Subject: [PATCH 20/27] update --- cpp/examples/string_transforms/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 909e0278b5f3..3c3712246b78 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -22,6 +22,7 @@ include(rapids-cmake) rapids_cmake_build_type("Release") # Fetch librtcx and its dependencies +find_package(CUDAToolkit REQUIRED) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_zstd.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_xxhash.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_rtcx.cmake) From 387e3431c7979b0572bdd9133f60767ddb18d368 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Wed, 22 Jul 2026 22:11:48 +0000 Subject: [PATCH 21/27] update examples deps --- conda/recipes/libcudf/recipe.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda/recipes/libcudf/recipe.yaml b/conda/recipes/libcudf/recipe.yaml index 236d702d0954..f0c9e89b674a 100644 --- a/conda/recipes/libcudf/recipe.yaml +++ b/conda/recipes/libcudf/recipe.yaml @@ -318,6 +318,8 @@ outputs: - cuda-version =${{ cuda_version }} - cuda-nvtx-dev - cuda-cudart-dev + - cuda-nvrtc-dev + - libnvjitlink-dev run: - ${{ pin_subpackage("libcudf", exact=True) }} - ${{ pin_compatible("cuda-version", upper_bound="x", lower_bound="x") }} From f62bc1372596a055c96815436493def01e07ddd8 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Thu, 30 Jul 2026 18:09:55 +0000 Subject: [PATCH 22/27] update --- cpp/examples/string_transforms/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 3c3712246b78..c5598988f47d 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -25,6 +25,7 @@ rapids_cmake_build_type("Release") find_package(CUDAToolkit REQUIRED) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_zstd.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_xxhash.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_nvtx.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/thirdparty/get_rtcx.cmake) # Include the helper function for embedding FATBINs of fragments From 1c95110d4f98c60ad57ab2620903a502cd7c3d6b Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 3 Aug 2026 17:37:36 +0000 Subject: [PATCH 23/27] Add URL parsing and transformation functionality - 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. --- cpp/CMakeLists.txt | 4 +- cpp/examples/string_transforms/CMakeLists.txt | 30 +- cpp/examples/string_transforms/README.md | 16 +- .../string_transforms/http_logs/fragments.cu | 108 ---- .../string_transforms/http_logs/logs.csv | 13 - .../http_logs/transforms.cpp | 381 ------------ .../string_transforms/url_logs/fragments.cu | 232 +++++++ .../string_transforms/url_logs/logs.csv | 46 ++ .../string_transforms/url_logs/transforms.cpp | 566 ++++++++++++++++++ 9 files changed, 869 insertions(+), 527 deletions(-) delete mode 100644 cpp/examples/string_transforms/http_logs/fragments.cu delete mode 100644 cpp/examples/string_transforms/http_logs/logs.csv delete mode 100644 cpp/examples/string_transforms/http_logs/transforms.cpp create mode 100644 cpp/examples/string_transforms/url_logs/fragments.cu create mode 100644 cpp/examples/string_transforms/url_logs/logs.csv create mode 100644 cpp/examples/string_transforms/url_logs/transforms.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index d11590f31802..908cc44c6c47 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -617,8 +617,8 @@ function(precompile_string_kernel_fragments) foreach(OUTPUT_TYPE OUTPUT_COLUMN_TYPE IN ZIP_LISTS CUDF_PRECOMPILE_STRING_OUTPUT_PHYSICAL_TYPES CUDF_PRECOMPILE_STRING_OUTPUT_COLUMN_TYPES ) - # Pre-compile for up to 4 output columns - foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3") + # Pre-compile for up to 8 output columns + foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3" "0;1;2;3;4" "0;1;2;3;4;5" "0;1;2;3;4;5;6" "0;1;2;3;4;5;6;7") set(FRAGMENT_NAME transform_kernel) get_property( FILE_INDEX diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index c5598988f47d..5ad329604bd2 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -59,40 +59,40 @@ add_string_transforms_example(localize_phone_jit localize_phone_jit.cpp) add_string_transforms_example(localize_phone_precompiled localize_phone_precompiled.cpp) # Compile and embed the UDFs so we can link them at runtime -rtcx_add_embed(http_log_fragments) +rtcx_add_embed(url_log_fragments) add_fragment( - http_log_fragments + url_log_fragments FRAGMENT - request_line_sizes + url_component_sizes SOURCE - http_logs/fragments.cu + url_logs/fragments.cu DEFINITIONS UDF_COMPUTE_SIZES INCLUDE_DIRECTORIES - ${CMAKE_CURRENT_LIST_DIR}/http_logs + ${CMAKE_CURRENT_LIST_DIR}/url_logs LINK_LIBRARIES cudf::cudf ) add_fragment( - http_log_fragments + url_log_fragments FRAGMENT - request_line_output + url_component_output SOURCE - http_logs/fragments.cu + url_logs/fragments.cu DEFINITIONS UDF_WRITE_OUTPUT INCLUDE_DIRECTORIES - ${CMAKE_CURRENT_LIST_DIR}/http_logs + ${CMAKE_CURRENT_LIST_DIR}/url_logs LINK_LIBRARIES cudf::cudf ) -rtcx_embed(http_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed") +rtcx_embed(url_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed") -add_string_transforms_example(http_log_transforms http_logs/transforms.cpp) -target_sources(http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}/http_log_fragments.s) -target_include_directories(http_log_transforms PRIVATE ${http_log_fragments_SOURCE_DIR}) -add_dependencies(http_log_transforms http_log_fragments) +add_string_transforms_example(url_log_transforms url_logs/transforms.cpp) +target_sources(url_log_transforms PRIVATE ${url_log_fragments_SOURCE_DIR}/url_log_fragments.s) +target_include_directories(url_log_transforms PRIVATE ${url_log_fragments_SOURCE_DIR}) +add_dependencies(url_log_transforms url_log_fragments) -install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv ${CMAKE_CURRENT_LIST_DIR}/http_logs/logs.csv +install(FILES ${CMAKE_CURRENT_LIST_DIR}/info.csv ${CMAKE_CURRENT_LIST_DIR}/url_logs/logs.csv DESTINATION bin/examples/libcudf/string_transformers ) diff --git a/cpp/examples/string_transforms/README.md b/cpp/examples/string_transforms/README.md index 68503a01887f..8830e18397b6 100644 --- a/cpp/examples/string_transforms/README.md +++ b/cpp/examples/string_transforms/README.md @@ -15,14 +15,14 @@ The following examples are included: 5. `extract_email_precompiled` - Performs same transformation on the table as `output` but uses precompiled public APIs 6. `format_phone_jit` - Using a transform kernel to output a string to a pre-allocated buffer 7. `format_phone_precompiled` - Performs same transformation on the table as `preallocated` but uses precompiled public APIs -8. `http_log_transforms` - Compares four multi-output HTTP log extractors: - - `regex`: `cudf::strings::extract` with a regex program. - - `precompiled`: public string partition and slice APIs, without regular expressions. - - `jit`: two CUDA source transforms compiled at runtime. The first produces exact per-row string - sizes; inclusive scans turn those sizes into run-end offsets, and the second writes directly to - the resulting string character buffers. - - `lto`: the same sizing and output transform ABI, AOT-compiled to embedded fatbins and JIT-linked - with libcudf's precompiled transform kernels. +8. `url_log_transforms` - Searches raw text log lines for an embedded URL and decomposes the + first match into protocol, host, port, path, query, and fragment columns (based on https://datatracker.ietf.org/doc/html/rfc3986): + - `regex`: one six-capture `cudf::strings::extract` expression. + - `precompiled`: sequential public partition, conditional-copy, and concatenate APIs. + - `jit`: a fused byte parser compiled from CUDA source at runtime. Its sizing pass produces exact + per-row output sizes; scans create offsets and a second pass writes all six output columns. + - `lto`: the same fused parser ABI, AOT-compiled to embedded fatbins and JIT-linked with + libcudf's precompiled transform kernels. ## Compile and execute diff --git a/cpp/examples/string_transforms/http_logs/fragments.cu b/cpp/examples/string_transforms/http_logs/fragments.cu deleted file mode 100644 index 18d6fa74e636..000000000000 --- a/cpp/examples/string_transforms/http_logs/fragments.cu +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include - -#include -#include - -__device__ int32_t find_character(cudf::string_view input, int32_t begin, char needle) -{ - return cuda::std::find(input.data() + begin, input.data() + input.size_bytes(), needle) - - input.data(); -} - -__device__ int compute_request_line_sizes(int32_t* method_size, - int32_t* path_size, - int32_t* version_size, - cudf::string_view input) -{ - // Initialize output sizes to zero in case of early return. - *method_size = 0; - *path_size = 0; - *version_size = 0; - - auto n = input.size_bytes(); - - auto method_end = find_character(input, 0, ' '); - if (method_end == n) { return 0; } - - auto target_end = find_character(input, method_end + 1, ' '); - if (target_end == n || n - target_end < 6) { return 0; } - if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || - input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || - input.data()[target_end + 5] != '/') { - return 0; - } - - auto query_begin = find_character(input, method_end + 1, '?'); - - // The path ends at the query or the target, whichever comes first. - auto path_end = query_begin < target_end ? query_begin : target_end; - - *method_size = method_end; - *path_size = path_end - method_end - 1; - *version_size = n - target_end - 6; - - // return 0 to indicate success - return 0; -} - -// Each span points at the final character buffer for one output string in this row. Its size came -// from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. -__device__ int write_request_line(cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) -{ - auto n = input.size_bytes(); - - auto method_end = find_character(input, 0, ' '); - if (method_end == n) { return 0; } - - auto target_end = find_character(input, method_end + 1, ' '); - if (target_end == n || n - target_end < 6) { return 0; } - if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || - input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || - input.data()[target_end + 5] != '/') { - return 0; - } - - auto query_begin = find_character(input, method_end + 1, '?'); - - // The path ends at the query or the target, whichever comes first. - auto path_end = query_begin < target_end ? query_begin : target_end; - - memcpy(method->data(), input.data(), method_end); - memcpy(path->data(), input.data() + method_end + 1, path_end - (method_end + 1)); - memcpy(version->data(), input.data() + target_end + 6, n - (target_end + 6)); - - // return 0 to indicate success - return 0; -} - -// The symbol `transform` is the UDF entry point for `cudf::transform_lto`. -#ifdef UDF_COMPUTE_SIZES -extern "C" __device__ int transform(int32_t* method_size, - int32_t* path_size, - int32_t* version_size, - cudf::string_view input) -{ - return compute_request_line_sizes(method_size, path_size, version_size, input); -} -#else -#ifdef UDF_WRITE_OUTPUT -extern "C" __device__ int transform(cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) -{ - return write_request_line(method, path, version, input); -} -#else -#error "Must define either UDF_COMPUTE_SIZES or UDF_WRITE_OUTPUT" -#endif -#endif diff --git a/cpp/examples/string_transforms/http_logs/logs.csv b/cpp/examples/string_transforms/http_logs/logs.csv deleted file mode 100644 index ccc9083deb6b..000000000000 --- a/cpp/examples/string_transforms/http_logs/logs.csv +++ /dev/null @@ -1,13 +0,0 @@ -RequestLine,CombinedLog -"GET / HTTP/1.1","203.0.113.10 - alice [05/Jul/2026:14:32:10 +0000] ""GET / HTTP/1.1"" 200 512 ""https://example.com/"" ""Mozilla/5.0 (X11; Linux x86_64)""" -"GET /api/v1/orders/123?expand=items HTTP/1.1","198.51.100.24 - bob [05/Jul/2026:14:32:11 +0000] ""GET /api/v1/orders/123?expand=items HTTP/1.1"" 200 1532 ""https://example.com/cart"" ""Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36""" -"POST /api/v1/login HTTP/2.0","192.0.2.35 - - [05/Jul/2026:14:32:12 +0000] ""POST /api/v1/login HTTP/2.0"" 401 96 ""-"" ""curl/8.8.0""" -"PUT /api/v1/users/8675309 HTTP/1.1","203.0.113.47 - carol [05/Jul/2026:14:32:13 +0000] ""PUT /api/v1/users/8675309 HTTP/1.1"" 204 0 ""https://admin.example.com/users"" ""Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0""" -"DELETE /api/v1/sessions/current HTTP/1.1","198.51.100.58 - dave [05/Jul/2026:14:32:14 +0000] ""DELETE /api/v1/sessions/current HTTP/1.1"" 202 48 ""https://example.com/settings"" ""Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)""" -"PATCH /api/v2/catalog/items/42?locale=en-GB HTTP/2.0","192.0.2.69 - erin [05/Jul/2026:14:32:15 +0000] ""PATCH /api/v2/catalog/items/42?locale=en-GB HTTP/2.0"" 200 2048 ""https://admin.example.com/catalog"" ""Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:127.0) Firefox/127.0""" -"GET /assets/app.8f31c2.js HTTP/1.1","203.0.113.71 - - [05/Jul/2026:14:32:16 +0000] ""GET /assets/app.8f31c2.js HTTP/1.1"" 304 0 ""https://example.com/dashboard"" ""Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) Chrome/126.0 Mobile""" -"HEAD /healthz HTTP/1.1","198.51.100.82 - probe [05/Jul/2026:14:32:17 +0000] ""HEAD /healthz HTTP/1.1"" 200 0 ""-"" ""kube-probe/1.30""" -"OPTIONS /api/v1/orders HTTP/2.0","192.0.2.93 - - [05/Jul/2026:14:32:18 +0000] ""OPTIONS /api/v1/orders HTTP/2.0"" 204 0 ""https://shop.example.net/"" ""Mozilla/5.0 (X11; Fedora; Linux x86_64) Chrome/126.0""" -"POST /graphql?operation=Checkout HTTP/2.0","203.0.113.104 - frank [05/Jul/2026:14:32:19 +0000] ""POST /graphql?operation=Checkout HTTP/2.0"" 200 8192 ""https://shop.example.net/checkout"" ""ShopMobile/6.4.1 (iOS 17.5; Scale/3.00)""" -"GET /search?q=gpu+dataframes&page=2 HTTP/1.1","198.51.100.115 - grace [05/Jul/2026:14:32:20 +0000] ""GET /search?q=gpu+dataframes&page=2 HTTP/1.1"" 200 16384 ""https://www.example.org/"" ""Googlebot/2.1 (+http://www.google.com/bot.html)""" -"POST /events/batch HTTP/1.1","192.0.2.126 - service [05/Jul/2026:14:32:21 +0000] ""POST /events/batch HTTP/1.1"" 503 128 ""-"" ""telemetry-agent/3.12.0 linux/amd64""" diff --git a/cpp/examples/string_transforms/http_logs/transforms.cpp b/cpp/examples/string_transforms/http_logs/transforms.cpp deleted file mode 100644 index 0c050807a1e0..000000000000 --- a/cpp/examples/string_transforms/http_logs/transforms.cpp +++ /dev/null @@ -1,381 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -constexpr auto output_count = std::size_t{3}; - -// Runtime JIT compilation consumes one CUDA source string for each pass. -constexpr char request_line_sizes_udf[] = R"***( -// multi_transform calls this function once per input row. Pointer parameters are output columns; -// writing a byte count to each one lets the host create exact string offsets before allocation. -__device__ int compute_request_line_sizes(int32_t* method_size, - int32_t* path_size, - int32_t* version_size, - cudf::string_view input) { - // Initialize output sizes to zero in case of early return. - *method_size = 0; - *path_size = 0; - *version_size = 0; - - auto n = input.size_bytes(); - - auto find_character = [&](int32_t begin, char needle) { - return cuda::std::find(input.data() + begin, input.data() + input.size_bytes(), needle) - - input.data(); - }; - - auto method_end = find_character(0, ' '); - if (method_end == n) { return 0; } - - auto target_end = find_character(method_end + 1, ' '); - if (target_end == n || n - target_end < 6) { return 0; } - if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || - input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || - input.data()[target_end + 5] != '/') { - return 0; - } - - auto query_begin = find_character(method_end + 1, '?'); - - // The path ends at the query or the target, whichever comes first. - auto path_end = query_begin < target_end ? query_begin : target_end; - - *method_size = method_end; - *path_size = path_end - method_end - 1; - *version_size = n - target_end - 6; - - // return 0 to indicate success - return 0; -} -)***"; - -constexpr char request_line_output_udf[] = R"***( -// Each span points at the final character buffer for one output string in this row. Its size came -// from compute_request_line_sizes, so this pass only copies bytes and performs no allocation. -__device__ int write_request_line(cuda::std::span* method, - cuda::std::span* path, - cuda::std::span* version, - cudf::string_view input) { - auto n = input.size_bytes(); - - auto find_character = [&](int32_t begin, char needle) { - return cuda::std::find(input.data() + begin, input.data() + input.size_bytes(), needle) - - input.data(); - }; - - auto method_end = find_character(0, ' '); - if (method_end == n) { return 0; } - - auto target_end = find_character(method_end + 1, ' '); - if (target_end == n || n - target_end < 6) { return 0; } - if (input.data()[target_end + 1] != 'H' || input.data()[target_end + 2] != 'T' || - input.data()[target_end + 3] != 'T' || input.data()[target_end + 4] != 'P' || - input.data()[target_end + 5] != '/') { - return 0; - } - - auto query_begin = find_character(method_end + 1, '?'); - - // The path ends at the query or the target, whichever comes first. - auto path_end = query_begin < target_end ? query_begin : target_end; - - memcpy(method->data(), input.data(), method_end); - memcpy(path->data(), input.data() + method_end + 1, path_end - (method_end + 1)); - memcpy(version->data(), input.data() + target_end + 6, n - (target_end + 6)); - - // return 0 to indicate success - return 0; -} -)***"; - -constexpr std::string_view usage = - "usage: http_log_transforms INPUT.csv OUTPUT.csv " - " ROWS ITERATIONS\n" - " http_log_transforms \n"; - -[[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // Match the CUDA parsers: accept any method and HTTP version, extract the path before an optional - // query, and require only the delimiters and literal "HTTP/" prefix that they validate. - static auto program = - cudf::strings::regex_program::create(R"(^([^ ]*) ([^ ?]*)[^ ]* HTTP/(.*)$)"); - return cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); -} - -[[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - auto space = cudf::string_scalar{" ", true, stream, mr}; - auto query = cudf::string_scalar{"?", true, stream, mr}; - - // split "METHOD target HTTP/version" into its three logical fields using public string APIs. - auto request = cudf::strings::partition(cudf::strings_column_view{input}, space, stream, mr); - auto request_columns = request->release(); - auto target_and_protocol = cudf::strings::rpartition( - cudf::strings_column_view{request_columns[2]->view()}, space, stream, mr); - auto target_and_protocol_columns = target_and_protocol->release(); - - // remove the optional query from the request target and the "HTTP/" protocol prefix. - auto path_and_query = cudf::strings::partition( - cudf::strings_column_view{target_and_protocol_columns[0]->view()}, query, stream, mr); - auto path_and_query_columns = path_and_query->release(); - auto version = - cudf::strings::slice_strings(cudf::strings_column_view{target_and_protocol_columns[2]->view()}, - 5, - std::nullopt, - std::nullopt, - stream, - mr); - - std::vector> result; - result.reserve(output_count); - result.push_back(std::move(request_columns[0])); - result.push_back(std::move(path_and_query_columns[0])); - result.push_back(std::move(version)); - return std::make_unique(std::move(result)); -} - -[[nodiscard]] std::unique_ptr run_jit(cudf::column_view input, - bool use_lto, - rmm::cuda_stream_view stream, - rmm::device_async_resource_ref mr) -{ - // Pass 1 produces one byte-count column for each eventual string output. - cudf::transform_output const size_spec{cudf::data_type{cudf::type_id::INT32}, - cudf::output_nullability::ALL_VALID}; - std::vector const size_outputs(output_count, size_spec); - cudf::transform_input inputs[] = {input}; - - std::unique_ptr sizes; - - if (use_lto) { - auto range = http_log_fragments::file_ranges[http_log_fragments::request_line_sizes]; - auto fragment = http_log_fragments::files.subspan(range[0], range[1]); - - sizes = cudf::transform_lto(fragment, - cudf::lto_binary_type::FATBIN, - cudf::null_aware::NO, - std::nullopt, - inputs, - size_outputs, - {}, - std::nullopt, - stream, - mr); - } else { - sizes = cudf::multi_transform(request_line_sizes_udf, - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - size_outputs, - {}, - std::nullopt, - stream, - mr); - } - - // Inclusive scans turn the sizes into the offsets needed for the final strings columns. - std::vector> offsets; - offsets.reserve(output_count); - - for (auto& string_sizes : sizes->view()) { - auto run_ends = cudf::scan(string_sizes, - *cudf::make_sum_aggregation(), - cudf::scan_type::INCLUSIVE, - cudf::null_policy::EXCLUDE, - stream, - mr); - - auto zero = cudf::numeric_scalar{0, true, stream, mr}; - auto first = cudf::make_column_from_scalar(zero, 1, stream, mr); - offsets.push_back(cudf::concatenate( - std::vector{first->view(), run_ends->view()}, stream, mr)); - } - - // Pass 2 writes directly into final character buffers described by those offsets. - cudf::transform_output const output_spec{cudf::data_type{cudf::type_id::STRING}, - cudf::output_nullability::ALL_VALID}; - std::vector const outputs(output_count, output_spec); - - if (use_lto) { - auto range = http_log_fragments::file_ranges[http_log_fragments::request_line_output]; - auto fragment = http_log_fragments::files.subspan(range[0], range[1]); - return cudf::transform_lto(fragment, - cudf::lto_binary_type::FATBIN, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - std::move(offsets), - std::nullopt, - stream, - mr); - } - - return cudf::multi_transform(request_line_output_udf, - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - std::move(offsets), - std::nullopt, - stream, - mr); -} - -} // namespace - -int main(int argc, char const** argv) -{ - try { - if (argc == 2 && - (std::string_view{argv[1]} == "--help" || std::string_view{argv[1]} == "usage")) { - std::cout << usage; - return EXIT_SUCCESS; - } - - if (argc != 6) { - throw std::invalid_argument("invalid arguments; run http_log_transforms --help for usage"); - } - - auto input_path = std::string{argv[1]}; - auto output_path = std::string{argv[2]}; - auto implementation = std::string_view{argv[3]}; - if (implementation != "regex" && implementation != "precompiled" && implementation != "jit" && - implementation != "lto") { - throw std::invalid_argument("variant must be regex, precompiled, jit, or lto"); - } - - auto requested_rows = std::stoll(argv[4]); - auto iterations = std::stoi(argv[5]); - if (requested_rows < 0 || requested_rows > std::numeric_limits::max()) { - throw std::invalid_argument("ROWS is outside the cudf::size_type range"); - } - - if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } - - auto rows = static_cast(requested_rows); - auto use_lto = implementation == "lto"; - auto stream = cudf::get_default_stream(); - auto mr = cudf::get_current_device_resource_ref(); - - auto read_options = cudf::io::csv_reader_options::builder(cudf::io::source_info{input_path}) - .header(0) - .use_cols_names({"RequestLine"}) - .build(); - auto input = cudf::io::read_csv(read_options).tbl; - if (rows != input->num_rows()) { - // Sampling with replacement scales the small checked-in dataset to the requested size. - input = cudf::sample(input->view(), rows, cudf::sample_with_replacement::TRUE); - } - - auto input_bytes = input->get_column(0).alloc_size(); - auto input_view = input->get_column(0).view(); - - // Track allocations made by the transforms without changing the application's upstream memory - // resource. - rmm::mr::statistics_resource_adaptor stats{mr}; - auto stats_mr = rmm::device_async_resource_ref{stats}; - auto run_transform = [&]() { - if (implementation == "regex") { return run_regex(input_view, stream, stats_mr); } - if (implementation == "precompiled") { return run_precompiled(input_view, stream, stats_mr); } - return run_jit(input_view, use_lto, stream, stats_mr); - }; - - stream.synchronize(); - // The cold measurement includes regex setup or JIT compilation/linking performed on first use. - auto cold_start = std::chrono::steady_clock::now(); - nvtxRangePush("http_log_cold"); - auto cold_result = run_transform(); - stream.synchronize(); - nvtxRangePop(); - auto cold_seconds = - std::chrono::duration{std::chrono::steady_clock::now() - cold_start}.count(); - cold_result.reset(); - - std::unique_ptr result; - // Subsequent calls exercise the cached kernel and represent steady-state throughput. - auto warm_start = std::chrono::steady_clock::now(); - nvtxRangePush("http_log_warm"); - for (auto i = 0; i < iterations; ++i) { - result.reset(); - result = run_transform(); - } - stream.synchronize(); - nvtxRangePop(); - auto warm_seconds = - std::chrono::duration{std::chrono::steady_clock::now() - warm_start}.count() / - iterations; - - // A dash suppresses CSV output so file I/O does not affect benchmark runs. - if (output_path != "-") { - auto write_options = - cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result->view()) - .include_header(true) - .names({"method", "path", "http_version"}) - .build(); - cudf::io::write_csv(write_options); - } - - auto bytes = stats.get_bytes_counter(); - auto output_bytes = result->alloc_size(); - auto gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); - - std::cout << std::format( - "variant={}\nrows={}\ncold_seconds={}\nwarm_seconds={}\nrows_per_second={}\neffective_gib_" - "per_second={}\ninput_bytes={}\noutput_bytes={}\npeak_memory_bytes={}\ntotal_allocated_bytes=" - "{}\nallocated_bytes_per_call={}\n", - implementation, - rows, - cold_seconds, - warm_seconds, - static_cast(rows) / warm_seconds, - gib / warm_seconds, - input_bytes, - output_bytes, - bytes.peak, - bytes.total, - bytes.total / static_cast(iterations + 1)); - return EXIT_SUCCESS; - } catch (std::exception const& error) { - std::cerr << error.what() << '\n'; - return EXIT_FAILURE; - } -} diff --git a/cpp/examples/string_transforms/url_logs/fragments.cu b/cpp/examples/string_transforms/url_logs/fragments.cu new file mode 100644 index 000000000000..7191e775f899 --- /dev/null +++ b/cpp/examples/string_transforms/url_logs/fragments.cu @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include + +struct range32 { + int32_t begin{}; + int32_t end{}; +}; + +struct url_ranges { + range32 protocol; + range32 host; + range32 port; + range32 path; + range32 query; + range32 fragment; +}; + +__device__ bool is_ascii_alpha(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } + +__device__ bool is_ascii_digit(char c) { return c >= '0' && c <= '9'; } + +__device__ bool is_hex_digit(char c) +{ + return is_ascii_digit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); +} + +// Parses the first valid URL candidate and records byte ranges for all six components. +__device__ bool parse_url(cudf::string_view input, url_ranges* out) +{ + auto n = input.size_bytes(); + *out = {}; + auto is_scheme_char = [](char c) { + return is_ascii_alpha(c) || is_ascii_digit(c) || c == '+' || c == '-' || c == '.'; + }; + auto is_unreserved = [](char c) { + return is_ascii_alpha(c) || is_ascii_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'; + }; + auto is_sub_delim = [](char c) { + return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || + c == '+' || c == ',' || c == ';' || c == '='; + }; + auto is_gen_delim = [](char c) { + return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'; + }; + auto is_context_delimiter = [](char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '"' || c == '<' || c == '>'; + }; + + auto scheme_end = n; + for (auto i = 1; i + 2 < n; ++i) { + if (input.data()[i] == ':' && input.data()[i + 1] == '/' && input.data()[i + 2] == '/') { + scheme_end = i; + break; + } + } + if (scheme_end == n) { return false; } + + auto url_begin = scheme_end; + while (url_begin > 0 && is_scheme_char(input.data()[url_begin - 1])) { + --url_begin; + } + if (url_begin == scheme_end || !is_ascii_alpha(input.data()[url_begin])) { return false; } + + auto url_end = n; + for (auto i = scheme_end + 3; i < n; ++i) { + if (is_context_delimiter(input.data()[i])) { + url_end = i; + break; + } + } + for (auto i = url_begin; i < url_end; ++i) { + auto c = input.data()[i]; + if (c == '%') { + if (i + 2 >= url_end || !is_hex_digit(input.data()[i + 1]) || + !is_hex_digit(input.data()[i + 2])) { + return false; + } + i += 2; + } else if (!is_unreserved(c) && !is_sub_delim(c) && !is_gen_delim(c)) { + return false; + } + } + + auto hash = url_end; + for (auto i = scheme_end + 3; i < url_end; ++i) { + if (input.data()[i] == '#') { + hash = i; + break; + } + } + auto question = hash; + for (auto i = scheme_end + 3; i < hash; ++i) { + if (input.data()[i] == '?') { + question = i; + break; + } + } + auto base_end = question < hash ? question : hash; + out->protocol = {url_begin, scheme_end}; + if (question < hash) { out->query = {question + 1, hash}; } + if (hash < url_end) { out->fragment = {hash + 1, url_end}; } + + auto authority_begin = scheme_end + 3; + auto authority_end = base_end; + for (auto i = authority_begin; i < base_end; ++i) { + if (input.data()[i] == '/') { + authority_end = i; + break; + } + } + out->path = {authority_end, base_end}; + + auto host_begin = authority_begin; + for (auto i = authority_begin; i < authority_end; ++i) { + if (input.data()[i] == '@') { host_begin = i + 1; } + } + + if (host_begin < authority_end && input.data()[host_begin] == '[') { + auto close = authority_end; + for (auto i = host_begin + 1; i < authority_end; ++i) { + if (input.data()[i] == ']') { + close = i; + break; + } + } + if (close == authority_end) { return false; } + out->host = {host_begin, close + 1}; + if (close + 1 < authority_end) { + if (input.data()[close + 1] != ':') { return false; } + out->port = {close + 2, authority_end}; + } + } else { + auto colon = authority_end; + for (auto i = host_begin; i < authority_end; ++i) { + if (input.data()[i] == ':') { colon = i; } + } + out->host = {host_begin, colon}; + if (colon < authority_end) { out->port = {colon + 1, authority_end}; } + } + + for (auto i = out->port.begin; i < out->port.end; ++i) { + if (!is_ascii_digit(input.data()[i])) { return false; } + } + return true; +} + +// Computes exact output byte counts for the six URL component columns. +__device__ int compute_url_component_sizes(int32_t* protocol_size, + int32_t* host_size, + int32_t* port_size, + int32_t* path_size, + int32_t* query_size, + int32_t* fragment_size, + cudf::string_view input) +{ + *protocol_size = 0; + *host_size = 0; + *port_size = 0; + *path_size = 0; + *query_size = 0; + *fragment_size = 0; + url_ranges ranges; + if (!parse_url(input, &ranges)) { return 0; } + *protocol_size = ranges.protocol.end - ranges.protocol.begin; + *host_size = ranges.host.end - ranges.host.begin; + *port_size = ranges.port.end - ranges.port.begin; + *path_size = ranges.path.end - ranges.path.begin; + *query_size = ranges.query.end - ranges.query.begin; + *fragment_size = ranges.fragment.end - ranges.fragment.begin; + return 0; +} + +// Copies the six parsed URL components into their preallocated string buffers. +__device__ int write_url_components(cuda::std::span* protocol, + cuda::std::span* host, + cuda::std::span* port, + cuda::std::span* path, + cuda::std::span* query, + cuda::std::span* fragment, + cudf::string_view input) +{ + url_ranges ranges; + if (!parse_url(input, &ranges)) { return 0; } + cuda::std::span* outputs[] = {protocol, host, port, path, query, fragment}; + range32 components[] = { + ranges.protocol, ranges.host, ranges.port, ranges.path, ranges.query, ranges.fragment}; + for (auto component = 0; component < 6; ++component) { + auto range = components[component]; + auto size = range.end - range.begin; + if (size > 0) { memcpy(outputs[component]->data(), input.data() + range.begin, size); } + } + return 0; +} + +#ifdef UDF_COMPUTE_SIZES +// Exposes the sizing pass through the transform LTO ABI. +extern "C" __device__ int transform(int32_t* protocol_size, + int32_t* host_size, + int32_t* port_size, + int32_t* path_size, + int32_t* query_size, + int32_t* fragment_size, + cudf::string_view input) +{ + return compute_url_component_sizes( + protocol_size, host_size, port_size, path_size, query_size, fragment_size, input); +} +#else +#ifdef UDF_WRITE_OUTPUT +// Exposes the component-writing pass through the transform LTO ABI. +extern "C" __device__ int transform(cuda::std::span* protocol, + cuda::std::span* host, + cuda::std::span* port, + cuda::std::span* path, + cuda::std::span* query, + cuda::std::span* fragment, + cudf::string_view input) +{ + return write_url_components(protocol, host, port, path, query, fragment, input); +} +#else +#error "Must define either UDF_COMPUTE_SIZES or UDF_WRITE_OUTPUT" +#endif +#endif diff --git a/cpp/examples/string_transforms/url_logs/logs.csv b/cpp/examples/string_transforms/url_logs/logs.csv new file mode 100644 index 000000000000..dbc29149f21a --- /dev/null +++ b/cpp/examples/string_transforms/url_logs/logs.csv @@ -0,0 +1,46 @@ +Timestamp,Host,Source,Level,LogLine +2026-07-31T09:14:00.000Z,edge-proxy-02,envoy,INFO,level=info service=edge method=GET status=200 url=https://api.example.com/v1/orders/123?expand=items&locale=en-GB#summary latency_ms=12 +2026-07-31T09:14:00.173Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:00.346Z,app-worker-07,checkout-worker,WARN,WARN checkout retry scheduled attempt=2 queue=payments +2026-07-31T09:14:00.519Z,identity-03,identity-api,INFO,ts=2026-07-31T09:14:00Z service=identity trace=0af765 client=198.51.100.24 target=https://login.example.com:8443/oauth2/callback?code=redacted&state=abc status=302 +2026-07-31T09:14:00.692Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:00.865Z,edge-proxy-02,envoy,INFO,edge[731]: cache=hit region=us-west-2 object=https://cdn.example.net/assets/app.8f31c2.js status=304 +2026-07-31T09:14:01.038Z,control-01,scheduler,DEBUG,level=debug component=scheduler queue_depth=0 workers=32 +2026-07-31T09:14:01.211Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:01.384Z,search-06,search-api,INFO,service=search region=ap-southeast-2 url=https://search.example.org/query?q=gpu+dataframes&page=2#results method=GET status=200 +2026-07-31T09:14:01.557Z,app-worker-07,checkout-worker,WARN,WARN checkout retry scheduled attempt=2 queue=payments +2026-07-31T09:14:01.730Z,realtime-04,realtime-gateway,INFO,INFO websocket connected client=203.0.113.71 endpoint=wss://stream.example.com/socket?token=redacted shard=4 +2026-07-31T09:14:01.903Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:02.076Z,node-17,prometheus-agent,INFO,host=node-17 process=metrics destination=http://metrics.internal.example:9090/api/v1/query?query=up result=success +2026-07-31T09:14:02.249Z,app-api-12,inventory-api,ERROR,level=error service=inventory msg=upstream_timeout attempt=3 +2026-07-31T09:14:02.422Z,control-01,scheduler,DEBUG,level=debug component=scheduler queue_depth=0 workers=32 +2026-07-31T09:14:02.595Z,graphql-02,graphql-api,INFO,request completed trace=4bf92f service=graphql method=POST url=https://api.example.com/graphql?operation=Checkout status=200 latency_ms=66 +2026-07-31T09:14:02.768Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:02.941Z,edge-proxy-02,envoy,INFO,cdn event status=206 bytes=1048576 resource=https://media.example.net/video/launch.mp4?start=120&quality=1080p region=us-west-2 +2026-07-31T09:14:03.114Z,app-worker-07,checkout-worker,WARN,WARN checkout retry scheduled attempt=2 queue=payments +2026-07-31T09:14:03.287Z,edge-proxy-01,redirector,INFO,level=info service=redirector status=301 location=http://example.com latency_ms=2 +2026-07-31T09:14:03.460Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:03.633Z,admin-01,auditd,INFO,audit actor=alice action=view target=https://admin.example.com/users/8675309#permissions result=allow +2026-07-31T09:14:03.806Z,control-01,scheduler,DEBUG,level=debug component=scheduler queue_depth=0 workers=32 +2026-07-31T09:14:03.979Z,mesh-sidecar-19,service-mesh,INFO,proxy upstream selected cluster=orders endpoint=http://orders.default.svc.cluster.local:8080/health retry=0 +2026-07-31T09:14:04.152Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:04.325Z,billing-05,billing-api,INFO,billing request tenant=42 invoice=2026-07 url=https://billing.example.com/invoices/2026-07?download=true status=200 +2026-07-31T09:14:04.498Z,app-api-12,inventory-api,ERROR,level=error service=inventory msg=upstream_timeout attempt=3 +2026-07-31T09:14:04.671Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:04.844Z,docs-01,nginx,INFO,docs access ref=homepage destination=https://docs.example.org/guides/url-parsing#query-parameters status=200 +2026-07-31T09:14:05.017Z,app-worker-07,checkout-worker,WARN,WARN checkout retry scheduled attempt=2 queue=payments +2026-07-31T09:14:05.190Z,ingest-08,event-ingest,INFO,event accepted batch=842 source=mobile callback=https://events.example.com/v3/batch?source=mobile&sdk=6.4.1 status=202 +2026-07-31T09:14:05.363Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:05.536Z,control-01,scheduler,DEBUG,level=debug component=scheduler queue_depth=0 workers=32 +2026-07-31T09:14:05.709Z,legacy-02,legacy-api,INFO,legacy request client=198.51.100.159 url=http://192.0.2.200:8000/v1/status?format=json method=GET status=200 +2026-07-31T09:14:05.882Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:06.055Z,app-worker-07,checkout-worker,WARN,WARN checkout retry scheduled attempt=2 queue=payments +2026-07-31T09:14:06.228Z,download-03,artifact-service,INFO,download complete artifact=cudf target=https://downloads.example.com/releases/cudf-26.10.tar.gz?signature=redacted#sha256 bytes=48234412 +2026-07-31T09:14:06.401Z,app-api-12,inventory-api,ERROR,level=error service=inventory msg=upstream_timeout attempt=3 +2026-07-31T09:14:06.574Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 +2026-07-31T09:14:06.747Z,control-01,scheduler,DEBUG,level=debug component=scheduler queue_depth=0 workers=32 +2026-07-31T09:14:06.920Z,edge-proxy-03,rfc-fixture,INFO,"rfc authority test url=https://user:pass@example.com:8443/a,b;c?x=/a?b#frag?part result=ok" +2026-07-31T09:14:07.093Z,directory-01,rfc-fixture,INFO,rfc ip-literal target=ldap://[2001:db8::7]/c=GB?objectClass?one result=ok +2026-07-31T09:14:07.266Z,future-net-01,rfc-fixture,INFO,rfc ipvfuture endpoint=https://[v1.fe80::a]:9443/resource result=ok +2026-07-31T09:14:07.439Z,node-17,rfc-fixture,INFO,rfc empty-authority resource=file:///etc/hosts result=ok +2026-07-31T09:14:07.612Z,edge-proxy-03,rfc-fixture,INFO,rfc pct-encoded-host url=https://example%2Ecom/a%20b result=ok diff --git a/cpp/examples/string_transforms/url_logs/transforms.cpp b/cpp/examples/string_transforms/url_logs/transforms.cpp new file mode 100644 index 000000000000..171459a047ce --- /dev/null +++ b/cpp/examples/string_transforms/url_logs/transforms.cpp @@ -0,0 +1,566 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr auto output_count = std::size_t{6}; + +// Shared CUDA source inserted into both runtime-compiled UDF bodies. +constexpr char parse_url_udf[] = R"***( + struct range32 { + int32_t begin{}; + int32_t end{}; + }; + struct url_ranges { + range32 protocol; + range32 host; + range32 port; + range32 path; + range32 query; + range32 fragment; + }; + // Parses the first URL candidate and records byte ranges for all six components. + auto const parse_url = [&](url_ranges* out) { + *out = {}; + auto const n = input.size_bytes(); + auto const is_alpha = [](char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + }; + auto const is_digit = [](char c) { return c >= '0' && c <= '9'; }; + auto const is_scheme_char = [&](char c) { + return is_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.'; + }; + auto const is_hex = [&](char c) { + return is_digit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + }; + auto const is_unreserved = [&](char c) { + return is_alpha(c) || is_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'; + }; + auto const is_sub_delim = [](char c) { + return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || + c == '+' || c == ',' || c == ';' || c == '='; + }; + auto const is_gen_delim = [](char c) { + return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'; + }; + auto const is_context_delimiter = [](char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '"' || c == '<' || + c == '>'; + }; + + auto scheme_end = n; + for (auto i = 1; i + 2 < n; ++i) { + if (input.data()[i] == ':' && input.data()[i + 1] == '/' && input.data()[i + 2] == '/') { + scheme_end = i; + break; + } + } + if (scheme_end == n) { return false; } + + auto url_begin = scheme_end; + while (url_begin > 0 && is_scheme_char(input.data()[url_begin - 1])) { --url_begin; } + if (url_begin == scheme_end || !is_alpha(input.data()[url_begin])) { return false; } + + auto url_end = n; + for (auto i = scheme_end + 3; i < n; ++i) { + if (is_context_delimiter(input.data()[i])) { + url_end = i; + break; + } + } + for (auto i = url_begin; i < url_end; ++i) { + auto const c = input.data()[i]; + if (c == '%') { + if (i + 2 >= url_end || !is_hex(input.data()[i + 1]) || !is_hex(input.data()[i + 2])) { + return false; + } + i += 2; + } else if (!is_unreserved(c) && !is_sub_delim(c) && !is_gen_delim(c)) { + return false; + } + } + + auto hash = url_end; + for (auto i = scheme_end + 3; i < url_end; ++i) { + if (input.data()[i] == '#') { + hash = i; + break; + } + } + auto question = hash; + for (auto i = scheme_end + 3; i < hash; ++i) { + if (input.data()[i] == '?') { + question = i; + break; + } + } + auto const base_end = question < hash ? question : hash; + out->protocol = {url_begin, scheme_end}; + if (question < hash) { out->query = {question + 1, hash}; } + if (hash < url_end) { out->fragment = {hash + 1, url_end}; } + + auto const authority_begin = scheme_end + 3; + auto authority_end = base_end; + for (auto i = authority_begin; i < base_end; ++i) { + if (input.data()[i] == '/') { + authority_end = i; + break; + } + } + out->path = {authority_end, base_end}; + + auto host_begin = authority_begin; + for (auto i = authority_begin; i < authority_end; ++i) { + if (input.data()[i] == '@') { host_begin = i + 1; } + } + if (host_begin < authority_end && input.data()[host_begin] == '[') { + auto close = authority_end; + for (auto i = host_begin + 1; i < authority_end; ++i) { + if (input.data()[i] == ']') { + close = i; + break; + } + } + if (close == authority_end) { return false; } + out->host = {host_begin, close + 1}; + if (close + 1 < authority_end) { + if (input.data()[close + 1] != ':') { return false; } + out->port = {close + 2, authority_end}; + } + } else { + auto colon = authority_end; + for (auto i = host_begin; i < authority_end; ++i) { + if (input.data()[i] == ':') { colon = i; } + } + out->host = {host_begin, colon}; + if (colon < authority_end) { out->port = {colon + 1, authority_end}; } + } + for (auto i = out->port.begin; i < out->port.end; ++i) { + if (!is_digit(input.data()[i])) { return false; } + } + return true; + }; +)***"; + +// Builds the sizing UDF by inserting the shared parser into a self-contained device function. +std::string const url_component_sizes_udf = std::string{R"***( +// Computes exact output byte counts for the six URL component columns. +__device__ int compute_url_component_sizes(int32_t* protocol_size, + int32_t* host_size, + int32_t* port_size, + int32_t* path_size, + int32_t* query_size, + int32_t* fragment_size, + cudf::string_view input) { + *protocol_size = *host_size = *port_size = 0; + *path_size = *query_size = *fragment_size = 0; +)***"} + parse_url_udf + R"***( + url_ranges ranges; + if (!parse_url(&ranges)) { return 0; } + *protocol_size = ranges.protocol.end - ranges.protocol.begin; + *host_size = ranges.host.end - ranges.host.begin; + *port_size = ranges.port.end - ranges.port.begin; + *path_size = ranges.path.end - ranges.path.begin; + *query_size = ranges.query.end - ranges.query.begin; + *fragment_size = ranges.fragment.end - ranges.fragment.begin; + return 0; +} +)***"; + +// Builds the output UDF from the same parser so both CUDA passes use identical ranges. +std::string const url_component_output_udf = std::string{R"***( +// Copies the six parsed URL components into their preallocated string buffers. +__device__ int write_url_components(cuda::std::span* protocol, + cuda::std::span* host, + cuda::std::span* port, + cuda::std::span* path, + cuda::std::span* query, + cuda::std::span* fragment, + cudf::string_view input) { +)***"} + parse_url_udf + R"***( + url_ranges ranges; + if (!parse_url(&ranges)) { return 0; } + cuda::std::span* outputs[] = {protocol, host, port, path, query, fragment}; + range32 components[] = { + ranges.protocol, ranges.host, ranges.port, ranges.path, ranges.query, ranges.fragment}; + for (auto component = 0; component < 6; ++component) { + auto const range = components[component]; + auto const size = range.end - range.begin; + if (size > 0) { memcpy(outputs[component]->data(), input.data() + range.begin, size); } + } + return 0; +} +)***"; + +constexpr std::string_view usage = + "usage: url_log_transforms INPUT.csv OUTPUT.csv ROWS ITERATIONS\n" + " url_log_transforms \n"; + +// Extracts RFC 3986-style hierarchical URI components from unstructured log lines. +[[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // Derived from RFC 3986 Appendix B. The authority capture is expanded into optional + // userinfo plus host and port, and Appendix C delimiters bound the URI within a log line. + static auto program = cudf::strings::regex_program::create( + R"((?:^|[^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"<>]))"); + auto extracted = cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); + auto columns = extracted->release(); + auto empty = cudf::string_scalar{"", true, stream, mr}; + for (auto& column : columns) { + column = cudf::replace_nulls(column->view(), empty, stream, mr); + } + return std::make_unique(std::move(columns)); +} + +// Decomposes key-value URL tokens using only precompiled libcudf string primitives. +[[nodiscard]] std::unique_ptr run_precompiled(cudf::column_view input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + // Materialize the delimiters used by each partitioning stage. + auto empty = cudf::string_scalar{"", true, stream, mr}; + auto scheme_separator = cudf::string_scalar{"://", true, stream, mr}; + auto marker_separator = cudf::string_scalar{"=", true, stream, mr}; + auto token_separator = cudf::string_scalar{" ", true, stream, mr}; + auto hash = cudf::string_scalar{"#", true, stream, mr}; + auto question = cudf::string_scalar{"?", true, stream, mr}; + auto slash = cudf::string_scalar{"/", true, stream, mr}; + auto at = cudf::string_scalar{"@", true, stream, mr}; + auto right_bracket = cudf::string_scalar{"]", true, stream, mr}; + auto left_bracket = cudf::string_scalar{"[", true, stream, mr}; + auto colon = cudf::string_scalar{":", true, stream, mr}; + + // Mark rows containing an authority-style URI and split at the first "://". + auto has_url = + cudf::strings::contains(cudf::strings_column_view{input}, scheme_separator, stream, mr); + auto scheme_table = + cudf::strings::partition(cudf::strings_column_view{input}, scheme_separator, stream, mr); + auto scheme_columns = scheme_table->release(); + + // Extract the scheme from the key-value token immediately preceding "://". + auto marker_table = cudf::strings::rpartition( + cudf::strings_column_view{scheme_columns[0]->view()}, marker_separator, stream, mr); + auto marker_columns = marker_table->release(); + + // Stop at the first space so later log fields are excluded from the URI. + auto token_table = cudf::strings::partition( + cudf::strings_column_view{scheme_columns[2]->view()}, token_separator, stream, mr); + auto token_columns = token_table->release(); + + // Split off the fragment; everything after the first '#' belongs to it. + auto fragment_table = + cudf::strings::partition(cudf::strings_column_view{token_columns[0]->view()}, hash, stream, mr); + auto fragment_columns = fragment_table->release(); + + // Split the pre-fragment portion at the first '?' to isolate the query. + auto query_table = cudf::strings::partition( + cudf::strings_column_view{fragment_columns[0]->view()}, question, stream, mr); + auto query_columns = query_table->release(); + + // Split the remaining hierarchical part at its first slash into authority and path. + auto authority_path_table = cudf::strings::partition( + cudf::strings_column_view{query_columns[0]->view()}, slash, stream, mr); + auto authority_path_columns = authority_path_table->release(); + + // Reattach the slash delimiter to produce the RFC path value. + auto path = cudf::strings::concatenate( + cudf::table_view{{authority_path_columns[1]->view(), authority_path_columns[2]->view()}}, + empty, + cudf::string_scalar{"", false, stream, mr}, + cudf::strings::separator_on_nulls::YES, + stream, + mr); + + // Remove optional userinfo by retaining everything after the authority's last '@'. + auto has_userinfo = cudf::strings::contains( + cudf::strings_column_view{authority_path_columns[0]->view()}, at, stream, mr); + auto userinfo_table = cudf::strings::rpartition( + cudf::strings_column_view{authority_path_columns[0]->view()}, at, stream, mr); + auto userinfo_columns = userinfo_table->release(); + auto host_port = cudf::copy_if_else(userinfo_columns[2]->view(), + authority_path_columns[0]->view(), + has_userinfo->view(), + stream, + mr); + + // Bracketed IP literals and regular hosts require different port splitting rules. + auto is_ip_literal = cudf::strings::starts_with( + cudf::strings_column_view{host_port->view()}, left_bracket, stream, mr); + auto bracket_table = cudf::strings::partition( + cudf::strings_column_view{host_port->view()}, right_bracket, stream, mr); + auto bracket_columns = bracket_table->release(); + + // Preserve both brackets as part of an IP-literal host. + auto bracket_host = cudf::strings::concatenate( + cudf::table_view{{bracket_columns[0]->view(), bracket_columns[1]->view()}}, + empty, + cudf::string_scalar{"", false, stream, mr}, + cudf::strings::separator_on_nulls::YES, + stream, + mr); + + // For an IP literal, parse an optional port only after the closing bracket. + auto bracket_port_table = cudf::strings::partition( + cudf::strings_column_view{bracket_columns[2]->view()}, colon, stream, mr); + auto bracket_port_columns = bracket_port_table->release(); + + // For a regular authority, treat the final colon as the port separator. + auto has_regular_port = + cudf::strings::contains(cudf::strings_column_view{host_port->view()}, colon, stream, mr); + auto regular_table = + cudf::strings::rpartition(cudf::strings_column_view{host_port->view()}, colon, stream, mr); + auto regular_columns = regular_table->release(); + auto regular_host = cudf::copy_if_else( + regular_columns[0]->view(), host_port->view(), has_regular_port->view(), stream, mr); + auto regular_port = + cudf::copy_if_else(regular_columns[2]->view(), empty, has_regular_port->view(), stream, mr); + + // Select the bracketed or regular host/port result for each row. + auto host = cudf::copy_if_else( + bracket_host->view(), regular_host->view(), is_ip_literal->view(), stream, mr); + auto port = cudf::copy_if_else( + bracket_port_columns[2]->view(), regular_port->view(), is_ip_literal->view(), stream, mr); + + // Convert missing components to empty strings and blank rows without a URL. + auto normalize = [&](cudf::column_view column) { + auto no_nulls = cudf::replace_nulls(column, empty, stream, mr); + return cudf::copy_if_else(no_nulls->view(), empty, has_url->view(), stream, mr); + }; + + // Return the six columns in the same order used by the regex and CUDA implementations. + std::vector> result; + result.reserve(output_count); + result.push_back(normalize(marker_columns[2]->view())); + result.push_back(normalize(host->view())); + result.push_back(normalize(port->view())); + result.push_back(normalize(path->view())); + result.push_back(normalize(query_columns[2]->view())); + result.push_back(normalize(fragment_columns[2]->view())); + return std::make_unique(std::move(result)); +} + +// Runs either the runtime-compiled CUDA-string UDFs or their AOT fatbin/LTO counterparts. +[[nodiscard]] std::unique_ptr run_jit(cudf::column_view input, + bool use_lto, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + cudf::transform_output const size_spec{cudf::data_type{cudf::type_id::INT32}, + cudf::output_nullability::ALL_VALID}; + std::vector const size_outputs(output_count, size_spec); + cudf::transform_input inputs[] = {input}; + std::unique_ptr sizes; + + if (use_lto) { + auto range = url_log_fragments::file_ranges[url_log_fragments::url_component_sizes]; + auto fragment = url_log_fragments::files.subspan(range[0], range[1]); + sizes = cudf::transform_lto(fragment, + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + size_outputs, + {}, + std::nullopt, + stream, + mr); + } else { + sizes = cudf::multi_transform(url_component_sizes_udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + size_outputs, + {}, + std::nullopt, + stream, + mr); + } + + std::vector> offsets; + offsets.reserve(output_count); + for (auto& string_sizes : sizes->view()) { + auto run_ends = cudf::scan(string_sizes, + *cudf::make_sum_aggregation(), + cudf::scan_type::INCLUSIVE, + cudf::null_policy::EXCLUDE, + stream, + mr); + auto zero = cudf::numeric_scalar{0, true, stream, mr}; + auto first = cudf::make_column_from_scalar(zero, 1, stream, mr); + offsets.push_back(cudf::concatenate( + std::vector{first->view(), run_ends->view()}, stream, mr)); + } + + cudf::transform_output const output_spec{cudf::data_type{cudf::type_id::STRING}, + cudf::output_nullability::ALL_VALID}; + std::vector const outputs(output_count, output_spec); + if (use_lto) { + auto range = url_log_fragments::file_ranges[url_log_fragments::url_component_output]; + auto fragment = url_log_fragments::files.subspan(range[0], range[1]); + return cudf::transform_lto(fragment, + cudf::lto_binary_type::FATBIN, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + std::move(offsets), + std::nullopt, + stream, + mr); + } + return cudf::multi_transform(url_component_output_udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + std::move(offsets), + std::nullopt, + stream, + mr); +} + +} // namespace + +int main(int argc, char const** argv) +{ + try { + if (argc == 2 && + (std::string_view{argv[1]} == "--help" || std::string_view{argv[1]} == "usage")) { + std::cout << usage; + return EXIT_SUCCESS; + } + if (argc != 6) { + throw std::invalid_argument("invalid arguments; run url_log_transforms --help for usage"); + } + + auto input_path = std::string{argv[1]}; + auto output_path = std::string{argv[2]}; + auto implementation = std::string_view{argv[3]}; + if (implementation != "regex" && implementation != "precompiled" && implementation != "jit" && + implementation != "lto") { + throw std::invalid_argument("variant must be regex, precompiled, jit, or lto"); + } + auto requested_rows = std::stoll(argv[4]); + auto iterations = std::stoi(argv[5]); + if (requested_rows < 0 || requested_rows > std::numeric_limits::max()) { + throw std::invalid_argument("ROWS is outside the cudf::size_type range"); + } + if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } + + auto rows = static_cast(requested_rows); + auto use_lto = implementation == "lto"; + auto stream = cudf::get_default_stream(); + auto mr = cudf::get_current_device_resource_ref(); + auto read_options = cudf::io::csv_reader_options::builder(cudf::io::source_info{input_path}) + .header(0) + .use_cols_names({"LogLine"}) + .build(); + auto input = cudf::io::read_csv(read_options).tbl; + if (rows != input->num_rows()) { + input = cudf::sample(input->view(), rows, cudf::sample_with_replacement::TRUE); + } + + auto input_bytes = input->get_column(0).alloc_size(); + auto input_view = input->get_column(0).view(); + rmm::mr::statistics_resource_adaptor stats{mr}; + auto stats_mr = rmm::device_async_resource_ref{stats}; + auto run_transform = [&]() { + if (implementation == "regex") { return run_regex(input_view, stream, stats_mr); } + if (implementation == "precompiled") { return run_precompiled(input_view, stream, stats_mr); } + return run_jit(input_view, use_lto, stream, stats_mr); + }; + + stream.synchronize(); + auto cold_start = std::chrono::steady_clock::now(); + nvtxRangePush("url_log_cold"); + auto cold_result = run_transform(); + stream.synchronize(); + nvtxRangePop(); + auto cold_seconds = + std::chrono::duration{std::chrono::steady_clock::now() - cold_start}.count(); + cold_result.reset(); + + std::unique_ptr result; + auto warm_start = std::chrono::steady_clock::now(); + nvtxRangePush("url_log_warm"); + for (auto i = 0; i < iterations; ++i) { + result.reset(); + result = run_transform(); + } + stream.synchronize(); + nvtxRangePop(); + auto warm_seconds = + std::chrono::duration{std::chrono::steady_clock::now() - warm_start}.count() / + iterations; + + if (output_path != "-") { + auto write_options = + cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result->view()) + .include_header(true) + .names({"protocol", "host", "port", "path", "query", "fragment"}) + .build(); + cudf::io::write_csv(write_options); + } + + auto bytes = stats.get_bytes_counter(); + auto output_bytes = result->alloc_size(); + auto gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); + std::cout << std::format( + "variant={}\nrows={}\ncold_seconds={}\nwarm_seconds={}\nrows_per_second={}\neffective_gib_" + "per_second={}\ninput_bytes={}\noutput_bytes={}\npeak_memory_bytes={}\ntotal_allocated_bytes=" + "{}\nallocated_bytes_per_call={}\n", + implementation, + rows, + cold_seconds, + warm_seconds, + static_cast(rows) / warm_seconds, + gib / warm_seconds, + input_bytes, + output_bytes, + bytes.peak, + bytes.total, + bytes.total / static_cast(iterations + 1)); + return EXIT_SUCCESS; + } catch (std::exception const& error) { + std::cerr << error.what() << '\n'; + return EXIT_FAILURE; + } +} From b1def55b247d29ee1e41dd91b2a8753f5bea769b Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 3 Aug 2026 22:18:22 +0000 Subject: [PATCH 24/27] Refactor CMakeLists.txt for string kernel fragments and simplify url_log_fragments definition --- cpp/CMakeLists.txt | 4 +++- cpp/examples/string_transforms/CMakeLists.txt | 13 ++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index f3b9d3dc3e42..0aa00829da92 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -618,7 +618,9 @@ function(precompile_string_kernel_fragments) CUDF_PRECOMPILE_STRING_OUTPUT_COLUMN_TYPES ) # Pre-compile for up to 8 output columns - foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3" "0;1;2;3;4" "0;1;2;3;4;5" "0;1;2;3;4;5;6" "0;1;2;3;4;5;6;7") + foreach(OUTPUT_INDICES IN ITEMS "0" "0;1" "0;1;2" "0;1;2;3" "0;1;2;3;4" "0;1;2;3;4;5" + "0;1;2;3;4;5;6" "0;1;2;3;4;5;6;7" + ) set(FRAGMENT_NAME transform_kernel) get_property( FILE_INDEX diff --git a/cpp/examples/string_transforms/CMakeLists.txt b/cpp/examples/string_transforms/CMakeLists.txt index 5ad329604bd2..3156e0366769 100644 --- a/cpp/examples/string_transforms/CMakeLists.txt +++ b/cpp/examples/string_transforms/CMakeLists.txt @@ -74,17 +74,8 @@ add_fragment( cudf::cudf ) add_fragment( - url_log_fragments - FRAGMENT - url_component_output - SOURCE - url_logs/fragments.cu - DEFINITIONS - UDF_WRITE_OUTPUT - INCLUDE_DIRECTORIES - ${CMAKE_CURRENT_LIST_DIR}/url_logs - LINK_LIBRARIES - cudf::cudf + url_log_fragments FRAGMENT url_component_output SOURCE url_logs/fragments.cu DEFINITIONS + UDF_WRITE_OUTPUT INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/url_logs LINK_LIBRARIES cudf::cudf ) rtcx_embed(url_log_fragments COMPRESSION none OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/embed") From b6feb73289035af81c7efecc88238460e100c204 Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 3 Aug 2026 22:22:54 +0000 Subject: [PATCH 25/27] Update download URL in logs.csv to use the latest package version --- cpp/examples/string_transforms/url_logs/logs.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/examples/string_transforms/url_logs/logs.csv b/cpp/examples/string_transforms/url_logs/logs.csv index dbc29149f21a..a58d41c36bbd 100644 --- a/cpp/examples/string_transforms/url_logs/logs.csv +++ b/cpp/examples/string_transforms/url_logs/logs.csv @@ -35,7 +35,7 @@ Timestamp,Host,Source,Level,LogLine 2026-07-31T09:14:05.709Z,legacy-02,legacy-api,INFO,legacy request client=198.51.100.159 url=http://192.0.2.200:8000/v1/status?format=json method=GET status=200 2026-07-31T09:14:05.882Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 2026-07-31T09:14:06.055Z,app-worker-07,checkout-worker,WARN,WARN checkout retry scheduled attempt=2 queue=payments -2026-07-31T09:14:06.228Z,download-03,artifact-service,INFO,download complete artifact=cudf target=https://downloads.example.com/releases/cudf-26.10.tar.gz?signature=redacted#sha256 bytes=48234412 +2026-07-31T09:14:06.228Z,download-03,artifact-service,INFO,download complete artifact=cudf target=https://downloads.example.com/releases/package.tar.gz?signature=redacted#sha256 bytes=48234412 2026-07-31T09:14:06.401Z,app-api-12,inventory-api,ERROR,level=error service=inventory msg=upstream_timeout attempt=3 2026-07-31T09:14:06.574Z,k8s-worker-03,kubelet,INFO,level=info service=kube-probe msg=healthy status=200 2026-07-31T09:14:06.747Z,control-01,scheduler,DEBUG,level=debug component=scheduler queue_depth=0 workers=32 From b312a1ce759e99b50494c963112727ae3267428d Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Thu, 20 Aug 2026 12:40:47 +0000 Subject: [PATCH 26/27] improve benchmark setup --- .../string_transforms/url_logs/transforms.cpp | 350 +++++++++++------- 1 file changed, 218 insertions(+), 132 deletions(-) diff --git a/cpp/examples/string_transforms/url_logs/transforms.cpp b/cpp/examples/string_transforms/url_logs/transforms.cpp index 171459a047ce..91b2420d7c42 100644 --- a/cpp/examples/string_transforms/url_logs/transforms.cpp +++ b/cpp/examples/string_transforms/url_logs/transforms.cpp @@ -54,30 +54,30 @@ constexpr char parse_url_udf[] = R"***( range32 fragment; }; // Parses the first URL candidate and records byte ranges for all six components. - auto const parse_url = [&](url_ranges* out) { + auto parse_url = [&](url_ranges* out) { *out = {}; - auto const n = input.size_bytes(); - auto const is_alpha = [](char c) { + auto n = input.size_bytes(); + auto is_alpha = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); }; - auto const is_digit = [](char c) { return c >= '0' && c <= '9'; }; - auto const is_scheme_char = [&](char c) { + auto is_digit = [](char c) { return c >= '0' && c <= '9'; }; + auto is_scheme_char = [&](char c) { return is_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.'; }; - auto const is_hex = [&](char c) { + auto is_hex = [&](char c) { return is_digit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); }; - auto const is_unreserved = [&](char c) { + auto is_unreserved = [&](char c) { return is_alpha(c) || is_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'; }; - auto const is_sub_delim = [](char c) { + auto is_sub_delim = [](char c) { return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '='; }; - auto const is_gen_delim = [](char c) { + auto is_gen_delim = [](char c) { return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'; }; - auto const is_context_delimiter = [](char c) { + auto is_context_delimiter = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '"' || c == '<' || c == '>'; }; @@ -103,7 +103,7 @@ constexpr char parse_url_udf[] = R"***( } } for (auto i = url_begin; i < url_end; ++i) { - auto const c = input.data()[i]; + auto c = input.data()[i]; if (c == '%') { if (i + 2 >= url_end || !is_hex(input.data()[i + 1]) || !is_hex(input.data()[i + 2])) { return false; @@ -128,12 +128,12 @@ constexpr char parse_url_udf[] = R"***( break; } } - auto const base_end = question < hash ? question : hash; + auto base_end = question < hash ? question : hash; out->protocol = {url_begin, scheme_end}; if (question < hash) { out->query = {question + 1, hash}; } if (hash < url_end) { out->fragment = {hash + 1, url_end}; } - auto const authority_begin = scheme_end + 3; + auto authority_begin = scheme_end + 3; auto authority_end = base_end; for (auto i = authority_begin; i < base_end; ++i) { if (input.data()[i] == '/') { @@ -218,8 +218,8 @@ __device__ int write_url_components(cuda::std::span* protocol, range32 components[] = { ranges.protocol, ranges.host, ranges.port, ranges.path, ranges.query, ranges.fragment}; for (auto component = 0; component < 6; ++component) { - auto const range = components[component]; - auto const size = range.end - range.begin; + auto range = components[component]; + auto size = range.end - range.begin; if (size > 0) { memcpy(outputs[component]->data(), input.data() + range.begin, size); } } return 0; @@ -227,16 +227,47 @@ __device__ int write_url_components(cuda::std::span* protocol, )***"; constexpr std::string_view usage = - "usage: url_log_transforms INPUT.csv OUTPUT.csv ROWS ITERATIONS\n" + "usage: url_log_transforms INPUT.csv OUTPUT.csv ROWS\n" + " url_log_transforms INPUT.csv OUTPUT.csv ROWS " + "<--warm|--cold|--cold-warm-pch>\n" " url_log_transforms \n"; +// warmup the PCH cache +void warmup_pch(cudf::column_view input, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + constexpr char udf[] = R"***( +__device__ int transform(int32_t* output, cudf::string_view input) { + *output = input.size_bytes(); + return 0; +} +)***"; + cudf::transform_input inputs[] = {input}; + cudf::transform_output const output{cudf::data_type{cudf::type_id::INT32}, + cudf::output_nullability::ALL_VALID}; + std::vector const outputs{output}; + auto result = cudf::transform(udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + {}, + std::nullopt, + stream, + mr); + stream.synchronize(); +} + // Extracts RFC 3986-style hierarchical URI components from unstructured log lines. [[nodiscard]] std::unique_ptr run_regex(cudf::column_view input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - // Derived from RFC 3986 Appendix B. The authority capture is expanded into optional - // userinfo plus host and port, and Appendix C delimiters bound the URI within a log line. + // Derived from RFC 3986 Appendix B (https://www.rfc-editor.org/info/rfc3986/#page-50). The + // authority capture is expanded into optional userinfo plus host and port, and Appendix C + // delimiters bound the URI within a log line. static auto program = cudf::strings::regex_program::create( R"((?:^|[^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"<>]))"); auto extracted = cudf::strings::extract(cudf::strings_column_view{input}, *program, stream, mr); @@ -401,16 +432,16 @@ constexpr std::string_view usage = stream, mr); } else { - sizes = cudf::multi_transform(url_component_sizes_udf, - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - size_outputs, - {}, - std::nullopt, - stream, - mr); + sizes = cudf::transform(url_component_sizes_udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + size_outputs, + {}, + std::nullopt, + stream, + mr); } std::vector> offsets; @@ -445,122 +476,177 @@ constexpr std::string_view usage = stream, mr); } - return cudf::multi_transform(url_component_output_udf, - cudf::udf_source_type::CUDA, - cudf::null_aware::NO, - std::nullopt, - inputs, - outputs, - std::move(offsets), - std::nullopt, - stream, - mr); + return cudf::transform(url_component_output_udf, + cudf::udf_source_type::CUDA, + cudf::null_aware::NO, + std::nullopt, + inputs, + outputs, + std::move(offsets), + std::nullopt, + stream, + mr); } } // namespace int main(int argc, char const** argv) -{ - try { - if (argc == 2 && - (std::string_view{argv[1]} == "--help" || std::string_view{argv[1]} == "usage")) { - std::cout << usage; - return EXIT_SUCCESS; - } - if (argc != 6) { - throw std::invalid_argument("invalid arguments; run url_log_transforms --help for usage"); - } +try { + if (argc == 2 && + (std::string_view{argv[1]} == "--help" || std::string_view{argv[1]} == "usage")) { + std::cout << usage; + return EXIT_SUCCESS; + } + if (argc != 5 && argc != 6) { + throw std::invalid_argument("invalid arguments; run url_log_transforms --help for usage"); + } - auto input_path = std::string{argv[1]}; - auto output_path = std::string{argv[2]}; - auto implementation = std::string_view{argv[3]}; - if (implementation != "regex" && implementation != "precompiled" && implementation != "jit" && - implementation != "lto") { - throw std::invalid_argument("variant must be regex, precompiled, jit, or lto"); - } - auto requested_rows = std::stoll(argv[4]); - auto iterations = std::stoi(argv[5]); - if (requested_rows < 0 || requested_rows > std::numeric_limits::max()) { - throw std::invalid_argument("ROWS is outside the cudf::size_type range"); - } - if (iterations < 1) { throw std::invalid_argument("ITERATIONS must be positive"); } - - auto rows = static_cast(requested_rows); - auto use_lto = implementation == "lto"; - auto stream = cudf::get_default_stream(); - auto mr = cudf::get_current_device_resource_ref(); - auto read_options = cudf::io::csv_reader_options::builder(cudf::io::source_info{input_path}) - .header(0) - .use_cols_names({"LogLine"}) - .build(); - auto input = cudf::io::read_csv(read_options).tbl; - if (rows != input->num_rows()) { - input = cudf::sample(input->view(), rows, cudf::sample_with_replacement::TRUE); + auto input_path = std::string{argv[1]}; + auto output_path = std::string{argv[2]}; + auto impl = std::string_view{argv[3]}; + if (impl != "regex" && impl != "precompiled" && impl != "cuda-jit" && impl != "lto-jit") { + throw std::invalid_argument("executor must be regex, precompiled, cuda-jit, or lto-jit"); + } + auto requested_rows = std::stoll(argv[4]); + auto is_jit = impl == "cuda-jit" || impl == "lto-jit"; + if (is_jit && argc != 6) { + throw std::invalid_argument("cuda-jit and lto-jit require a warm-up control"); + } + if (!is_jit && argc != 5) { + throw std::invalid_argument("regex and precompiled do not accept a warm-up control"); + } + auto warmup_control = argc == 6 ? std::string_view{argv[5]} : std::string_view{"none"}; + if (is_jit && warmup_control != "--warm" && warmup_control != "--cold" && + warmup_control != "--cold-warm-pch") { + throw std::invalid_argument("warm-up control must be --warm, --cold, or --cold-warm-pch"); + } + if (requested_rows < 0 || requested_rows > std::numeric_limits::max()) { + throw std::invalid_argument("ROWS is outside the cudf::size_type range"); + } + nvtxRangePush("url_log_process"); + auto process_start = std::chrono::steady_clock::now(); + auto rows = static_cast(requested_rows); + auto use_lto = impl == "lto-jit"; + auto stream = cudf::get_default_stream(); + auto upstream_mr = cudf::get_current_device_resource_ref(); + // Tracks setup, measured work, and output + rmm::mr::statistics_resource_adaptor whole_stats{upstream_mr}; + auto whole_mr = rmm::device_async_resource_ref{whole_stats}; + cudf::set_current_device_resource(whole_mr); + + nvtxRangePush("url_log_setup"); + auto read_options = cudf::io::csv_reader_options::builder(cudf::io::source_info{input_path}) + .header(0) + .use_cols_names({"LogLine"}) + .build(); + auto input = cudf::io::read_csv(read_options).tbl; + if (rows != input->num_rows()) { + input = + cudf::sample(input->view(), rows, cudf::sample_with_replacement::TRUE, 0, stream, whole_mr); + } + stream.synchronize(); + auto input_view = input->get_column(0).view(); + auto logical_input_bytes = cudf::strings_column_view{input_view}.chars_size(stream); + nvtxRangePop(); + + // Tracks measured work; nested allocations also update whole_stats. + rmm::mr::statistics_resource_adaptor measured_stats{whole_mr}; + auto measured_mr = rmm::device_async_resource_ref{measured_stats}; + auto run_transform = [&](rmm::device_async_resource_ref mr) { + if (impl == "regex") { + return run_regex(input_view, stream, mr); + } else if (impl == "precompiled") { + return run_precompiled(input_view, stream, mr); + } else { + return run_jit(input_view, use_lto, stream, mr); } + }; - auto input_bytes = input->get_column(0).alloc_size(); - auto input_view = input->get_column(0).view(); - rmm::mr::statistics_resource_adaptor stats{mr}; - auto stats_mr = rmm::device_async_resource_ref{stats}; - auto run_transform = [&]() { - if (implementation == "regex") { return run_regex(input_view, stream, stats_mr); } - if (implementation == "precompiled") { return run_precompiled(input_view, stream, stats_mr); } - return run_jit(input_view, use_lto, stream, stats_mr); - }; + std::unique_ptr result; + auto warmup_duration = std::chrono::steady_clock::duration::zero(); - stream.synchronize(); - auto cold_start = std::chrono::steady_clock::now(); - nvtxRangePush("url_log_cold"); - auto cold_result = run_transform(); - stream.synchronize(); + if (warmup_control == "--cold-warm-pch") { + // Do not track warm-up allocations. + cudf::set_current_device_resource(upstream_mr); + nvtxRangePush("url_log_warmup"); + warmup_pch(input_view, stream, upstream_mr); nvtxRangePop(); - auto cold_seconds = - std::chrono::duration{std::chrono::steady_clock::now() - cold_start}.count(); - cold_result.reset(); - - std::unique_ptr result; - auto warm_start = std::chrono::steady_clock::now(); - nvtxRangePush("url_log_warm"); - for (auto i = 0; i < iterations; ++i) { - result.reset(); - result = run_transform(); - } + cudf::set_current_device_resource(whole_mr); + } else if (warmup_control == "--warm") { + // Do not track warm-up allocations. + cudf::set_current_device_resource(upstream_mr); + stream.synchronize(); + auto warmup_start = std::chrono::steady_clock::now(); + nvtxRangePush("url_log_warmup"); + result = run_transform(upstream_mr); stream.synchronize(); nvtxRangePop(); - auto warm_seconds = - std::chrono::duration{std::chrono::steady_clock::now() - warm_start}.count() / - iterations; - - if (output_path != "-") { - auto write_options = - cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result->view()) - .include_header(true) - .names({"protocol", "host", "port", "path", "query", "fragment"}) - .build(); - cudf::io::write_csv(write_options); - } + warmup_duration = std::chrono::steady_clock::now() - warmup_start; + result.reset(); + cudf::set_current_device_resource(whole_mr); + } - auto bytes = stats.get_bytes_counter(); - auto output_bytes = result->alloc_size(); - auto gib = static_cast(input_bytes + output_bytes) / (1ULL << 30); - std::cout << std::format( - "variant={}\nrows={}\ncold_seconds={}\nwarm_seconds={}\nrows_per_second={}\neffective_gib_" - "per_second={}\ninput_bytes={}\noutput_bytes={}\npeak_memory_bytes={}\ntotal_allocated_bytes=" - "{}\nallocated_bytes_per_call={}\n", - implementation, - rows, - cold_seconds, - warm_seconds, - static_cast(rows) / warm_seconds, - gib / warm_seconds, - input_bytes, - output_bytes, - bytes.peak, - bytes.total, - bytes.total / static_cast(iterations + 1)); - return EXIT_SUCCESS; - } catch (std::exception const& error) { - std::cerr << error.what() << '\n'; - return EXIT_FAILURE; + // Measured allocations update both statistics scopes. + cudf::set_current_device_resource(measured_mr); + stream.synchronize(); + auto measured_start = std::chrono::steady_clock::now(); + nvtxRangePush("url_log_measured"); + result = run_transform(measured_mr); + stream.synchronize(); + nvtxRangePop(); + auto measured_duration = std::chrono::steady_clock::now() - measured_start; + + if (output_path != "-") { + // Exclude output serialization from measured statistics. + cudf::set_current_device_resource(whole_mr); + auto write_options = + cudf::io::csv_writer_options::builder(cudf::io::sink_info{output_path}, result->view()) + .include_header(true) + .names({"protocol", "host", "port", "path", "query", "fragment"}) + .build(); + cudf::io::write_csv(write_options); } + + // Read measured and broader workload scopes separately. + auto measured_bytes = measured_stats.get_bytes_counter(); + auto whole_bytes = whole_stats.get_bytes_counter(); + auto output_allocated_bytes = result->alloc_size(); + auto input_gib = static_cast(logical_input_bytes) / static_cast(1ULL << 30); + auto whole_duration = std::chrono::steady_clock::now() - process_start; + auto warmup_seconds = std::chrono::duration{warmup_duration}.count(); + auto measured_seconds = std::chrono::duration{measured_duration}.count(); + auto whole_seconds = std::chrono::duration{whole_duration}.count(); + std::cout << std::format( + "executor={}\nwarmup_control={}\nrows={}\nwarmup_seconds={}\n" + "measured_cpu_wall_seconds={}\nrows_per_second={}\n" + "input_gib_per_second={}\nlogical_input_bytes={}\noutput_allocated_bytes={}\n" + "peak_memory_bytes={}\n" + "total_allocated_bytes={}\nallocated_bytes_per_call={}\nmeasured_gpu_peak_bytes={}\n" + "measured_gpu_allocation_volume_bytes={}\nwhole_workload_seconds={}\n" + "whole_gpu_peak_bytes={}\nwhole_gpu_allocation_volume_bytes={}\n", + impl, + warmup_control, + rows, + warmup_seconds, + measured_seconds, + static_cast(rows) / measured_seconds, + input_gib / measured_seconds, + logical_input_bytes, + output_allocated_bytes, + measured_bytes.peak, + measured_bytes.total, + measured_bytes.total, + measured_bytes.peak, + measured_bytes.total, + whole_seconds, + whole_bytes.peak, + whole_bytes.total); + result.reset(); + input.reset(); + cudf::set_current_device_resource(upstream_mr); + nvtxRangePop(); + return EXIT_SUCCESS; +} catch (std::exception const& error) { + std::cerr << error.what() << '\n'; + return EXIT_FAILURE; } From 892fed89cae8817bdfb79a2ba9739017214fc77d Mon Sep 17 00:00:00 2001 From: Basit Ayantunde Date: Mon, 24 Aug 2026 17:51:33 +0000 Subject: [PATCH 27/27] Remove memory resource parameter from device_uvector in extract functions --- cpp/src/strings/extract/extract.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/strings/extract/extract.cu b/cpp/src/strings/extract/extract.cu index d3a23360a8b8..06e9b3b35a10 100644 --- a/cpp/src/strings/extract/extract.cu +++ b/cpp/src/strings/extract/extract.cu @@ -83,7 +83,7 @@ std::unique_ptr
extract(strings_column_view const& input, auto const groups = d_prog->group_counts(); CUDF_EXPECTS(groups > 0, "Group indicators not found in regex pattern"); - auto indices = rmm::device_uvector(input.size() * groups, stream, mr); + auto indices = rmm::device_uvector(input.size() * groups, stream); auto d_indices = cudf::detail::device_2dspan(indices, groups); auto const d_strings = column_device_view::create(input.parent(), stream); @@ -158,7 +158,7 @@ std::unique_ptr extract_single(strings_column_view const& input, "group parameter outside the range of capture groups found in the regex pattern", std::invalid_argument); - auto indices = rmm::device_uvector(input.size(), stream, mr); + auto indices = rmm::device_uvector(input.size(), stream); auto const d_strings = column_device_view::create(input.parent(), stream);