Skip to content

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

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

Compare Vortex GPU decompression against a cuDF Parquet read#9147
joseph-isaacs wants to merge 17 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 degrade performance by 8.28%

⚠️ 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.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 3 regressed benchmarks
✅ 2033 untouched benchmarks
⏩ 46 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation decompress[u64, (10000, 4)] 310.2 µs 401.7 µs -22.77%
Simulation decompress[u64, (1000, 16)] 64.4 µs 72.8 µs -11.57%
Simulation take[small_m/shuffled/primitive/nonnull/chunks=16384/indices=16] 1 ms 1.2 ms -11.31%
Simulation cold_misaligned[(64, 256)] 5.1 ms 4.4 ms +16.82%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/gpu-decompress-benchmarks-4mmn93 (8e060ca) with develop (93b0535)

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.

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
claude added 2 commits August 13, 2026 16:10
…ess-benchmarks-4mmn93

Signed-off-by: Claude <noreply@anthropic.com>
The verification gate ran before the timed pass, so a single unsupported
dataset meant the run produced no numbers at all. Two changes:

- the per-dataset survey now applies to any GPU run, not just a verifying
  one, and the timing tables render before the failure summary, so datasets
  that do decode still publish their numbers;
- the workflow runs the benchmark before the gate and fails the job at the
  end on either a failed verification or a failed benchmark.

Signed-off-by: Claude <noreply@anthropic.com>
@joseph-isaacs joseph-isaacs changed the title Add a GPU Parquet decompression backend to the compression benchmark Compare Vortex GPU decompression against a cuDF Parquet read Aug 13, 2026
@joseph-isaacs joseph-isaacs added action/bench-gpu-compress Run only the GPU compression benchmark on this PR and removed action/bench-gpu-compress Run only the GPU compression benchmark on this PR action/benchmark-gpu-compress labels Aug 13, 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 13, 2026
claude added 3 commits August 13, 2026 16:17
Signed-off-by: Claude <noreply@anthropic.com>
The per-dataset grep captures only the first line of each error, so a Python
traceback from the cuDF script or a Rust backtrace never reached the comment.
Attach the tail of the raw output in a collapsed block on failure, and match
the summary line's current wording.

Signed-off-by: Claude <noreply@anthropic.com>
fix(cuda): copy validity back to the host in `into_host`

`CanonicalCudaExt::into_host` migrated a canonical array's values buffer but
passed its validity through untouched, so a nullable array came back to the
host half-migrated and the first host read of the mask panicked with
"unwrap_host called for Device allocation" — via `Validity::execute_mask` ->
`BoolArray::into_bit_buffer`. Non-nullable arrays were unaffected, which is
why it only showed up on the Public BI tables. The `Bool` arm already carried
a TODO for exactly this.

Do not compare Parquet DATE columns across representations

pyarrow materialises a DATE column as `datetime.date` objects and cuDF as
`datetime64[s]`. The values agree, but `check_dtype=False` does not bridge
object-vs-datetime64, so the comparison reported every row as different and
failed both TPC-H datasets. Coerce both sides to datetime64 first.

Read the Vortex GPU file through the page cache by default

cuDF takes an untimed warm-up read, so its timed read is served from the page
cache, while the Vortex reader used `O_DIRECT` on every iteration and paid
real disk reads each time. That compared a read of the disk against a read of
RAM. Direct IO is now off by default and available behind `--gpu-direct-io`
for measuring storage bandwidth, which is not a decode comparison.

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 — with Claude
@github-actions github-actions Bot removed the action/bench-gpu-compress Run only the GPU compression benchmark on this PR label Aug 13, 2026
The GPU ratio says which of two GPU readers is faster, not whether either
beats the CPU decoders. Run the same binary over the same datasets with the
CPU path on the same machine and publish it alongside, so the GPU numbers can
be read against something.

Also capture the benchmark's exit status rather than letting `shell: bash`'s
-e skip the `cat`, which kept the timing tables out of the job log and left
them only in the PR comment.

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 14, 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 14, 2026
claude added 2 commits August 14, 2026 10:51
The workflow had grown a verification pass, a CPU baseline pass, two extra PR
comments and a separate failure gate. None of that is needed to report a
Parquet and a Vortex number. Reset the file to its develop version and add
back only what the benchmark cannot run without:

- the DuckDB CLI, which builds the Public BI Parquet fixtures;
- uv and the cuDF wheel, which the GPU Parquet backend shells out to.

Every pre-existing step is now untouched, and `--gpu-verify` remains available
as a local flag.

Signed-off-by: Claude <noreply@anthropic.com>
Parquet was read as ~1M-row row groups while Vortex inherited the Arrow
reader's ~8K-row batches, so the Vortex file carried hundreds of small chunks
where Parquet carried a handful of row groups. A chunk is the unit the reader
plans and dispatches over, so that turned single dispatches into hundreds of
small kernel launches and made the two numbers measure different shapes of
work rather than two decoders.

Pin both to `GPU_ROW_GROUP_SIZE` (1,048,576 rows, Parquet's
`DEFAULT_MAX_ROW_GROUP_ROW_COUNT`):

- state the Parquet row group count explicitly so it moves with the constant;
- add `parquet_to_vortex_chunks_with_batch_size`, which concatenates the source
  batches and re-slices on exact boundaries. Setting the Arrow reader's batch
  size alone is not enough, because the reader also breaks at the source file's
  row group boundaries and still emits short batches;
- write those batches through as root chunks with `ChunkedLayoutStrategy` and
  read them back with `SplitBy::RowCount(GPU_ROW_GROUP_SIZE)`.

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 14, 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 14, 2026
@vortex-data vortex-data deleted a comment from github-actions Bot Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

BENCHMARK FAILED

GPU Compression failed. Check the workflow run for details.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants