Skip to content

Benchmarks

iderex edited this page Jul 19, 2026 · 1 revision

Benchmarks

The measured baselines for cudec's LZ4 decoder. A number without its methodology is not a claim — every figure here carries the conditions it was measured under (GPU, driver, CUDA version, corpus, chunk-size distribution, run count), and the benchmark harness (bench/bench_lz4) emits that block inline so a bare number cannot be produced by construction. The raw record, with the full harness reports, is docs/BENCHMARKS.md; the narrative on how to read and reproduce these numbers is docs/BENCHMARK-METHODOLOGY.md. This page is a summary of both.

Read this first

  • These are single-machine numbers from the development hardware, not a cross-vendor sweep. GPU: NVIDIA GeForce RTX 3080 (sm_86, 10 GB GDDR6X, ~760 GB/s output-bandwidth ceiling), driver 12.6, CUDA runtime 12.6, inside the digest-pinned dev container (nvidia/cuda:12.6.2-devel-ubuntu24.04). Host CPU for the reference: AMD Ryzen 9 5950X. GPU timing jitters ~1–2% run to run.
  • The CPU figure is the reference the GPU is measured against: the single-threaded liblz4 1.10.0 LZ4_decompress_safe, not a parallel CPU decode. The comparison is honest about that.
  • The device-resident GPU rows exclude host↔device transfer (the data is already on the GPU, CUDA-event timed). The streaming rows include the transfer and per-wave submission — a different, honest metric, not a regression of the device-resident number.
  • No cudec-vs-nvCOMP head-to-head is published here; the reasoning (and how a third party can run one under their own license) is in the methodology document. cudec's own numbers and the CPU comparison are what is reported.

LZ4 block decode — Silesia (the headline number)

The average-case throughput on the Silesia corpus (3239 chunks, ~212 MB original, median chunk 64 KiB), recorded 2026-07-17:

Path Throughput (p50) Relative
CPU oracle (liblz4, single thread) ~3.41 GB/s 1× (reference)
cudec GPU decode (device-resident) ~18.1 GB/s ~5.3× CPU
cudec GPU parse-only ceiling (copies elided) ~34.6 GB/s ~10× CPU

Full harness report for the GPU decode:

## bench_lz4 report
- decoder: CPU oracle, LZ4_decompress_safe (liblz4 1.10.0), single thread
- host CPU: AMD Ryzen 9 5950X 16-Core Processor
- CUDA device: NVIDIA GeForce RTX 3080 (sm_86), driver 12.6, runtime 12.6
- corpus: dickens+mozilla+mr+nci+ooffice+osdb+reymont+samba+sao+webster+x-ray+xml, 3239 chunks, 211.94 MB original, 102.44 MB compressed (ratio 0.483), compressed in-harness via LZ4_compress_default
- chunk sizes: min 8066 / median 65536 / max 65536 bytes
- method: 3 warmup + 30 measured runs; CPU wall clock per whole-batch decode; GPU device-resident, CUDA-event timed; output byte-verified before timing
- CPU decode throughput (liblz4 baseline): p50 3.41 GB/s
- GPU decode (cudec, device-resident): p50 11.7 ms, 18.1 GB/s (~5.3x the CPU baseline)
- GPU parse-only ceiling (copies elided): p50 6.1 ms, 34.6 GB/s

The parse-only ceiling matters beyond a curiosity: the decoder is parse-bound, not bandwidth-bound, and that single number settles the kernel-decomposition question (a two-phase scan-then-copy design shares the identical serial parse, so it cannot exceed the same ceiling). Two measured optimization passes (copy/parse micro-ops, and a higher-occupancy register-reduction lever) were both rejected by measurement and shipped no code — the detail is in MASTERPLAN §9 and the BENCHMARKS record. Reproduce with bench_lz4 --gpu.

Worst case — an adversarial-but-valid block

A security-posture number, not an average. The worst input for this kernel is a valid LZ4 block of back-to-back minimum matches (match length 4, offset 1) — the maximum sequence density a valid block can carry, one parsed sequence per 4 decoded bytes. A standard compressor never emits it (it would extend an offset-1 run into one long match), so the harness constructs it directly (--worst4b) and the liblz4 oracle validates it before any timing. Recorded on the same hardware, 3200 identical 64 KB chunks (~210 MB):

Path Silesia average Worst-4Bmatch Degradation
CPU oracle (liblz4, single thread) ~3.41 GB/s ~1.49 GB/s ~2.3×
cudec GPU decode (device-resident) ~18.1 GB/s ~8.1 GB/s ~2.2×
cudec GPU parse-only ceiling ~34.6 GB/s ~15.3 GB/s ~2.3×

The degradation is linear and bounded — every path slows by the same ~2.2–2.3× factor, which is exactly the sequence-density ratio, with no super-linear blow-up and no size amplification (each chunk decodes to at most its destination capacity; the block barely compresses). The GPU advantage holds under the worst input: ~8.1 GB/s is still ~5.4× the CPU worst case. Reproduce with bench_lz4 --worst4b --gpu; the construction is oracle-validated in-harness and locked against rot by a ctest self-check on the GPU-less runner.

Streaming path — end-to-end, host memory in

The streaming decoder takes host-resident compressed chunks, stages them to the GPU, decodes, and (for host output) reads the result back. Its wall is clocked around the whole synchronous call, so it includes the transfers and the per-wave submission cost — a different metric from the device-resident kernel number above:

Path (reusable context, steady state) Throughput (p50)
Device output (stays in VRAM) ~0.92 GB/s
Host output (synchronous readback) ~0.58 GB/s

These are dominated not by decode (~12 ms device-resident for the whole batch) or the compressed H2D copy (~4 ms) but by per-wave serial submission: the batch is submitted in ~51 waves on this WSL2/WDDM setup, where each submission flush costs milliseconds. Raising the wave granularity so the path submits once is the open lever, tracked as issue #33. The reusable context (cudec_stream_ctx_create / cudec_lz4_decompress_stream_ctx / cudec_stream_ctx_destroy) exists so the staging allocation — measured at only ~8 ms — is paid once rather than per call; prefer it over the one-shot streaming entry point for repeated decodes. Reproduce with bench_lz4 --gpu --gpu-stream-ctx.

Reproducing any of these

The corpora are fetched hash-pinned via bench/get-corpora.sh (never committed). Build the CUDA target in the pinned container (see Home for the exact docker run line, with --gpus all for the device path), then run bench/bench_lz4 with the corpus paths and the --gpu flag. Every run reprints its own methodology block; that block, not the throughput line alone, is the citable result.

See also