Skip to content

[FEA] Add zstd compression support for to_csv - #21518

Open
a-hirota wants to merge 6 commits into
NVIDIA:mainfrom
a-hirota:feature/csv-zstd-compression-upstream
Open

[FEA] Add zstd compression support for to_csv#21518
a-hirota wants to merge 6 commits into
NVIDIA:mainfrom
a-hirota:feature/csv-zstd-compression-upstream

Conversation

@a-hirota

@a-hirota a-hirota commented Feb 22, 2026

Copy link
Copy Markdown
Contributor

Description

Adds GPU-accelerated ZSTD compression to the CSV writer, matching the pandas compression="zstd" API.

Why ZSTD only:

  • nvCOMP provides GPU-accelerated ZSTD compression
  • ZSTD supports concatenated frames, so each chunk can be compressed independently while the resulting file remains a single stream that standard tools (zstd -d) can decompress
  • Other codecs are possible as follow-ups; this change focuses on ZSTD

Implementation:

  • Add compression to csv_writer_options and its builder, rejecting codecs other than NONE/ZSTD at option-construction time
  • Compress via the existing cudf::io::detail::compress API, so the writer inherits the shared host/device dispatch rather than re-implementing nvCOMP setup
  • Fail loudly if compression fails, instead of silently writing uncompressed bytes into a .zst file
  • Plumb compression through the pylibcudf bindings and cudf.to_csv, rejecting the combination with a string return value since compressed output is binary

Usage:

df.to_csv("output.csv.zst", compression="zstd", chunksize=10000)

Notes

Taken over from @a-hirota (original authorship preserved). Rebased onto current main and the outstanding review items are addressed; see the comment below for details.

Known follow-ups, deliberately out of scope here so the feature can land and be measured first:

  • Decouple rows_per_chunk from the ZSTD frame size (raised by @vuule)
  • Fold the header and trailing newline into the adjacent data frame instead of emitting them as their own frames
  • Reuse the compression output buffer across chunks rather than allocating per write

Checklist

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

@copy-pr-bot

copy-pr-bot Bot commented Feb 22, 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 libcudf Affects libcudf (C++/CUDA) code. Python Affects Python cuDF API. pylibcudf Issues specific to the pylibcudf package labels Feb 22, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Feb 22, 2026
@a-hirota
a-hirota force-pushed the feature/csv-zstd-compression-upstream branch 2 times, most recently from 37b9318 to e5a1fc2 Compare February 25, 2026 10:22
@a-hirota
a-hirota marked this pull request as ready for review February 25, 2026 10:22
@a-hirota
a-hirota requested review from a team as code owners February 25, 2026 10:22

Copilot AI 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.

Pull request overview

This pull request adds GPU-accelerated ZSTD compression support to cuDF's CSV writer, providing API compatibility with pandas' compression="zstd" parameter. The implementation leverages nvCOMP for GPU-accelerated ZSTD compression and uses concatenated ZSTD frames to enable progressive chunk-based compression.

Changes:

  • Added ZSTD compression support to CSV writer at C++, pylibcudf, and Python API levels
  • Implemented compression using nvCOMP's batched compression API with fallback to uncompressed on failure
  • Added compression parameter to write_csv with validation to only allow ZSTD
  • Added Python and C++ tests for ZSTD compression (both chunked and non-chunked)

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
cpp/include/cudf/io/csv.hpp Added compression member variable, getter, setter, and builder method to csv_writer_options
cpp/src/io/csv/writer_impl.cu Implemented ZSTD compression logic including compress_chunk(), write_data_with_compression() helper, and integration into write_chunked() and write_chunked_begin()
cpp/tests/io/csv_test.cpp Added two C++ tests for ZSTD compression (basic and chunked)
python/pylibcudf/pylibcudf/libcudf/io/csv.pxd Added compression() method declaration to csv_writer_options_builder
python/pylibcudf/pylibcudf/io/csv.pyx Implemented compression() method in CsvWriterOptionsBuilder
python/pylibcudf/pylibcudf/io/csv.pyi Added type hint for compression() method
python/pylibcudf/pylibcudf/io/csv.pxd Added compression() method declaration
python/cudf/cudf/utils/ioutils.py Updated documentation to describe ZSTD compression support
python/cudf/cudf/io/csv.py Added compression parameter validation and mapping to pylibcudf enum
python/cudf/cudf/tests/input_output/test_csv.py Added Python tests for ZSTD compression including unsupported compression types, basic compression, and chunked compression

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cpp/src/io/csv/writer_impl.cu Outdated
Comment thread cpp/tests/io/csv_test.cpp Outdated
Comment thread cpp/include/cudf/io/csv.hpp Outdated
Comment thread python/pylibcudf/pylibcudf/io/csv.pyx
Comment thread python/cudf/cudf/io/csv.py Outdated
@a-hirota
a-hirota force-pushed the feature/csv-zstd-compression-upstream branch 3 times, most recently from 2aa9404 to 0a58680 Compare February 25, 2026 12:43
@vuule
vuule self-requested a review February 27, 2026 06:44
@mhaseeb123
mhaseeb123 self-requested a review February 27, 2026 07:14
@vuule vuule added feature request New feature or request non-breaking Non-breaking change labels Mar 3, 2026
@mhaseeb123

mhaseeb123 commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Pasting the review by Claude here (still manually validating if they make sense). Please exercise caution:

Click to expand

Findings

Critical

  • [writer_impl.cu, compress_chunk, ~lines 100-105] thrust::copy with
    rmm::exec_policy_nosync(stream) is used to copy host-resident device_span structs
    (input_span, output_span) into device uvectors. The source pointers (&input_span,
    &input_span + 1) point to stack-local host memory, but rmm::exec_policy_nosync uses a
    device execution policy — this is a device-to-device Thrust call being fed host pointers,
    which is undefined behavior. The spans are trivially-copyable structs living on the host
    stack. Use cudaMemcpyAsync (host-to-device) or use cudf::detail::hostdevice_vector as the
    existing compression infrastructure does. Compare with how device_compress in
    compression.cpp receives its inputs: callers use hostdevice_vector and call
    host_to_device_async to transfer spans to the device before passing them as device_span.

  • [writer_impl.cu, compress_chunk, ~line 108] Same issue with thrust::fill on results
    init_result is a host-local struct being used as the fill value in a device execution policy.
    While Thrust may handle scalar fill values from host memory in some backends, this is fragile
    and inconsistent with the rest of the codebase.

  • [writer_impl.cu, compress_chunk] The function should not duplicate the batched
    compression setup that already exists in device_compress() (compression.cpp). The existing
    device_compress function handles nvCOMP dispatch, disabled-check, and fallback. The new code
    re-implements all of this (nvcomp type mapping, is_compression_disabled check, batched
    compress call, result checking) — this is significant code duplication and diverges from the
    established pattern. Instead, use device_compress directly, which already accepts
    device_span<device_span<uint8_t const> const> inputs/outputs/results. This eliminates ~80
    lines of error-prone code.

  • [writer_impl.cu, write_data_with_compression] The write_data_with_compression function
    allocates a fresh rmm::device_buffer for the compressed output on every call. For chunked
    writes, this means N+1 allocations (N chunks + header). Since the max compressed size is
    predictable, the compression buffer should be allocated once and reused across chunks.

  • [writer_impl.cu, write_chunked_begin] When compression is enabled, the header is copied
    to the GPU via cudaMemcpyAsync, compressed, then written. But the header is typically a few
    hundred bytes. Compressing such a tiny payload with ZSTD is wasteful (the compressed frame
    header overhead alone may exceed the data size). More importantly, the ZSTD frame for the header
    will be a separate frame from the data frames, which means decompression tools must handle
    concatenated frames starting from the very first frame. While this works with zstd -d and
    streaming decompressors, it adds unnecessary complexity. Consider writing the header uncompressed
    (it's plain text and tiny) and only compressing the data chunks, or concatenating the header
    into the first data chunk before compression.

  • [csv.py, to_csv, ~line 387-390] When path_or_buf is None, the code creates a StringIO
    and sets return_as_string = True. But if compression="zstd" is also set, the compressed
    binary output will be written to a StringIO (text mode), which will fail or produce garbage.
    There is no guard against compression + return_as_string. Either raise an error for this
    combination or use BytesIO when compression is enabled.

Suggestions

  • [writer_impl.cu, compress_chunk] The silent fallback to uncompressed output when
    compression fails (returning 0) is dangerous for correctness. If the user explicitly requested
    ZSTD compression, silently writing uncompressed data means the output file will not be a valid
    ZSTD file, but the user won't know. This should either raise an error or at minimum log a
    warning. The is_compression_disabled check should also throw rather than silently fall back —
    if the user asked for ZSTD and nvCOMP is disabled via LIBCUDF_NVCOMP_POLICY, that's an error
    condition, not a "try uncompressed" condition.

  • [writer_impl.cu, write_data_with_compression] The function has three near-identical
    code paths for writing to the sink (device_write vs host_write with
    is_device_write_preferred). This pattern already exists in the original code and is duplicated
    three times in the new function (compressed success, compressed failure fallback, uncompressed).
    Extract a small write_to_sink(data_sink*, void const*, size_t, stream) helper to avoid this
    3x duplication.

  • [csv.py, _plc_write_csv] The compression string-to-enum mapping is done inline with
    if compression == "zstd" / else. This means any string other than "zstd" (including
    typos like "Zstd") silently maps to NONE. The validation in to_csv catches unknown
    strings, but _plc_write_csv is a separate function that could be called independently. Add
    validation here too, or at least assert that compression is None or "zstd".

  • [csv_test.cpp, ZstdCompression] The first C++ test only checks that compressed output
    differs from uncompressed output. It does not verify that the compressed output can be
    decompressed back to the original data. The ZstdCompressionChunked test does this correctly
    by reading back via read_csv with ZSTD decompression. The basic test should do the same.

  • [test_csv.py] The Python test test_to_csv_zstd_compression decompresses with
    dctx.decompress(f.read()) which requires the entire content to fit in a single ZSTD frame.
    But the writer produces concatenated frames (header frame + data frame + newline frame). This
    may fail depending on the zstandard library version. The chunked test correctly uses
    stream_reader which handles concatenated frames. Both tests should use the streaming API for
    consistency and correctness.

  • [test_csv.py] Missing edge-case tests: empty DataFrame with compression, single-row
    DataFrame, DataFrame with null values, DataFrame with only string columns (high compression
    ratio), and very large chunks that might stress the nvCOMP buffer sizing.

  • [csv.hpp, set_compression] The setter is defined inline in the public header. This is
    fine for a simple setter, but it pulls in CUDF_EXPECTS (and transitively <stdexcept>) into
    the public header. Consider moving the validation to the .cu implementation file, keeping only
    the trivial assignment in the header, consistent with how set_quoting is implemented (which
    also validates inline, so this is at least consistent).

Nits

  • [PR files] The PR includes .serena/cache/cpp/document_symbols.pkl and
    .serena/cache/cpp/raw_document_symbols.pkl (binary pickle files) and a PR_DESCRIPTION.md
    file. These are clearly not intended to be part of the PR and should be removed. The
    PR_DESCRIPTION.md also describes a completely different feature (CP932/Shift-JIS encoding
    support) — this appears to be leftover from a different branch or tool.

  • [writer_impl.cu] using namespace cudf::io; is already present at the top of the file.
    The new code uses cudf::io::detail::nvcomp:: and io::detail::codec_exec_result with
    varying qualification levels. Be consistent with the existing namespace usage.

  • [writer_impl.cu, compress_chunk] nvcomp::compression_type nvcomp_type = nvcomp::compression_type::ZSTD; — use auto const per cudf style, and this variable is
    redundant since the function already asserts ZSTD-only. Just use the literal directly.

  • [csv.hpp] The @return tag says compression_type The compression type for the output
    the type name in the description is redundant with the actual return type. Simplify to
    @return The compression type for the output.

@vuule

vuule commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Thank you for the PR, @a-hirota! I ran some benchmarks locally and it performs surprisingly well (given that we compress one block at a time). Would be nice to decouple chunk size from ZSDT block size, but this can be a separate PR.

Please let us know if you need help addressing the comments in the review that @mhaseeb123 has posted :)

@GregoryKimball

Copy link
Copy Markdown
Contributor

@vuule Should we take this over and merge it?

@GregoryKimball GregoryKimball moved this to Burndown in libcudf Jul 14, 2026
@vuule
vuule force-pushed the feature/csv-zstd-compression-upstream branch from d52e473 to 3d09384 Compare July 28, 2026 19:15
@vuule

vuule commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Taking this over so it can land — thanks @a-hirota for the original work, authorship is preserved on the commit.

I rebased onto current main (the branch was ~1500 commits behind) and squashed to a single clean commit, dropping the stray .serena/ cache files and PR_DESCRIPTION.md. Addressing @mhaseeb123's review:

  • Host pointers with a device execution policy. Removed. The custom compress_chunk is gone; spans and results now go through cudf::detail::hostdevice_vector + host_to_device_async, matching the rest of the IO code. Worth noting the repo's own exec-policy-nosync-memory-resource pre-commit hook flags the old code, so this was blocking commits outright.
  • Duplicated nvCOMP setup. Removed. The writer now calls cudf::io::detail::compress, so it inherits the shared host/device dispatch and the disabled-codec handling instead of re-deriving them. This also means a build with device ZSTD unavailable transparently uses the host path rather than needing writer-specific fallback logic.
  • Silent fallback to uncompressed output. Fixed. A failed compression is now a CUDF_EXPECTS failure rather than emitting raw CSV bytes into a .zst file.
  • set_compression validation. Kept and now covered by a test; invalid codecs throw at option construction, mirroring set_quoting.
  • Test coverage. The C++ tests now round-trip through read_csv and compare against the uncompressed write read back the same way, which is what actually catches chunk-separator corruption. Added a no-header case and an invalid-codec case. The Python tests use the streaming decompressor (required for concatenated frames) and assert the decompressed bytes equal the uncompressed to_csv output exactly, across single-chunk, multi-chunk, single-row, and null-containing inputs.

One correctness hole not in the original review: to_csv(compression="zstd") with no path_or_buf wrote binary into a text-mode StringIO. That combination now raises, and _plc_write_csv no longer silently maps an unrecognized compression string to NONE.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added CSV writing support for Zstandard (ZSTD) compression.
    • Extended CSV writer option/builder APIs to configure compression.
    • Python to_csv now accepts compression="zstd" for file outputs, including chunked writes.
    • Python read_csv can read ZSTD-compressed CSVs with compression="zstd".
  • Bug Fixes
    • Unsupported compression codecs now raise clear errors.
    • ZSTD compression is disallowed when to_csv returns CSV content as a string.
  • Documentation
    • Updated CSV docstrings to document supported compression options and ZSTD behavior.
  • Tests
    • Added ZSTD round-trip and edge-case coverage (including header-only and chunked cases).

Walkthrough

CSV writer options now support validated NONE or ZSTD compression. The setting flows through libcudf, pylibcudf, and cuDF APIs, with compressed device output, updated documentation, and C++ and Python round-trip coverage.

Changes

CSV ZSTD writer compression

Layer / File(s) Summary
Writer compression contract
cpp/include/cudf/io/csv.hpp, python/pylibcudf/pylibcudf/io/csv.*, python/pylibcudf/pylibcudf/libcudf/io/csv.pxd
CSV writer options store and validate NONE or ZSTD compression, with matching chainable Python binding declarations.
Device-side compressed output
cpp/src/io/csv/writer_impl.cu
CSV headers and chunks use compression-aware writes, including separate ZSTD frames for trailing newlines.
Python compression binding
python/cudf/cudf/io/csv.py
The Python API maps zstd to the pylibcudf compression enum, validates unsupported values, rejects string-return compression, and forwards the setting through both CSV write paths.
Validation and round-trip coverage
cpp/tests/io/csv_test.cpp, python/cudf/cudf/tests/input_output/test_csv.py, python/cudf/cudf/utils/ioutils.py
Tests and documentation cover ZSTD round trips, chunking, headers, unsupported codecs, decompression, read-back equivalence, and API restrictions.

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

Suggested labels: cuIO, improvement

Suggested reviewers: bdice, mroeschke, mhaseeb123, vuule, lamarrr

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% 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 clearly names the main change: adding zstd compression support for to_csv.
Description check ✅ Passed The description matches the changeset and accurately summarizes the new ZSTD CSV writer support and related API plumbing.
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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@cpp/tests/io/csv_test.cpp`:
- Around line 2974-3037: Expand the ZSTD CSV coverage in
cpp/tests/io/csv_test.cpp at lines 2974-3037 with cases for empty inputs,
null-containing and sliced columns, boundary and multi-block row sizes, and
non-ASCII UTF-8 strings; reuse existing round-trip helpers and preserve
header/no-header coverage. Also update
python/cudf/cudf/tests/input_output/test_csv.py at lines 2073-2107 to add empty
and all-null DataFrame cases alongside the existing single-element and
mixed-null tests, covering the required Python input categories.

In `@python/cudf/cudf/tests/input_output/test_csv.py`:
- Around line 2103-2107: Add the missing "zstd" entry to the compression map
used by the high-level read_csv implementation in csv.py, mapping it to
plc.io.types.CompressionType.ZSTD. Preserve the existing mappings and ensure
read_csv accepts compression="zstd" without raising KeyError.
🪄 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: Pro Plus

Run ID: de4841dd-a92c-4a33-a974-5488e543a4c9

📥 Commits

Reviewing files that changed from the base of the PR and between 708e454 and 3d09384.

📒 Files selected for processing (10)
  • cpp/include/cudf/io/csv.hpp
  • cpp/src/io/csv/writer_impl.cu
  • cpp/tests/io/csv_test.cpp
  • python/cudf/cudf/io/csv.py
  • python/cudf/cudf/tests/input_output/test_csv.py
  • python/cudf/cudf/utils/ioutils.py
  • python/pylibcudf/pylibcudf/io/csv.pxd
  • python/pylibcudf/pylibcudf/io/csv.pyi
  • python/pylibcudf/pylibcudf/io/csv.pyx
  • python/pylibcudf/pylibcudf/libcudf/io/csv.pxd

Comment thread cpp/tests/io/csv_test.cpp
Comment thread python/cudf/cudf/tests/input_output/test_csv.py Outdated
@vuule vuule changed the title feat(csv): add zstd compression support for to_csv [FEA] Add zstd compression support for to_csv Jul 28, 2026
@vuule
vuule force-pushed the feature/csv-zstd-compression-upstream branch from 3d09384 to 5816f91 Compare July 28, 2026 20:08

@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 (1)
python/cudf/cudf/tests/input_output/test_csv.py (1)

2073-2097: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add empty, all-null, and no-header cases.

Extend the matrix with an empty DataFrame, an explicitly typed all-null column, and header=False; adjust the read-back assertion for the no-header case. These branches are currently untested. As per coding guidelines, Python test files must provide empty, all-null, single-element, and mixed-type coverage.

🤖 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 `@python/cudf/cudf/tests/input_output/test_csv.py` around lines 2073 - 2097,
Extend test_to_csv_zstd_compression’s parameter matrix with an empty DataFrame,
an explicitly typed all-null column, and a header=False case. Add a header
parameter to the test and pass it to both CSV writes, adjusting the read-back
comparison so no-header output is validated without expecting a header.

Source: Coding guidelines

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

Inline comments:
In `@python/cudf/cudf/tests/input_output/test_csv.py`:
- Around line 2098-2101: Update the ZSTD decompression setup in the affected CSV
test to call stream_reader with read_across_frames=True, ensuring concatenated
frames are fully read. Store the reader, read and decode its contents, then
explicitly close the reader without closing or otherwise touching the file
handle still used by cuDF.

---

Nitpick comments:
In `@python/cudf/cudf/tests/input_output/test_csv.py`:
- Around line 2073-2097: Extend test_to_csv_zstd_compression’s parameter matrix
with an empty DataFrame, an explicitly typed all-null column, and a header=False
case. Add a header parameter to the test and pass it to both CSV writes,
adjusting the read-back comparison so no-header output is validated without
expecting a header.
🪄 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: Pro Plus

Run ID: 798f3d14-8380-4171-a41c-00fcbaed3b54

📥 Commits

Reviewing files that changed from the base of the PR and between 3d09384 and 5816f91.

📒 Files selected for processing (10)
  • cpp/include/cudf/io/csv.hpp
  • cpp/src/io/csv/writer_impl.cu
  • cpp/tests/io/csv_test.cpp
  • python/cudf/cudf/io/csv.py
  • python/cudf/cudf/tests/input_output/test_csv.py
  • python/cudf/cudf/utils/ioutils.py
  • python/pylibcudf/pylibcudf/io/csv.pxd
  • python/pylibcudf/pylibcudf/io/csv.pyi
  • python/pylibcudf/pylibcudf/io/csv.pyx
  • python/pylibcudf/pylibcudf/libcudf/io/csv.pxd
🚧 Files skipped from review as they are similar to previous changes (9)
  • python/pylibcudf/pylibcudf/libcudf/io/csv.pxd
  • python/pylibcudf/pylibcudf/io/csv.pyi
  • python/pylibcudf/pylibcudf/io/csv.pxd
  • python/pylibcudf/pylibcudf/io/csv.pyx
  • python/cudf/cudf/utils/ioutils.py
  • python/cudf/cudf/io/csv.py
  • cpp/src/io/csv/writer_impl.cu
  • cpp/include/cudf/io/csv.hpp
  • cpp/tests/io/csv_test.cpp

Comment on lines +2098 to +2101
with open(fname, "rb") as f:
# the writer emits concatenated frames, so the streaming API is required
decompressed = zstd.ZstdDecompressor().stream_reader(f).read()
assert decompressed.decode("utf-8") == expected

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python - <<'PY'
import io
import zstandard as zstd

frames = (
    zstd.ZstdCompressor().compress(b"frame-1")
    + zstd.ZstdCompressor().compress(b"frame-2")
)
with zstd.ZstdDecompressor().stream_reader(
    io.BytesIO(frames), read_across_frames=True
) as reader:
    assert reader.read() == b"frame-1frame-2"
PY

Repository: rapidsai/cudf

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate test_csv.py =="
git ls-files | grep -F 'python/cudf/cudf/tests/input_output/test_csv.py' || true

echo "== relevant lines =="
sed -n '2040,2125p' python/cudf/cudf/tests/input_output/test_csv.py | cat -n | sed 's/^/2040+/'

echo "== zstandard usages in test_csv.py =="
rg -n "zstd|read_across_frames|stream_reader|chunksize|csv_writer" python/cudf/cudf/tests/input_output/test_csv.py | sed -n '1,200p'

echo "== zstandard requirements/docs references =="
rg -n "python-zstandard|zstandard|read_across_frames" -S pyproject.toml setup.py setup.cfg requirements* constraints* python -g '*txt' -g '*.toml' -g '*.cfg' -g '*.py' 2>/dev/null | head -n 200

Repository: rapidsai/cudf

Length of output: 6354


🌐 Web query:

python-zstandard ZstdDecompressor stream_reader read_across_frames default

💡 Result:

In the python-zstandard library, the ZstdDecompressor.stream_reader method includes a read_across_frames argument that controls how the reader handles input containing multiple Zstandard frames [1][2]. The default value for read_across_frames is False [1][2]. When this is set to False, the reader stops decompressing when it encounters the end of a single Zstandard frame [1][3][4]. If set to True, the reader can transparently continue decompressing data that spans multiple Zstandard frames [1][3][4]. The library documentation and maintainers have noted that the default behavior may change to True in a future release [1][2][5]. Consequently, it is strongly recommended that users explicitly specify read_across_frames=True or read_across_frames=False in their code to ensure consistent, future-proof behavior [2][5][6].

Citations:


🌐 Web query:

python-zstandard stream_reader context manager close __enter__ __exit__

💡 Result:

In the python-zstandard library, the object returned by ZstdDecompressor.stream_reader (an instance of ZstdDecompressionReader) supports the context manager protocol using the enter and exit methods [1][2]. Context Manager Behavior: - enter: Marks the reader as having been entered (it cannot be entered multiple times) and returns the instance itself [2]. - exit: Explicitly calls the close method on the stream [1][3]. This releases underlying resources, such as the buffer and the decompressor reference, and marks the stream as closed [2]. Once closed, any subsequent I/O operations will fail [1]. Important Considerations: - Automatic Closure: Using the stream as a context manager is a convenient way to ensure that resources are cleaned up promptly when processing is finished [1][4]. - Underlying Stream: The stream_reader method accepts a closefd argument (defaulting to True) [1][3]. If closefd is True, calling close (including via the context manager's exit) will also close the underlying source stream [1]. If you need to keep the source stream open after the reader is finished, set closefd=False [1][3]. - Not Mandatory: While context manager usage is supported and recommended for resource management, it is not strictly required [5]. You can manually call close on the reader instance if you prefer [1][2].

Citations:


Read across concatenated ZSTD frames.

stream_reader() defaults to read_across_frames=False, so the chunksize=10 case reads only the first ZSTD frame and cannot match the full uncompressed CSV. Pass read_across_frames=True and close the reader explicitly to avoid touching the underlying file handle that cuDF is still using.

🤖 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 `@python/cudf/cudf/tests/input_output/test_csv.py` around lines 2098 - 2101,
Update the ZSTD decompression setup in the affected CSV test to call
stream_reader with read_across_frames=True, ensuring concatenated frames are
fully read. Store the reader, read and decode its contents, then explicitly
close the reader without closing or otherwise touching the file handle still
used by cuDF.

Source: MCP tools

@vuule
vuule force-pushed the feature/csv-zstd-compression-upstream branch from 5816f91 to d589b0e Compare July 28, 2026 21:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@python/cudf/cudf/io/csv.py`:
- Around line 406-411: Extend the compression guard in the CSV writer path to
reject StringIO and other text-mode sinks, not only cases returning output as a
string without path_or_buf. Ensure compressed binary output never reaches
SinkInfo with a text stream, while preserving supported binary sinks and
uncompressed StringIO behavior. Add a regression test covering compressed output
directed to an in-memory string buffer.

In `@python/cudf/cudf/tests/input_output/test_csv.py`:
- Around line 2077-2089: Extend the CSV test input matrix around the existing
DataFrame cases to include an empty frame such as {} and an all-null column case
such as {"a": [None, None]}. Keep the existing single-element, mixed-type, and
partially-null cases unchanged so the matrix covers both empty and all-null
writer paths.
🪄 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: Pro Plus

Run ID: 64f3d8bd-50b9-471f-a73e-00e9a2b78c45

📥 Commits

Reviewing files that changed from the base of the PR and between 5816f91 and d589b0e.

📒 Files selected for processing (10)
  • cpp/include/cudf/io/csv.hpp
  • cpp/src/io/csv/writer_impl.cu
  • cpp/tests/io/csv_test.cpp
  • python/cudf/cudf/io/csv.py
  • python/cudf/cudf/tests/input_output/test_csv.py
  • python/cudf/cudf/utils/ioutils.py
  • python/pylibcudf/pylibcudf/io/csv.pxd
  • python/pylibcudf/pylibcudf/io/csv.pyi
  • python/pylibcudf/pylibcudf/io/csv.pyx
  • python/pylibcudf/pylibcudf/libcudf/io/csv.pxd
🚧 Files skipped from review as they are similar to previous changes (7)
  • python/pylibcudf/pylibcudf/io/csv.pxd
  • python/pylibcudf/pylibcudf/libcudf/io/csv.pxd
  • python/cudf/cudf/utils/ioutils.py
  • python/pylibcudf/pylibcudf/io/csv.pyx
  • cpp/tests/io/csv_test.cpp
  • cpp/include/cudf/io/csv.hpp
  • cpp/src/io/csv/writer_impl.cu

Comment thread python/cudf/cudf/io/csv.py
Comment thread python/cudf/cudf/tests/input_output/test_csv.py
Adds GPU-accelerated ZSTD compression to `write_csv` and `to_csv`,
matching the pandas `compression="zstd"` API.

ZSTD is the only supported codec because it allows concatenated frames:
each chunk can be compressed independently while the resulting file
remains a single stream that standard tools (`zstd -d`) can decompress.

- Add `compression` to `csv_writer_options` and its builder, rejecting
  codecs other than NONE/ZSTD
- Compress via the existing `io::detail::compress` API so the writer
  inherits the shared host/device dispatch, and fail loudly rather than
  silently emitting uncompressed bytes into a `.zst` file
- Plumb `compression` through the pylibcudf bindings and `to_csv`,
  rejecting the combination with a string return value since compressed
  output is binary

Co-authored-by: Hirota Akio <33370421+a-hirota@users.noreply.github.com>
@vuule
vuule force-pushed the feature/csv-zstd-compression-upstream branch from d589b0e to 6aeb4c1 Compare July 28, 2026 22:29
@vuule

vuule commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

/ok to test 6aeb4c1

vuule added 3 commits July 29, 2026 20:35
The repo test guidelines call for empty, all-null, single-element and
mixed-type inputs. A frame with no columns writes a header consisting of
just the line terminator, which is still emitted as its own ZSTD frame,
so it exercises a distinct writer path.
The writer compressed each row chunk into a single ZSTD frame, so the
amount of work handed to the codec was tied to `rows_per_chunk` and a
large chunk became one serial compression task.

Concatenated ZSTD frames decompress to the concatenation of their
payloads, so the byte stream can be split at arbitrary offsets without
regard to row boundaries. Split each chunk into fixed-size blocks and
compress them in one batched call, then pack the resulting frames into a
contiguous buffer for a single sink write.

Add `compression_block_size` to the writer options so the block size can
be tuned independently of `rows_per_chunk`, defaulting to 1 MB and capped
at the codec's maximum input size. Capping at the codec limit also makes
the previous "chunk is too large" failure unreachable.
Mirror the new `compression_block_size` writer option on
`CsvWriterOptionsBuilder` so the block size can be tuned from Python.
@vyasr

vyasr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@vuule how do you want to deal with reviews here since you've taken over the PR?

@vuule

vuule commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@vuule how do you want to deal with reviews here since you've taken over the PR?

yeah, just struggling to find time to continue working on this. Should come back to this within a week.

@vyasr

vyasr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@vuule how do you want to deal with reviews here since you've taken over the PR?

yeah, just struggling to find time to continue working on this. Should come back to this within a week.

No rush, just checking in before I review (or ping for reviews).

@vuule

vuule commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

No rush, just checking in before I review (or ping for reviews).

Not quite ready for review :)

@vuule
vuule requested review from a team as code owners August 21, 2026 19:08
The trailing newline that separates a chunk from the next one was
compressed and written on its own, so a chunked write cost two compress
calls, two packing passes and two sink writes per chunk.

Pass it to `write_compressed_to_sink` as a tail span instead, where it
becomes one more block of the same batched call. Appending it still does
not require copying the chunk. Since the blocks are no longer uniformly
sized, lay the compression buffer out from the maximum compressed size
of each individual block, which also drops the padding the uniform
layout reserved for partial blocks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change pylibcudf Issues specific to the pylibcudf package Python Affects Python cuDF API.

Projects

Status: In Progress
Status: Burndown

Development

Successfully merging this pull request may close these issues.

8 participants