Skip to content

Compare Vortex GPU decompression against a cuDF Parquet read - #9147

Open
joseph-isaacs wants to merge 24 commits into
developfrom
claude/gpu-decompress-benchmarks-4mmn93
Open

Compare Vortex GPU decompression against a cuDF Parquet read#9147
joseph-isaacs wants to merge 24 commits into
developfrom
claude/gpu-decompress-benchmarks-4mmn93

Conversation

@joseph-isaacs

@joseph-isaacs joseph-isaacs commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

The GPU decompression benchmark measured only Vortex, so the --gpu-decompress numbers had
nothing to compare against — a device-side decode rate is only meaningful next to another
format decoded on the same device.

The comparison point is cuDF's read_parquet, which does
the whole Parquet read on the GPU: page header decode, codec decompression, dictionary/RLE/plain
decoding and column assembly. Both sides therefore decode all the way to device-resident arrays,
which is what makes the ratio a like-for-like number rather than a comparison against a host read.

Adding a second decoder also gave the Vortex CUDA path a reference to be checked against, and
that cross-check found two real correctness bugs in vortex-cuda (see below).

What changes are included in this PR?

GPU Parquet backend. benchmarks/compress-bench/src/gpu_parquet.rs rewrites each dataset
with GPU-friendly writer settings and times a cuDF read of it. cuDF is reached through its
prebuilt cudf-cu12 manylinux wheel and driven by scripts/cudf-parquet-read.py, so it is a
runtime dependency of the benchmark and never enters the Rust build. Timing is taken inside the
script after a warm-up read, so interpreter start, import cudf and CUDA context creation are
excluded.

GPU-friendly Parquet writer settings (src/gpu_writer.rs): v1 pages, Snappy (default) or
Zstd, dictionary enabled, 1 MiB data pages with a 1,000,000-row limit, chunk-level statistics.
The rationale for each is tabulated in the README.

Like-for-like I/O. cuDF takes an untimed warm-up read, so its timed read is served from the
page cache. The Vortex reader therefore no longer uses O_DIRECT by default — doing so compared
a Vortex read of the disk against a cuDF read of RAM on every iteration. --gpu-direct-io
restores it for measuring storage bandwidth, which is a different question and not a decode
comparison.

Correctness. --gpu-verify cross-checks both backends against the CPU decoders inline: the
cuDF frame against a CPU Parquet read, and each GPU-decoded Vortex field against a separate
host-only scan of a copy of the file, compared through Arrow with a pinned target type. A
mismatch reports the differing types, lengths and null counts plus the first differing row,
found by binary search.

Two vortex-cuda bugs found by the cross-check

  1. Bit-unpack dropped the frame of reference on patches. The kernel wrote inline patch
    (exception) values without adding the reference, and patches are stored reference-relative —
    so any patched value under FoR(BitPacked) decoded on the GPU came back short by exactly the
    reference. Fixed in vortex-cuda/src/bit_unpack_gen.rs, kernels regenerated, with an rstest
    regression case in vortex-cuda/src/kernel/encodings/for_.rs covering u32/u64 patches at
    lane, block and cross-block boundaries.
  2. into_host left validity on the device. CanonicalCudaExt::into_host migrated a
    canonical array's values buffer but passed its Validity through untouched, so a nullable
    array came back half-migrated and the first host read of the mask panicked in
    BufferHandle::unwrap_host (via Validity::execute_maskBoolArray::into_bit_buffer).
    Non-nullable arrays were unaffected, which is why it only surfaced on the nullable Public BI
    tables. The Bool arm already carried a TODO for exactly this.

Benchmark coverage. --gpu-decompress now runs nine datasets (TPC-H l_comment canonical
and chunked, taxi, and the Arade/Bimbo/CMSprovider/Euro2016/Food/HashTags Public BI tables)
rather than one, and reports on every dataset instead of stopping at the first failure — one run
shows the whole matrix. The timing tables render before the failure summary, so datasets that do
decode still publish numbers when another dataset cannot.

CI. pr-bench-gpu-compress.yml installs the DuckDB CLI (needed to build the Public BI
fixtures, as in pr-bench-compress.yml) and the cuDF wheel, runs a verification pass and the
timed pass, publishes both to the PR — attaching full tracebacks and backtraces on failure —
and fails the job at the end if either failed.

Known gaps

Two CUDA encoding gaps remain, both in vortex-cuda rather than in the benchmark, and both
outside the scope of this PR:

  • taxi and Arade: Unsupported ptype u16. The CUDA date_time_parts kernel dispatches with
    match_each_signed_integer_ptype! while the CPU canonicaliser uses match_each_integer_ptype!.
    Widening the fused kernel's dispatch turns 4³ = 64 PTX instantiations into 8³ = 512, so the fix
    is not free.
  • Euro2016 and HashTags: No CUDA kernel for encoding vortex.masked.

What APIs are changed? Are there any user-facing changes?

One library behaviour change: CanonicalCudaExt::into_host now migrates validity as well as
values, so a nullable canonical array copied back from the device is fully host-resident. That
is a bug fix — the previous result panicked on first use.

Everything else is confined to the compress-bench binary. New benchmark CLI flags:
--gpu-parquet-codec, --gpu-verify and --gpu-direct-io. Running --gpu-decompress now
additionally requires the cudf-cu12 wheel on PATH; benchmarks/compress-bench/README.md
documents the install and the remaining transfer-path asymmetry (the Vortex reader uses pinned
buffers; cuDF does its own host read and host-to-device copy).

The GPU compression benchmark only measured Vortex, and only on a single
dataset, so it could not say anything about how Vortex GPU decompression
compares to Parquet, nor about encodings beyond FSST strings.

Parquet compresses each page body independently, which is exactly the batch
shape nvCOMP's device decompressors take and how cuDF's Parquet reader gets
pages off the CPU. This adds a Parquet backend built on that: column chunks
are staged on the device through the same pinned, direct-I/O reader the Vortex
backend uses, then every page in a row group is decompressed in one batched
nvCOMP launch.

- vortex-nvcomp: bind the batched Snappy decompression entrypoints and the
  per-algorithm alignment queries, and share `DecompressBackend` between the
  Snappy and Zstd wrappers.
- compress-bench: locate compressed page bodies by walking the per-page Thrift
  headers (`parquet::format::PageHeader` is deprecated and `parquet`'s own
  parser is crate-private), and write files with GPU-friendly settings: v1
  pages, dictionary encoding, 1 MiB pages, Snappy by default.
- Run both Vortex and Parquet under `--gpu-decompress`, and expand the GPU
  dataset set from one to nine so ALP, bit-packed, run-end, date/time-parts
  and null-heavy columns are covered alongside FSST strings.
- Add `--gpu-verify`, which compares every GPU-decompressed page against the
  host codec and every GPU-decoded Vortex field against the CPU decode, and
  run it as a CI step before the timed benchmark. Independently of that flag,
  nvCOMP's per-page status and size arrays are checked on every iteration.

Page decoding is not part of the Parquet measurement, so its numbers are an
upper bound on a full GPU Parquet reader; the README states this.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
A CUDA scan hands back arrays whose buffers live in device memory, so decoding
those same arrays through the host Arrow path panics rather than producing a
CPU reference. Read the file a second time through the ordinary host reader and
compare the two scans batch by batch instead.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
Two changes to the Vortex GPU verification, after CI reported a `fastlanes.for`
mismatch with no detail:

- Read the CPU reference from a copy of the file. The session segment cache is
  keyed by URI and the CUDA reader deliberately bypasses it because its buffers
  are device-resident, so pointing both scans at one URI risks them sharing
  entries.
- Synchronize the stream before copying a decoded field back, and report the
  Arrow types, lengths, null counts and the first differing row when the two
  decodes disagree.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 2043 untouched benchmarks
⏩ 46 skipped benchmarks1


Comparing claude/gpu-decompress-benchmarks-4mmn93 (537e30b) with develop (6867cda)2

Open in CodSpeed

Footnotes

  1. 46 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on develop (d0f29f8) during the generation of this report, so 6867cda was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

The bit-unpack kernel writes patch values straight into the output while the
lane decoder adds the frame of reference to every unpacked value. Bit-packing
exceptions are stored in the same reference-relative domain as the packed
values, so under `FoR(BitPacked)` every patched position came out short by
exactly the reference.

The existing kernel tests could not catch this: they exercise `BitPacked`
directly, where the reference is zero. The new `FoRExecutor` case bit-packs to
8 bits with values that overflow into patches and a non-zero reference.

Found by the compression benchmark's new `--gpu-verify` pass, which reported a
`fastlanes.for` field decoding row 8038 as 131072 where the CPU produced
393061 — a difference of exactly the 261989 reference.

Also thread the dataset name through compress-bench failures, so a benchmark
error says which dataset it came from.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
A verification run stopped at the first dataset that failed, so finding the
GPU-clean set took one CI cycle per dataset. Run every dataset instead,
recording failures and reporting them together at the end, then exit non-zero.

Missing CUDA kernel support surfaces as a panic rather than an error, so the
survey catches those too.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
The per-dataset verification verdicts were only visible by digging through a
multi-thousand-line job log. Capture the verification output, publish the
per-dataset results to the step summary and a PR comment, and keep failing the
job through a separate gate step.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

GPU decompression verification

Verification failed. Per-dataset results:

2026-08-14T09:14:36.070831Z  INFO compress_bench::gpu_vortex: benchmarks/compress-bench/src/gpu_vortex.rs:207: verified 11728 GPU-decoded Vortex fields against the CPU decode
2026-08-14T09:15:24.938308Z  INFO compress_bench::gpu_vortex: benchmarks/compress-bench/src/gpu_vortex.rs:207: verified 11728 GPU-decoded Vortex fields against the CPU decode
2026-08-14T09:19:25.178471Z  INFO compress_bench::gpu_vortex: benchmarks/compress-bench/src/gpu_vortex.rs:207: verified 13584 GPU-decoded Vortex fields against the CPU decode
2026-08-14T09:22:57.170096Z  INFO compress_bench::gpu_vortex: benchmarks/compress-bench/src/gpu_vortex.rs:207: verified 1626 GPU-decoded Vortex fields against the CPU decode
GPU decompression failed for 5 dataset(s):
  - taxi: panicked: Unsupported ptype u16
  - Arade: panicked: Unsupported ptype u16
  - CMSprovider: panicked: Assertion failed error: expected host buffer
  - Euro2016: decompressing Euro2016 as vortex-file-compressed: Other error: GPU execution for encoding vortex.slice failed (Other error: GPU execution for encoding vortex.masked failed (Other error: No CUDA kernel for encoding Id("vortex.masked")
  - HashTags: decompressing HashTags as vortex-file-compressed: Other error: GPU execution for encoding vortex.slice failed (Other error: GPU execution for encoding vortex.masked failed (Other error: No CUDA kernel for encoding Id("vortex.masked")
Error: GPU decompression failed for: taxi, Arade, CMSprovider, Euro2016, HashTags
Full error detail
  16: <futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.33/src/future/future/catch_unwind.rs:37:9
  17: compress_bench::run_compress::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:310:56
  18: compress_bench::main::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:152:6
  19: <tokio::runtime::park::CachedParkThread>::block_on::<compress_bench::main::{closure#0}>::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/park.rs:284:71
  20: tokio::task::coop::with_budget::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::park::CachedParkThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/coop/mod.rs:167:5
  21: tokio::task::coop::budget::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::park::CachedParkThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/coop/mod.rs:133:5
  22: <tokio::runtime::park::CachedParkThread>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/park.rs:284:31
  23: <tokio::runtime::context::blocking::BlockingRegionGuard>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/context/blocking.rs:66:14
  24: <tokio::runtime::scheduler::multi_thread::MultiThread>::block_on::<compress_bench::main::{closure#0}>::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/scheduler/multi_thread/mod.rs:92:22
  25: tokio::runtime::context::runtime::enter_runtime::<<tokio::runtime::scheduler::multi_thread::MultiThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}, core::result::Result<(), anyhow::Error>>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/context/runtime.rs:65:16
  26: <tokio::runtime::scheduler::multi_thread::MultiThread>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/scheduler/multi_thread/mod.rs:91:9
  27: <tokio::runtime::runtime::Runtime>::block_on_inner::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/runtime.rs:376:50
  28: <tokio::runtime::runtime::Runtime>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/runtime.rs:343:18
  29: compress_bench::main
             at ./benchmarks/compress-bench/src/main.rs:152:6
  30: <fn() -> core::result::Result<(), anyhow::Error> as core::ops::function::FnOnce<()>>::call_once
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
  31: std::sys::backtrace::__rust_begin_short_backtrace::<fn() -> core::result::Result<(), anyhow::Error>, core::result::Result<(), anyhow::Error>>
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:166:18
  32: std::rt::lang_start::<core::result::Result<(), anyhow::Error>>::{closure#0}
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:206:18
  33: <&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync as core::ops::function::FnOnce<()>>::call_once
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/core/src/ops/function.rs:287:21
  34: std::panicking::catch_unwind::do_call::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:581:40
  35: std::panicking::catch_unwind::<i32, &dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:544:19
  36: std::panic::catch_unwind::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panic.rs:359:14
  37: std::rt::lang_start_internal::{closure#0}
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/rt.rs:175:24
  38: std::panicking::catch_unwind::do_call::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:581:40
  39: std::panicking::catch_unwind::<isize, std::rt::lang_start_internal::{closure#0}>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:544:19
  40: std::panic::catch_unwind::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panic.rs:359:14
  41: std::rt::lang_start_internal
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/rt.rs:171:5
  42: main
  43: <unknown>
  44: __libc_start_main
  45: _start
); CPU fallback with device-resident buffers is not supported
Backtrace:
   0: <vortex_array::array::erased::ArrayRef as vortex_cuda::executor::CudaArrayExt>::execute_cuda::{closure#0}
             at ./vortex-cuda/src/executor.rs:487:13
   1: <core::pin::Pin<alloc::boxed::Box<dyn core::future::future::Future<Output = core::result::Result<vortex_array::canonical::Canonical, vortex_error::VortexError>> + core::marker::Send>> as core::future::future::Future>::poll
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/future/future.rs:133:9
   2: compress_bench::gpu_vortex::verify_against_host_scan::{closure#0}
             at ./benchmarks/compress-bench/src/gpu_vortex.rs:188:65
   3: <compress_bench::gpu_vortex::GpuVortexCompressor as vortex_bench::compress::Compressor>::decompress::{closure#0}
             at ./benchmarks/compress-bench/src/gpu_vortex.rs:88:78
   4: <core::pin::Pin<alloc::boxed::Box<dyn core::future::future::Future<Output = core::result::Result<core::time::Duration, anyhow::Error>> + core::marker::Send>> as core::future::future::Future>::poll
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/future/future.rs:133:9
   5: vortex_bench::compress::benchmark_decompress::{closure#0}
             at ./vortex-bench/src/compress/mod.rs:192:59
   6: compress_bench::run_benchmark_for_dataset::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:448:22
   7: <core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}> as core::future::future::Future>::poll
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:300:9
   8: <futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.33/src/future/future/catch_unwind.rs:37:44
   9: <core::panic::unwind_safe::AssertUnwindSafe<<futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll::{closure#0}> as core::ops::function::FnOnce<()>>::call_once
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:275:9
  10: std::panicking::catch_unwind::do_call::<core::panic::unwind_safe::AssertUnwindSafe<<futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll::{closure#0}>, core::task::poll::Poll<core::result::Result<(vortex_bench::compress::CompressMeasurements, alloc::vec::Vec<vortex_bench::v3::V3Record>), anyhow::Error>>>
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
  11: std::panicking::catch_unwind::<core::task::poll::Poll<core::result::Result<(vortex_bench::compress::CompressMeasurements, alloc::vec::Vec<vortex_bench::v3::V3Record>), anyhow::Error>>, core::panic::unwind_safe::AssertUnwindSafe<<futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll::{closure#0}>>
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
  12: std::panic::catch_unwind::<core::panic::unwind_safe::AssertUnwindSafe<<futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll::{closure#0}>, core::task::poll::Poll<core::result::Result<(vortex_bench::compress::CompressMeasurements, alloc::vec::Vec<vortex_bench::v3::V3Record>), anyhow::Error>>>
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
  13: <futures_util::future::future::catch_unwind::CatchUnwind<core::panic::unwind_safe::AssertUnwindSafe<compress_bench::run_benchmark_for_dataset::{closure#0}>> as core::future::future::Future>::poll
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.33/src/future/future/catch_unwind.rs:37:9
  14: compress_bench::run_compress::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:310:56
  15: compress_bench::main::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:152:6
  16: <tokio::runtime::park::CachedParkThread>::block_on::<compress_bench::main::{closure#0}>::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/park.rs:284:71
  17: tokio::task::coop::with_budget::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::park::CachedParkThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/coop/mod.rs:167:5
  18: tokio::task::coop::budget::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::park::CachedParkThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/coop/mod.rs:133:5
  19: <tokio::runtime::park::CachedParkThread>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/park.rs:284:31
  20: <tokio::runtime::context::blocking::BlockingRegionGuard>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/context/blocking.rs:66:14
  21: <tokio::runtime::scheduler::multi_thread::MultiThread>::block_on::<compress_bench::main::{closure#0}>::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/scheduler/multi_thread/mod.rs:92:22
  22: tokio::runtime::context::runtime::enter_runtime::<<tokio::runtime::scheduler::multi_thread::MultiThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}, core::result::Result<(), anyhow::Error>>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/context/runtime.rs:65:16
  23: <tokio::runtime::scheduler::multi_thread::MultiThread>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/scheduler/multi_thread/mod.rs:91:9
  24: <tokio::runtime::runtime::Runtime>::block_on_inner::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/runtime.rs:376:50
  25: <tokio::runtime::runtime::Runtime>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/runtime.rs:343:18
  26: compress_bench::main
             at ./benchmarks/compress-bench/src/main.rs:152:6
  27: <fn() -> core::result::Result<(), anyhow::Error> as core::ops::function::FnOnce<()>>::call_once
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
  28: std::sys::backtrace::__rust_begin_short_backtrace::<fn() -> core::result::Result<(), anyhow::Error>, core::result::Result<(), anyhow::Error>>
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:166:18
  29: std::rt::lang_start::<core::result::Result<(), anyhow::Error>>::{closure#0}
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:206:18
  30: <&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync as core::ops::function::FnOnce<()>>::call_once
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/core/src/ops/function.rs:287:21
  31: std::panicking::catch_unwind::do_call::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:581:40
  32: std::panicking::catch_unwind::<i32, &dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:544:19
  33: std::panic::catch_unwind::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panic.rs:359:14
  34: std::rt::lang_start_internal::{closure#0}
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/rt.rs:175:24
  35: std::panicking::catch_unwind::do_call::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:581:40
  36: std::panicking::catch_unwind::<isize, std::rt::lang_start_internal::{closure#0}>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:544:19
  37: std::panic::catch_unwind::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panic.rs:359:14
  38: std::rt::lang_start_internal
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/rt.rs:171:5
  39: main
  40: <unknown>
  41: __libc_start_main
  42: _start

Error: GPU decompression failed for: taxi, Arade, CMSprovider, Euro2016, HashTags

Stack backtrace:
   0: <anyhow::Error>::msg::<alloc::string::String>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/anyhow-1.0.104/src/backtrace.rs:10:14
   1: compress_bench::run_compress::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:370:9
   2: compress_bench::main::{closure#0}
             at ./benchmarks/compress-bench/src/main.rs:152:6
   3: <tokio::runtime::park::CachedParkThread>::block_on::<compress_bench::main::{closure#0}>::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/park.rs:284:71
   4: tokio::task::coop::with_budget::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::park::CachedParkThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/coop/mod.rs:167:5
   5: tokio::task::coop::budget::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::park::CachedParkThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/task/coop/mod.rs:133:5
   6: <tokio::runtime::park::CachedParkThread>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/park.rs:284:31
   7: <tokio::runtime::context::blocking::BlockingRegionGuard>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/context/blocking.rs:66:14
   8: <tokio::runtime::scheduler::multi_thread::MultiThread>::block_on::<compress_bench::main::{closure#0}>::{closure#0}
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/scheduler/multi_thread/mod.rs:92:22
   9: tokio::runtime::context::runtime::enter_runtime::<<tokio::runtime::scheduler::multi_thread::MultiThread>::block_on<compress_bench::main::{closure#0}>::{closure#0}, core::result::Result<(), anyhow::Error>>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/context/runtime.rs:65:16
  10: <tokio::runtime::scheduler::multi_thread::MultiThread>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/scheduler/multi_thread/mod.rs:91:9
  11: <tokio::runtime::runtime::Runtime>::block_on_inner::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/runtime.rs:376:50
  12: <tokio::runtime::runtime::Runtime>::block_on::<compress_bench::main::{closure#0}>
             at /home/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.53.1/src/runtime/runtime.rs:343:18
  13: compress_bench::main
             at ./benchmarks/compress-bench/src/main.rs:152:6
  14: <fn() -> core::result::Result<(), anyhow::Error> as core::ops::function::FnOnce<()>>::call_once
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
  15: std::sys::backtrace::__rust_begin_short_backtrace::<fn() -> core::result::Result<(), anyhow::Error>, core::result::Result<(), anyhow::Error>>
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:166:18
  16: std::rt::lang_start::<core::result::Result<(), anyhow::Error>>::{closure#0}
             at /home/runner/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:206:18
  17: <&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync as core::ops::function::FnOnce<()>>::call_once
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/core/src/ops/function.rs:287:21
  18: std::panicking::catch_unwind::do_call::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:581:40
  19: std::panicking::catch_unwind::<i32, &dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:544:19
  20: std::panic::catch_unwind::<&dyn core::ops::function::Fn<(), Output = i32> + core::panic::unwind_safe::RefUnwindSafe + core::marker::Sync, i32>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panic.rs:359:14
  21: std::rt::lang_start_internal::{closure#0}
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/rt.rs:175:24
  22: std::panicking::catch_unwind::do_call::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:581:40
  23: std::panicking::catch_unwind::<isize, std::rt::lang_start_internal::{closure#0}>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panicking.rs:544:19
  24: std::panic::catch_unwind::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/panic.rs:359:14
  25: std::rt::lang_start_internal
             at /rustc/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/std/src/rt.rs:171:5
  26: main
  27: <unknown>
  28: __libc_start_main
  29: _start

The Public BI datasets build their Parquet fixture through the DuckDB CLI, as
in bench-pr.yml. The GPU job never installed it, so all six failed with ENOENT
before reaching the GPU at all.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 3, 2026
The nvCOMP backend only ran the codec stage on the device: page decoding stayed
on the CPU and was excluded from the measurement, so the Parquet figure was an
upper bound and the comparison against Vortex was not like-for-like.

cuDF's `read_parquet` does the whole read on the device — page header decode,
decompression, dictionary/RLE/plain decoding and column assembly — which is the
same amount of work the Vortex backend does when it decodes to canonical arrays.
It is reached through the prebuilt `cudf-cu12` wheel, so it stays a runtime
dependency and never enters the Rust build.

Timing is taken inside scripts/cudf-parquet-read.py, so interpreter start,
`import cudf` and CUDA context creation are excluded; a warm-up read runs first.
`--gpu-verify` now compares the cuDF frame against a CPU Parquet read.

This removes the page scanner, the batched nvCOMP launch path and the nvCOMP
Snappy bindings, all of which existed only to serve the codec-stage backend.
What remains of the Parquet side is the GPU-friendly writer settings, now in
gpu_writer.rs.

Signed-off-by: Claude <noreply@anthropic.com>
The reference side of the Vortex verification was executing through the CUDA
context: the host scan's batches and both Arrow conversions were handed
`cuda_ctx.execution_ctx()`. A CUDA context allocates its outputs in device
memory, so the Arrow conversion then read a device buffer from the host and
panicked with "unwrap_host called for Device allocation" on the string-heavy
Public BI datasets, where canonicalisation goes through the buffer directly.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 13, 2026
@0ax1

0ax1 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Can we make the PR description a bit more compact? 🙂 What do we compare correctness or performance?

with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
enable-sccache: "true"
- name: Install DuckDB

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.

We should centralize this somewhere. I think we have the CI logic for installing DuckDB duplicated like 5 times.

@0ax1 0ax1 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.

couple of thoughts inline

Comment thread benchmarks/compress-bench/src/gpu/parquet.rs
//! the like-for-like opponent for the Vortex GPU backend, which likewise decodes all the way
//! to canonical arrays on device.
//!
//! cuDF is reached through its prebuilt `cudf-cu12` wheel rather than by linking libcudf, so

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.

What does wheel mean here? Oh right, we invoke the Parquet GPU backed via Python and spawn a subprocess. Shall we make the more clear here?

/// Repo-relative path of the script that performs and times the cuDF read.
const CUDF_SCRIPT: &str = "scripts/cudf-parquet-read.py";

/// Parquet compressor whose decompression measurement is a full cuDF GPU read.

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 assume full means the complete file, unfiltered and all columns.

/// Parquet compressor whose decompression measurement is a full cuDF GPU read.
pub struct GpuParquetCompressor {
codec: GpuCodec,
verify: bool,

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.

verify deserves a comment. Does Parquet GPU have a verification for the returned results built in?

columns: u64,
}

impl GpuParquetCompressor {

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.

Are we doing a single iteration or multiple actually? And if one, is that sufficient?

let mut fields_checked = 0usize;
let mut batch_index = 0usize;
loop {
let (gpu_batch, host_batch) = (gpu_batches.next().await, host_batches.next().await);

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.

Having the break condition in the header of a while loop would be more explicit.

//! Parquet writer settings for the GPU benchmark.
//!
//! The GPU backend rewrites each dataset before reading it back with cuDF, so the file it
//! reads is written the way a GPU reader wants it rather than the way the CPU suite writes it.

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.

wants it is a little lose in wording

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum GpuCodec {
/// The Parquet default, and the codec with the highest device-side throughput.
#[default]

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.

Oh does Parquet only do Snappy and Zstd for GPU?

Comment thread benchmarks/compress-bench/src/main.rs Outdated
/// Get a compressor for the given format.
fn get_compressor(format: Format, gpu_decompress: bool) -> Box<dyn Compressor> {
if gpu_decompress {
fn get_compressor(format: Format, gpu: Option<GpuOptions>) -> Box<dyn Compressor> {

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.

API is a little unclear here, with options being optional. 😅 gpu: Option<GpuOptions>). Could we pass GpuOptions and name the parameter options?

Comment thread benchmarks/compress-bench/src/main.rs Outdated
Format::Parquet => Box::new(GpuParquetCompressor::new(gpu.codec, gpu.verify)),
_ => unimplemented!("GPU compress bench not implemented for {format}"),
};
#[cfg(not(feature = "cuda"))]

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.

Yeah I don't think the fact that the code is either compiled on Linux or macOS should not be represented by the Option.

@0ax1

0ax1 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

codex review:


Findings

  1. High — lazy device-backed validity is executed through a CPU context
    vortex-cuda/src/canonical.rs:52-55

    validity_into_host() first executes Validity::Array using legacy_session() and only then copies it to the host. That works for an already-canonical device BoolArray, but not for lazy validity expressions with device-backed children.

    A concrete trigger is a dictionary with nullable values and bit-packed codes:

    • CUDA decoding materializes the codes into device memory.
    • vortex-cuda/src/kernel/arrays/dict.rs:175-181 constructs output validity as values_validity.take(codes).
    • This is a lazy boolean dictionary referencing the device-backed codes.
    • validity_into_host() executes it on the CPU, eventually reading the codes as a host slice and panicking.

    This breaks --gpu-verify for valid nullable dictionary encodings. Materialize validity through CUDA before copying it to the host, or migrate its dependency tree first. Add a test combining nullable dictionary values, bit-packed codes, and CanonicalCudaExt::into_host().

  2. High — CI discards the partial matrix when the benchmark reports failures
    .github/workflows/pr-bench-gpu-compress.yml:77-90
    benchmarks/compress-bench/src/main.rs:362-377

    The benchmark intentionally renders successful timings and then exits non-zero if any dataset failed. However, GitHub’s Bash shell runs with -e, so the non-zero benchmark command:

    • skips cat gpu-compress.txt;
    • causes the Publish results step to be skipped;
    • leaves only the generic failure comment.

    This defeats the branch’s “survey every dataset and publish partial results” behavior. Capture the benchmark status, publish with if: always(), and propagate the failure in a final gate step.

  3. High — Parquet verification can accept incorrect decompression
    scripts/cudf-parquet-read.py:66-68
    scripts/cudf-parquet-read.py:91-103

    assert_frame_equal(..., check_dtype=False) uses tolerant floating-point comparison by default. A small floating-point decoding error can therefore pass even though Parquet decompression must be lossless. Use check_exact=True while still disabling dtype comparison if representation differences are intentional.

    Additionally, only the warm-up frame is verified. The separately timed frame is immediately deleted, despite the README claiming verification happens on every iteration. Stop the timer, then verify that timed frame outside the measured interval.

  4. Medium — exact rebatching can retain roughly three decoded copies of a dataset
    vortex-bench/src/conversions.rs:131-148

    The new implementation:

    1. collects every Arrow batch;
    2. concatenates them into another full-table allocation;
    3. accumulates copied canonical Vortex chunks while both Arrow representations remain alive.

    This substantially increases peak memory compared with the previous streaming conversion and can OOM on larger Public BI inputs. Implement exact boundaries with a streaming carry/coalescing buffer rather than concatenating the complete table. At minimum, explicitly drop the source batch vector after concatenation, though that still leaves two full copies.

  5. Medium — distinct GPU configurations collide in benchmark ingest
    benchmarks/compress-bench/src/main.rs:455-465

    Every GPU timing record gets dataset_variant = "gpu". Neither of the behavior-changing options is represented:

    • --gpu-parquet-codec snappy|zstd;
    • --gpu-direct-io.

    The ingest measurement ID includes the dataset variant, format, and operation, so running different GPU configurations for the same commit overwrites the previous records. The records are also ambiguous when viewed later. Add configuration dimensions to the identity, or reject non-default GPU configurations when --ingest-jsonl is used until the schema can represent them.

  6. Medium — direct-I/O mode still emits a known invalid cross-format ratio
    benchmarks/compress-bench/src/main.rs:477-480
    benchmarks/compress-bench/src/main.rs:493-514

    With --gpu-direct-io, Vortex bypasses the page cache while cuDF still times a cached read after warm-up. Nevertheless, push_gpu_ratio() emits the normal vortex:parquet-<codec> gpu ratio measurement. The README warns users not t interpret it as a decode comparison, but it should not be emitted under the same metric name. Suppress it in direct-I/O mode or label it as a different measurement.

  7. Medium — CI’s cuDF comparator is unpinned
    .github/workflows/pr-bench-gpu-compress.yml:49-51

    cudf-cu12, pandas, and pyarrow resolve to current releases on every workflow run. A package release can therefore change the measured Parquet implementation—or break compatibility—without a repository change. Pin a tested set of versions or install from a checked-in lock/requirements file.

Lower-priority issues

  • .github/workflows/pr-bench-gpu-compress.yml:42-43 should pass sync: false to setup-uv; otherwise the action performs the unrelated repository-wide Python sync before creating .venv-cudf.
  • benchmarks/compress-bench/README.md:45-50 should show creation/selection of a Python 3.12 virtual environment and mention the DuckDB CLI requirement. The current standalone uv pip install command fails without an active or discoverable environment.
  • scripts/cudf-parquet-read.py:51-54 only recognizes DATE columns when the first value is non-null. A DATE column beginning with null can still fail verification because the CPU and GPU representations are not normalized.

joseph-isaacs and others added 2 commits August 17, 2026 12:31
Centralize the DuckDB CLI install as a `setup-duckdb` composite action and use it from
all four workflows that had the download inlined.

Behaviour changes, each from a review thread:

- `GpuParquetCompressor::compress` now refuses like the Vortex GPU backend does. It timed
  the host Parquet writer, which is not a device measurement, and GPU mode only runs the
  decompress op so the number was never rendered.
- `--gpu-verify` no longer returns its own elapsed time as the decompress measurement.
  That bundled a file copy, a second host scan and every Arrow conversion into a number
  the timing table published as a decode time. Verification is now a precondition and the
  ordinary timed scan runs after it, so a verifying run reports a comparable number.
- The cuDF script is asked for several timed reads per invocation instead of the default
  one, so the reported minimum is over repeated reads rather than a single sample.
- `--gpu-direct-io` on a non-Linux target is now an error instead of a silent no-op, since
  the flag changes what the measurement means.

Readability, also from review:

- Build the cuDF `Command` in one expression, and move the "is cudf-cu12 installed" hint
  onto the non-zero-exit arm, where a failed `import cudf` actually surfaces.
- Give the verification loop its exit condition in the header via `next_batch_pair`, which
  keeps a batch-count mismatch an error rather than a stopping condition.
- Rename the `Option<GpuOptions>` parameters to `options`/`gpu_options`.

Comment fixes: spell out that the Parquet backend drives cuDF through a `python3`
subprocess rather than a linked library; say that "whole-file" means every row and column
with no pushdown; document the `verify` field and that cuDF has no built-in verification;
note that the two-codec `GpuCodec` set is a deliberate choice, not a format limit; and
reword the writer module header. README updated to match.

Checks: `cargo clippy -p compress-bench --all-targets -- -D warnings` and the same with
`--features cuda,unstable_encodings` both pass; `cargo test -p compress-bench --lib`
passes; `yamllint --strict -c .yamllint.yaml` passes on the five changed YAML files.
Not run: the CUDA tests and the GPU benchmark itself, which need a GPU.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FnRK96p8jiB9NdSKxU2iy
`Option<GpuOptions>` conflated two questions: which suite this run measures, and how the
GPU is configured. GPU mode is not a setting on the CPU suite — it picks a different
dataset list, restricts the ops to decompression, swaps both compressors and labels its
ratio differently — so `None` was carrying the weight of "run the host suite".

Replace it with `BenchMode { Cpu, Gpu(GpuOptions) }`. The `is_some()` checks become
`is_gpu()`, and the ratio dispatch and compressor selection match on the variant, so no
site has to know that an absent config means the CPU path.

Checks: `cargo clippy -p compress-bench --all-targets -- -D warnings` and the same with
`--features cuda,unstable_encodings` both pass; `cargo test -p compress-bench --lib`
passes.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FnRK96p8jiB9NdSKxU2iy
@joseph-isaacs
joseph-isaacs marked this pull request as ready for review August 17, 2026 12:48
@joseph-isaacs joseph-isaacs added the changelog/feature A new feature label Aug 17, 2026
joseph-isaacs and others added 2 commits August 17, 2026 12:55
The list carried nine datasets while its own comment said to add one "only after its
CUDA-compatible compression and decompression kernels have been verified end to end".
Five had not been: the last published verification matrix showed `taxi` and `Arade`
failing on `Unsupported ptype u16`, `Euro2016` and `HashTags` on a missing
`vortex.masked` kernel, and `CMSprovider` on `expected host buffer`. Since a failing
dataset makes the run exit non-zero, the benchmark job could not go green.

Keep the four that verified — both TPC-H `l_comment` variants, `Bimbo` and `Food` — and
record the other five next to the list with the `vortex-cuda` gap each is waiting on, so
closing a gap has an obvious place to re-add its dataset. The per-dataset survey and
failure summary stay, so a listed dataset that regresses is still reported rather than
aborting the run.

Checks: `cargo clippy -p compress-bench --all-targets -- -D warnings` and the same with
`--features cuda,unstable_encodings` both pass. The GPU benchmark itself needs a GPU and
was not run here, so the four remaining datasets should be re-confirmed with
`--gpu-verify` on a GPU runner.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FnRK96p8jiB9NdSKxU2iy
`uvx ruff format --check .` failed the Python (lint) job on the `--iterations`
argument: the call was wrapped across three lines, but at 91 characters it fits
inside the repository's 120-character limit, so ruff collapses it to one line.

Ran `uvx ruff format` rather than hand-editing so the result matches CI byte for
byte. `uvx ruff format --check .` and `uvx ruff check .` both pass locally now.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FnRK96p8jiB9NdSKxU2iy
@joseph-isaacs
joseph-isaacs force-pushed the claude/gpu-decompress-benchmarks-4mmn93 branch from 085a592 to 29ccc8b Compare August 17, 2026 18:36
joseph-isaacs and others added 2 commits August 18, 2026 09:35
Two whole functions in `canonical.rs` carried `#[allow(clippy::disallowed_methods)]`, which
silences the `legacy_session` lint across every line in them — including code added later that
nobody chose to waive.

Replace both with `#[expect(clippy::disallowed_methods, reason = ...)]` on the three
`legacy_session().create_execution_ctx()` bindings themselves, matching the form already used
177 times elsewhere in the tree. `expect` also fails the build if the call is ever threaded a
real session, so the waiver cannot outlive the problem.

Checks: `cargo clippy -p vortex-cuda --all-targets -- -D warnings` passes, which also proves
each of the three expectations is fulfilled at its narrowed scope.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FnRK96p8jiB9NdSKxU2iy
`verify_against_host_scan` held stream setup, batch pairing, field pairing, the CUDA decode and
the comparison in one body. Move everything between the two `ensure!` guards into `verify_batch`,
which returns the number of fields it checked, so the caller is left with the scan wiring and a
running total.

Drop the two `collect::<Vec<_>>()` calls while moving it: `iter_unmasked_fields` is already an
`ExactSizeIterator`, so the count check reads its `len()` directly and the fields zip as
iterators instead of being cloned into two vectors per batch. Only the GPU side still clones,
because `execute_cuda` takes `self`.

Checks: `cargo clippy -p compress-bench --all-targets --features cuda,unstable_encodings --
-D warnings` passes. The verification path itself needs a GPU and was not run here.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FnRK96p8jiB9NdSKxU2iy
@joseph-isaacs
joseph-isaacs force-pushed the claude/gpu-decompress-benchmarks-4mmn93 branch from 3f3cff6 to 537e30b Compare August 18, 2026 09:48
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026
@joseph-isaacs
joseph-isaacs force-pushed the claude/gpu-decompress-benchmarks-4mmn93 branch from 398074e to 537e30b Compare August 18, 2026 12:07
@joseph-isaacs joseph-isaacs added the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants