Skip to content

Reduce the number of device allocations in the ORC writer - #23433

Open
vuule wants to merge 18 commits into
rapidsai:mainfrom
vuule:orc-writer-reduce-allocations
Open

Reduce the number of device allocations in the ORC writer #23433
vuule wants to merge 18 commits into
rapidsai:mainfrom
vuule:orc-writer-reduce-allocations

Conversation

@vuule

@vuule vuule commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Contributes to #23464

encode_columns allocates one device_uvector per (stripe, stream) pair, so a wide table with many stripes produces thousands of device allocations.
With this PR, we compute the size of every stream up front and suballocate an extent (an aligned byte range within an arena) for each. Each extent is aligned exactly as a fresh RMM allocation would be.

Extent sizes are upper bounds, because the encoded length of an RLE stream is not known until it has been encoded. gather_stripes compacts a stripe's per-rowgroup chunks into a contiguous, tightly sized stream. The encoder output is therefore split across two arenas, by whether an extent might be compacted:

  • transient_buffer - extents whose stripe spans several rowgroups and whose size is a strict upper bound rather than exact. gather_stripes compacts every extent in this arena, which lets the whole arena be released as soon as gathering completes, before compression allocates.
  • persistent_buffer - everything else: single-rowgroup stripes, and multi-rowgroup extents whose size estimate is exact. This arena lives until the encoded data is no longer needed.
  • gathered_buffer - the compaction destination.

Checklist

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

vuule added 2 commits April 30, 2026 21:46
`encode_columns` and `gather_stripes` previously allocated one
`device_uvector<uint8_t>` per (stripe, stream) pair, producing
~1,000 driver allocations each per write_orc call. Replace both
patterns with a single arena per phase plus non-owning
`device_span<uint8_t>` views, holding the encoded and gathered
arenas on the `encoded_data` struct for lifetime management.

Each per-(stripe, stream) region inside an arena starts on a
256-byte boundary (`rmm::CUDA_ALLOCATION_ALIGNMENT`) to match the
alignment that RMM previously gave each independent allocation:
the in-loop alignment fix-up only honors
`compress_required_chunk_alignment(compression)`, which is 1 for
`compression == NONE` and as low as 8 for nvcomp paths, so the
encoder/codec kernels rely on the underlying allocation being
naturally aligned.

Effect on `orc_write_encode` (INTEGRAL_SIGNED, 512 MB):
* `cudaMallocFromPoolAsync` per iter: ~2,135 -> ~88 (-96%).
* pool GPU time:  53.95 ms -> 48.23 ms (-10.6%).
* async GPU time: 104.65 ms -> 48.52 ms (-53.6%).
* async-vs-pool delta: +94% -> +0.6% (within noise).
* peak memory: +1 MiB on a 1.97 GiB run (alignment padding).
* encoded_file_size unchanged.

Made-with: Cursor
@copy-pr-bot

copy-pr-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Jul 24, 2026
Splitting the encoder output into one arena rather than a device_uvector per
(stripe, stream) kept the upper-bound-sized allocation alive through
compression, which raised peak memory by up to 53% on the ORC writer
benchmarks. Partition the output into two arenas instead, by whether a region
is expected to be compacted into the gather buffer: a region is, when its
stripe spans several rowgroups and its size is a strict upper bound rather than
exact. gather_stripes compacts every region in the transient arena as well as
any region it measures a gap in, so the transient arena can be released as soon
as gathering completes.

Predicting which regions carry slack only decides arena placement; whether a
region is compacted is still measured, so a misprediction cannot leave
non-contiguous chunks to be read as a contiguous stream. It only costs the
allocation that would have been released.

Peak memory is now within 0.08% of main on all 109 benchmark configs, and
timings are unchanged against the previous arena layout under the pool
resource.
@vuule

vuule commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Throughput vs main, pool memory resource (default)

ORC_WRITER_NVBENCH, A100 80GB, CUDA 12.9, GPU time, run-to-run noise below 1%. Faster on all 34 configurations:

benchmark configs vs main
orc_write_encode INTEGRAL_SIGNED 4 -2.6% to -7.9%
orc_write_encode FLOAT 4 -2.9% to -12.9%
orc_write_encode DECIMAL 4 -4.3% to -14.2%
orc_write_encode TIMESTAMP 4 -4.9% to -14.6%
orc_write_encode STRING 4 -9.1% to -14.2%
orc_write_encode LIST 4 -0.7% to -1.4%
orc_write_encode STRUCT 4 -22.4% to -34.8%
orc_write 2 -1.1% to -1.4%
orc_chunked_write 4 -0.2% to -0.3%

The saving is roughly fixed per write (3-5 ms on the encode configurations), so it reads large where the write is short and small where other work dominates. STRUCT leads because it has the most streams, and so the most allocations removed.

Per-configuration numbers
benchmark axes main this branch change
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=0 run_length=1 49.2 ms 46.3 ms -5.8%
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=0 run_length=32 40.2 ms 37.0 ms -7.9%
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=1000 run_length=1 120.6 ms 117.5 ms -2.6%
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=1000 run_length=32 42.9 ms 39.9 ms -6.9%
orc_write_encode data_type=FLOAT cardinality=0 run_length=1 37.4 ms 34.2 ms -8.6%
orc_write_encode data_type=FLOAT cardinality=0 run_length=32 25.2 ms 22.0 ms -12.9%
orc_write_encode data_type=FLOAT cardinality=1000 run_length=1 122.6 ms 119.0 ms -2.9%
orc_write_encode data_type=FLOAT cardinality=1000 run_length=32 25.4 ms 22.2 ms -12.6%
orc_write_encode data_type=DECIMAL cardinality=0 run_length=1 62.5 ms 57.3 ms -8.3%
orc_write_encode data_type=DECIMAL cardinality=0 run_length=32 39.3 ms 33.7 ms -14.2%
orc_write_encode data_type=DECIMAL cardinality=1000 run_length=1 132.4 ms 126.6 ms -4.3%
orc_write_encode data_type=DECIMAL cardinality=1000 run_length=32 39.4 ms 33.8 ms -14.2%
orc_write_encode data_type=TIMESTAMP cardinality=0 run_length=1 35.0 ms 29.9 ms -14.6%
orc_write_encode data_type=TIMESTAMP cardinality=0 run_length=32 34.1 ms 29.4 ms -14.0%
orc_write_encode data_type=TIMESTAMP cardinality=1000 run_length=1 109.3 ms 103.9 ms -4.9%
orc_write_encode data_type=TIMESTAMP cardinality=1000 run_length=32 35.2 ms 30.5 ms -13.5%
orc_write_encode data_type=STRING cardinality=0 run_length=1 45.5 ms 41.3 ms -9.1%
orc_write_encode data_type=STRING cardinality=0 run_length=32 45.5 ms 41.3 ms -9.1%
orc_write_encode data_type=STRING cardinality=1000 run_length=1 38.6 ms 33.1 ms -14.2%
orc_write_encode data_type=STRING cardinality=1000 run_length=32 38.7 ms 33.2 ms -14.2%
orc_write_encode data_type=LIST cardinality=0 run_length=1 72.6 ms 71.6 ms -1.4%
orc_write_encode data_type=LIST cardinality=0 run_length=32 98.4 ms 97.7 ms -0.8%
orc_write_encode data_type=LIST cardinality=1000 run_length=1 158.4 ms 157.3 ms -0.7%
orc_write_encode data_type=LIST cardinality=1000 run_length=32 118.3 ms 117.5 ms -0.7%
orc_write_encode data_type=STRUCT cardinality=0 run_length=1 67.4 ms 47.5 ms -29.4%
orc_write_encode data_type=STRUCT cardinality=0 run_length=32 69.5 ms 46.7 ms -32.8%
orc_write_encode data_type=STRUCT cardinality=1000 run_length=1 111.9 ms 86.9 ms -22.4%
orc_write_encode data_type=STRUCT cardinality=1000 run_length=32 75.9 ms 49.5 ms -34.8%
orc_write num_columns=64 509.3 ms 502.2 ms -1.4%
orc_chunked_write num_columns=64 num_chunks=64 7164.7 ms 7145.8 ms -0.3%
orc_chunked_write num_columns=64 num_chunks=8 3526.0 ms 3519.6 ms -0.2%
orc_write num_columns=8 93.5 ms 92.5 ms -1.1%
orc_chunked_write num_columns=8 num_chunks=64 2037.7 ms 2033.6 ms -0.2%
orc_chunked_write num_columns=8 num_chunks=8 331.1 ms 330.3 ms -0.2%

Faster on 34 of 34 configs; best -34.8% (orc_write_encode data_type=STRUCT cardinality=1000 run_length=32).

@vuule

vuule commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Throughput vs main, async memory resource

Same benchmarks with --rmm_mode async. The pool resource amortizes allocation cost, so this shows the effect of the allocation count directly. Faster on all 34 configurations:

benchmark configs vs main
orc_write_encode INTEGRAL_SIGNED 4 -24.0% to -52.8%
orc_write_encode FLOAT 4 -19.1% to -56.0%
orc_write_encode DECIMAL 4 -21.0% to -57.4%
orc_write_encode TIMESTAMP 4 -34.8% to -70.2%
orc_write_encode STRING 4 -38.7% to -69.7%
orc_write_encode LIST 4 -8.4% to -17.1%
orc_write_encode STRUCT 4 -67.2% to -88.7%
orc_write 2 -13.1% to -14.6%
orc_chunked_write 4 -0.8% to -3.9%

Releasing transient_buffer before compression is what costs 5-28% here versus keeping one arena. Isolated with a third build in which only the release is disabled: splitting into two arenas is itself free (-0.2% to -2.6%, slightly faster), and the whole cost is the single cudaFreeAsync. It tracks the size of the buffer returned, not the number of calls, so it appears exactly where the peak-memory benefit is largest (INTEGRAL_SIGNED run_length=1: +6.3 ms; the same type at run_length=32, where RLE shrinks the arena: +0.16 ms). Skipping the release for small arenas would therefore buy nothing.

Per-configuration numbers
benchmark axes main this branch change
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=0 run_length=1 97.0 ms 52.0 ms -46.4%
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=0 run_length=32 77.5 ms 36.6 ms -52.8%
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=1000 run_length=1 162.2 ms 123.3 ms -24.0%
orc_write_encode data_type=INTEGRAL_SIGNED cardinality=1000 run_length=32 76.1 ms 39.5 ms -48.1%
orc_write_encode data_type=FLOAT cardinality=0 run_length=1 86.7 ms 38.2 ms -56.0%
orc_write_encode data_type=FLOAT cardinality=0 run_length=32 53.8 ms 25.0 ms -53.5%
orc_write_encode data_type=FLOAT cardinality=1000 run_length=1 155.4 ms 125.7 ms -19.1%
orc_write_encode data_type=FLOAT cardinality=1000 run_length=32 52.6 ms 28.0 ms -46.7%
orc_write_encode data_type=DECIMAL cardinality=0 run_length=1 120.3 ms 59.5 ms -50.5%
orc_write_encode data_type=DECIMAL cardinality=0 run_length=32 78.8 ms 33.5 ms -57.4%
orc_write_encode data_type=DECIMAL cardinality=1000 run_length=1 160.4 ms 126.7 ms -21.0%
orc_write_encode data_type=DECIMAL cardinality=1000 run_length=32 58.3 ms 33.6 ms -42.3%
orc_write_encode data_type=TIMESTAMP cardinality=0 run_length=1 99.8 ms 29.7 ms -70.2%
orc_write_encode data_type=TIMESTAMP cardinality=0 run_length=32 75.1 ms 29.2 ms -61.1%
orc_write_encode data_type=TIMESTAMP cardinality=1000 run_length=1 159.6 ms 104.1 ms -34.8%
orc_write_encode data_type=TIMESTAMP cardinality=1000 run_length=32 75.9 ms 30.3 ms -60.1%
orc_write_encode data_type=STRING cardinality=0 run_length=1 92.0 ms 41.3 ms -55.1%
orc_write_encode data_type=STRING cardinality=0 run_length=32 136.7 ms 41.5 ms -69.7%
orc_write_encode data_type=STRING cardinality=1000 run_length=1 94.1 ms 38.0 ms -59.6%
orc_write_encode data_type=STRING cardinality=1000 run_length=32 59.2 ms 36.3 ms -38.7%
orc_write_encode data_type=LIST cardinality=0 run_length=1 87.1 ms 72.2 ms -17.1%
orc_write_encode data_type=LIST cardinality=0 run_length=32 113.7 ms 99.1 ms -12.9%
orc_write_encode data_type=LIST cardinality=1000 run_length=1 173.5 ms 158.8 ms -8.4%
orc_write_encode data_type=LIST cardinality=1000 run_length=32 133.6 ms 119.1 ms -10.9%
orc_write_encode data_type=STRUCT cardinality=0 run_length=1 472.4 ms 53.7 ms -88.6%
orc_write_encode data_type=STRUCT cardinality=0 run_length=32 432.8 ms 48.8 ms -88.7%
orc_write_encode data_type=STRUCT cardinality=1000 run_length=1 273.9 ms 89.8 ms -67.2%
orc_write_encode data_type=STRUCT cardinality=1000 run_length=32 261.0 ms 52.5 ms -79.9%
orc_write num_columns=64 590.9 ms 504.6 ms -14.6%
orc_chunked_write num_columns=64 num_chunks=64 7452.8 ms 7163.3 ms -3.9%
orc_chunked_write num_columns=64 num_chunks=8 3554.1 ms 3521.8 ms -0.9%
orc_write num_columns=8 105.8 ms 92.0 ms -13.1%
orc_chunked_write num_columns=8 num_chunks=64 2077.8 ms 2050.2 ms -1.3%
orc_chunked_write num_columns=8 num_chunks=8 334.6 ms 331.9 ms -0.8%

Faster on 34 of 34 configs; best -88.7% (orc_write_encode data_type=STRUCT cardinality=0 run_length=32).

@vuule

vuule commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Peak memory vs main

ORC_WRITER_NVBENCH, all 109 configurations, A100 80GB, CUDA 12.9. Run with --profile so each configuration is measured once, which makes the peak_memory_usage metric deterministic (two runs of an unmodified main binary agree on all 109).

benchmark configs mean change worst change worst delta
orc_write_encode 28 +0.02% +0.07% +0.8 MiB
orc_write_io_compression 60 +0.03% +0.08% +0.5 MiB
orc_write_statistics 15 +0.03% +0.06% +0.4 MiB
orc_write 2 +0.02% +0.03% +0.5 MiB
orc_chunked_write 4 +0.03% +0.06% +0.0 MiB

109 configs total; largest increase +0.08%, largest decrease +0.00%.

Peak memory is unchanged. This is the metric that motivated the two-arena split described in the PR body: with a single arena holding the encoded output through compression, orc_write_encode regressed by a mean of +21.9% and a worst case of +53.0% (+854.8 MiB), and the worst configurations overall were the wide chunked writes at +46.8% (+506.4 MiB).

vuule added 7 commits July 27, 2026 21:34
Treat the pass-through float/double data stream as an exact size estimate, so
those regions stay in the arena that is read in place instead of being copied
out for no reason. Hand the transient-region set from `encode_columns` to
`gather_stripes` as its own argument rather than parking it in `encoded_data`,
where it outlived its only use, and make the release of `transient_buffer`
unconditional with the invariant it relies on checked explicitly. Flatten the
per-(stripe, stream) bookkeeping into single allocations, and cover stripes that
span several rowgroups with compression disabled and with pass-through floats.
The alignment is not there because the encoder needs naturally aligned streams;
`encode_columns` already realigns every per-rowgroup pointer by absolute
address, so the encoded arenas would tolerate any base. It is the gather that
needs it: compaction drops that per-rowgroup alignment, leaving the region base
as the pointer the compressor receives, so it has to meet the codec's
requirement. Check that rather than assuming 256 bytes is always enough.
The string column in the test is dictionary encoded, whose per-stripe stream is
a single region no matter how many rowgroups it spans. Add the types whose
per-rowgroup sizes are known exactly and chained end to end -- direct encoded
strings and decimals -- since those are what actually exercise reading a
multi-rowgroup stripe in place.
The decimal parameterization already covers stripes of one and of several
rowgroups, and decimal data streams are the exactly sized kind, so adding a
compression axis to it covers writing a multi-rowgroup stripe straight from the
encoder output. Drops the standalone test that was doing the same thing.
OrcChunkedReaderInputLimitTest already writes null-free doubles with compression
disabled at 20k-row stripes, which is the same multi-rowgroup pass-through
layout at a much larger scale.
"Region" was a second name for something ORC already names. An extent is the
aligned byte range within an arena that holds one (stripe, stream) pair, which
is what replaces the allocation each pair used to get.
@vuule vuule changed the title Reduce ORC writer device allocations via per-phase arenas Reduce ORC writer device allocations with arenas of stream extents Jul 28, 2026
@vuule vuule changed the title Reduce ORC writer device allocations with arenas of stream extents Reduce the number of device allocations in the ORC writer Jul 28, 2026
@vuule vuule added Performance Performance related issue improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Jul 28, 2026
vuule added 6 commits July 28, 2026 20:35
The bespoke class was a 2D array of bools with a hand-rolled index; cudf
already has host_2dspan for passing [row][column] host data between the
writer's helpers.
@vuule

vuule commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 0b47b6b

@vuule
vuule marked this pull request as ready for review July 29, 2026 01:18
@vuule
vuule requested a review from a team as a code owner July 29, 2026 01:18
@vuule
vuule requested review from lamarrr and vyasr July 29, 2026 01:18
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 58138555-e78b-4127-9818-e5e9320ec211

📥 Commits

Reviewing files that changed from the base of the PR and between 0b47b6b and ff139af.

📒 Files selected for processing (1)
  • cpp/src/io/orc/writer_impl.cu

📝 Walkthrough

Summary by CodeRabbit

  • Improvements

    • Improved ORC writing efficiency by optimizing encoded data storage, alignment handling, and stripe/stream gathering.
    • Reduced device-memory usage during ORC generation by reusing and managing encoded extents more effectively.
    • Enhanced reliability of stream data handling throughout the ORC writing pipeline.
  • Bug Fixes

    • Improved reliability of ORC output involving aligned stream extents and decimal columns.
  • Tests

    • Expanded Decimal64 ORC writer test coverage to include additional compression settings (including AUTO and NONE).

Walkthrough

Changes

ORC extent management

Layer / File(s) Summary
Aligned encoded extent arenas
cpp/src/io/orc/writer_impl.hpp, cpp/src/io/orc/writer_impl.cu
encoded_data now owns persistent, transient, and gathered arenas while exposing device-span views; encode_columns allocates aligned arenas and assigns per-stream extent views.
Gathered extent lifecycle
cpp/src/io/orc/writer_impl.cu
gather_stripes compacts transient or oversized extents, updates stream descriptors and views, and releases transient storage after gathering.
Decimal64 compression coverage
cpp/tests/io/orc_test.cpp
Decimal64 tests now parameterize writer compression and cover AUTO and NONE combinations.

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

Suggested labels: cuIO

Suggested reviewers: vyasr, lamarrr, bdice, pointkernel

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: reducing device allocations in the ORC writer.
Description check ✅ Passed The description is directly related to the changeset and explains the arena-based ORC writer allocation strategy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
cpp/tests/io/orc_test.cpp (1)

1287-1292: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add empty-input and sliced-column coverage.

The matrix covers nulls, boundary sizes, and multi-block sizes, but all cases use non-empty, unsliced columns. Add an empty case and a dedicated sliced-column case.

As per coding guidelines, C++ tests must cover empty inputs, nulls, sliced columns, boundary and multi-block sizes.

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

In `@cpp/tests/io/orc_test.cpp` around lines 1287 - 1292, Add empty-input and
sliced-column scenarios to the parameterization for OrcWriterTestDecimal
alongside the existing size, scale, and compression combinations. Ensure the new
tests exercise a truly empty column and a column created from a non-zero slice
while preserving the current coverage.

Source: Coding guidelines

cpp/src/io/orc/writer_impl.hpp (1)

108-111: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the span/arena invariant on the members themselves.

data holds raw views into the three arenas, so clearing or reassigning any arena silently invalidates the corresponding spans (relied upon in gather_stripes and in the post-compression cleanup). A ///< doxygen note per member stating which arena each span may point into, and that data must be repointed or cleared before an arena is released, would make the coupling explicit.

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

In `@cpp/src/io/orc/writer_impl.hpp` around lines 108 - 111, Update the member
comments for persistent_buffer, transient_buffer, gathered_buffer, and data to
document which arena the data spans may reference. Add ///< Doxygen notes
stating that clearing or reassigning any referenced arena invalidates those
spans and that data must be repointed or cleared before the arena is released.

Source: Coding guidelines

cpp/src/io/orc/writer_impl.cu (3)

856-867: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a doxygen block for the new dual return.

The return contract (encoded_data plus a flattened [stripe][strm_id] transient flag array indexed with streams.size() stride) is non-obvious and is consumed by gather_stripes as a host_2dspan. A @brief/@param/@return block would match the style used for gather_stripes directly below.

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

In `@cpp/src/io/orc/writer_impl.cu` around lines 856 - 867, Add a Doxygen
documentation block immediately before encode_columns describing its inputs, the
paired encoded_data and flattened transient-buffer flag array return value, and
the array’s streams.size()-based [stripe][strm_id] indexing consumed by
gather_stripes. Match the existing gather_stripes documentation style.

Source: Coding guidelines


2582-2586: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clear data before releasing the arenas it points into.

Purely defensive — nothing dereferences the spans in between — but inverting the order removes the transient window where enc_data.data holds views into freed memory.

♻️ Reorder
+    enc_data.data.clear();
     enc_data.persistent_buffer = rmm::device_uvector<uint8_t>{0, stream};
     enc_data.gathered_buffer   = rmm::device_uvector<uint8_t>{0, stream};
-    enc_data.data.clear();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/io/orc/writer_impl.cu` around lines 2582 - 2586, In the encoded-data
cleanup block, update the deallocation order so enc_data.data.clear() runs
before resetting enc_data.persistent_buffer and enc_data.gathered_buffer. Keep
the existing cleanup operations unchanged otherwise, ensuring the spans are
cleared before their backing arenas are released.

1119-1124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider designated initializers here.

Positional aggregate init of five members (three of which are same-typed device_uvector<uint8_t>) will silently mis-bind if encoded_data's member order ever changes. C++20 designated initializers (.persistent_buffer = ...) make it order-safe and self-documenting.

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

In `@cpp/src/io/orc/writer_impl.cu` around lines 1119 - 1124, Update the
encoded_data aggregate construction in the surrounding return statement to use
C++20 designated initializers for all five members, including the three
device_uvector<uint8_t> values, while preserving their current values and move
semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/io/orc/writer_impl.cu`:
- Around line 1024-1056: Make the alignment invariant explicit in the validation
near the existing extent_alignment and uncomp_block_align checks: require that
extent_alignment is divisible by uncomp_block_align, not merely greater than or
equal to it, and document that the RMM arena base is aligned to
extent_alignment. Preserve the single-rowgroup optimization while ensuring its
skipped alignment shift remains valid.

---

Nitpick comments:
In `@cpp/src/io/orc/writer_impl.cu`:
- Around line 856-867: Add a Doxygen documentation block immediately before
encode_columns describing its inputs, the paired encoded_data and flattened
transient-buffer flag array return value, and the array’s streams.size()-based
[stripe][strm_id] indexing consumed by gather_stripes. Match the existing
gather_stripes documentation style.
- Around line 2582-2586: In the encoded-data cleanup block, update the
deallocation order so enc_data.data.clear() runs before resetting
enc_data.persistent_buffer and enc_data.gathered_buffer. Keep the existing
cleanup operations unchanged otherwise, ensuring the spans are cleared before
their backing arenas are released.
- Around line 1119-1124: Update the encoded_data aggregate construction in the
surrounding return statement to use C++20 designated initializers for all five
members, including the three device_uvector<uint8_t> values, while preserving
their current values and move semantics.

In `@cpp/src/io/orc/writer_impl.hpp`:
- Around line 108-111: Update the member comments for persistent_buffer,
transient_buffer, gathered_buffer, and data to document which arena the data
spans may reference. Add ///< Doxygen notes stating that clearing or reassigning
any referenced arena invalidates those spans and that data must be repointed or
cleared before the arena is released.

In `@cpp/tests/io/orc_test.cpp`:
- Around line 1287-1292: Add empty-input and sliced-column scenarios to the
parameterization for OrcWriterTestDecimal alongside the existing size, scale,
and compression combinations. Ensure the new tests exercise a truly empty column
and a column created from a non-zero slice while preserving the current
coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 73a4f3e3-0bc5-43f5-a7ea-93eb44812e6f

📥 Commits

Reviewing files that changed from the base of the PR and between 9708dd0 and 0b47b6b.

📒 Files selected for processing (3)
  • cpp/src/io/orc/writer_impl.cu
  • cpp/src/io/orc/writer_impl.hpp
  • cpp/tests/io/orc_test.cpp

Comment thread cpp/src/io/orc/writer_impl.cu

@bdice bdice left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I will have to read the PR more closely, but I wanted to check a couple of things. A different way to reduce the number of (small) allocations from the current memory resource is to create and suballocate from a temporary pool. Could we create a temporary RMM pool that uses the current device resource as its base allocator, and use that pool as the allocator for internal temporary allocations? This doesn’t need to return the final allocations, as far as I can tell. We should avoid doing extensive padding math and manual alignment if we can just suballocate from a pool object that handles that for us.

@vuule

vuule commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

I will have to read the PR more closely, but I wanted to check a couple of things. A different way to reduce the number of (small) allocations from the current memory resource is to create and suballocate from a temporary pool. Could we create a temporary RMM pool that uses the current device resource as its base allocator, and use that pool as the allocator for internal temporary allocations? This doesn’t need to return the final allocations, as far as I can tell. We should avoid doing extensive padding math and manual alignment if we can just suballocate from a pool object that handles that for us.

Using a temporary pool is doable, but I would argue that using arenas is the right approach, since we know in advance how much memory we'll need. Using the arenas should be faster and have lower peak memory than a pool. Also, the Parquet writer already does something very similar. The additional complexity here is to have two arenas to reduce the peak memory use, which is justified IMO.

vuule added 2 commits July 30, 2026 01:02
Extent bases are offset from the arena base by multiples of extent_alignment,
so the codec's chunk alignment has to divide it. The previous >= check only
implied that because every alignment in play today is a power of two.
@vuule

vuule commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test a86b485

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

Labels

improvement Improvement / enhancement to an existing function libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change Performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants