|
FZGPUModules 2.0
GPU-accelerated modular compression pipelines
|
This page is a concise reference for the fz::Pipeline class — the primary public interface. For context on how these pieces fit together see the Architecture Overview. For per-stage configuration options see the Stage Reference.
Every pipeline follows the same call sequence:
finalize() is the dividing line. Configuration calls go before it; execution calls go after.
Defined in include/advanced/dag.h.
| Value | Behavior |
|---|---|
MINIMAL | Allocate buffers on demand; free each one as soon as its last consumer reads it. Lowest peak GPU memory. |
PREALLOCATE | Allocate all buffers during finalize(). Required for CUDA Graph capture. Enables buffer coloring for lower memory footprint. |
Defined in modules/fused/lorenzo_quant/lorenzo_quant.h. Used by LorenzoQuantStage, QuantizerStage, and GInterpStage.
| Value | Meaning | Notes |
|---|---|---|
ABS | Absolute error — abs(x_orig - x_recon) ≤ eb | Useful when data is homogenous in magnitude (preserve big picture) |
REL | Guaranteed point-wise relative — abs(error) / abs(x_orig) ≤ eb for every element | Implemented only by QuantizerStage, via log-space quantization. LorenzoQuantStage / GInterpStage accept it as a deprecated alias for PREL and warn. |
NOA | Value-range relative — abs(error) / value_range ≤ eb (norm-of-absolute) | Useful for single bounds over multiple datasets |
PREL | Pseudo-relative — abs_eb = eb × max(abs(data)), then applied as a plain ABS bound | Bounds error / max(abs(x)), not error / abs(x). The cheap approximation used by the predictor-fused stages, which cannot vary the bound per element. |
REL vs PREL — the distinction that matters. PREL is only as tight as REL for elements at the peak magnitude. An element at 1% of peak sees an effective relative error 100× looser than the eb you asked for, and elements near zero are unbounded in relative terms. If you need a per-element relative guarantee, QuantizerStage with REL is the only stage that provides it. examples/eb_mode_analysis.cpp measures this on your own data.
| Call | Purpose |
|---|---|
setDims(x)setDims(x, y, z) | Spatial dimensions of the input data. Push dims before addStage() for Lorenzo-family stages. |
setMemoryStrategy(strategy) | Switch between MINIMAL and PREALLOCATE. |
setNumStreams(n) | Number of parallel CUDA streams for level-based execution (default: 4). |
enableGraphMode(true) | Enable CUDA Graph capture mode. Requires PREALLOCATE. |
setWarmupOnFinalize(true) | Auto-run warmup() at the end of finalize(). |
setColoringEnabled(false) | Disable buffer coloring (useful when inspecting buffers with a memory checker). |
enableBoundsCheck(true) | Enable runtime buffer-overwrite detection (always active in debug builds). |
Important: Call setDims() before addStage() for any Lorenzo-family stage. The dims are pushed into the stage at add-time and again at finalize().
New code should prefer the span-based Explicit-ownership API below, which states ownership in the return type. The pointer overloads documented here remain fully supported; ownership for each is summarized in the Memory Ownership Summary.
The default (pool-owned) path is the simplest — the pipeline holds the buffer, so you never cudaFree it:
The other overloads write into a caller-provided buffer (or hand back a caller-owned allocation). Each throws if a supplied buffer is too small, reporting the size needed:
| Overload | Ownership | Notes |
|---|---|---|
compress(in, n, d_buf, capacity, &actual, stream) | caller's buffer | size with getMaxCompressedSize(n) |
decompress(in, n, &d_out, &sz, stream) after setPoolManagedDecompOutput(false) | caller-owned (cudaFree) | fresh allocation each call |
decompress(in, n, d_buf, capacity, &actual, stream) | caller's buffer | synchronous; no temp alloc/copy/free |
decompressInto(in, n, d_buf, capacity, &actual, stream) | caller's buffer | fully async for overlapped decode — see Performance Tuning |
decompressInto() requires PREALLOCATE, leaves *actual as the planned size, and does not synchronize — the bytes are valid only after you synchronize the stream yourself. Use one Pipeline per concurrent stream. (Some inverse coders — RZE/RRE/Huffman/ AdaptiveBitpack — still do a blocking device→host header read inside execute(); for full overlap of those, drive each slot from its own host thread.)
Sizing helpers: getMaxCompressedSize(input_bytes) (tight upper bound for a compress buffer) and getLastUncompressedSize() (original size from the most recent compress()).
The pointer overloads above encode ownership in how you call them (void** vs void*) and in mutable pipeline state (setPoolManagedDecompOutput()). The span-based API states it in the type instead, and is a thin wrapper over the same execution core — identical behavior, no performance difference.
| Call | Returns | Ownership |
|---|---|---|
compress(ConstDeviceSpan, stream) | BorrowedDeviceBuffer | Pool-owned; do not free |
compressInto(ConstDeviceSpan, DeviceSpan, stream) | size_t bytes written | Caller's buffer |
decompressBorrowed(ConstDeviceSpan, stream) | BorrowedDeviceBuffer | Pool-owned; do not free |
decompressOwned(ConstDeviceSpan, stream) | OwnedDeviceBuffer | Caller-owned; freed on destruction |
decompressInto(ConstDeviceSpan, DeviceSpan, stream) | size_t bytes written | Caller's buffer (synchronous) |
decompressIntoAsync(ConstDeviceSpan, DeviceSpan, stream) | size_t planned bytes | Caller's buffer (async; PREALLOCATE only) |
decompressBorrowed() and decompressOwned() ignore setPoolManagedDecompOutput() — the call site decides, and the flag is left exactly as the caller set it. OwnedDeviceBuffer is move-only and records the device it was allocated on, so it frees through the backend the library was built against (never a hard-coded cudaFree) and on the right device. release() hands the raw pointer back if you need to manage it yourself.
The types live in include/pipeline/device_buffer.h and allocate nothing themselves; DeviceSpan / ConstDeviceSpan are plain non-owning views.
| Buffer | Owner | Rule |
|---|---|---|
Input (d_input) | Caller | Pipeline borrows; never freed by the library |
| Compressed output (pool-owned) | Pipeline | Do not cudaFree |
| Decompressed output (pool-owned, default) | Pipeline | Do not cudaFree |
| Decompressed output (caller-owned) | Caller | Must cudaFree |
File decompress (decompressFromFile static) | Caller | Must cudaFree |
File decompress (decompressFromFileInstance) | Depends on setPoolManagedDecompOutput() | Same rules as decompress() |
Memory decompress (decompressFromMemory) | Depends on setPoolManagedDecompOutput() | Same rules as decompress() |
BorrowedDeviceBuffer (from compress() / decompressBorrowed()) | Pipeline | Do not free; invalidated by the next call reusing the slot |
OwnedDeviceBuffer (from decompressOwned()) | Caller | Freed on destruction; release() to take the raw pointer |
See the FZM File Format page for the full file header specification.
A pipeline that only ever decompresses blobs produced elsewhere — e.g. K streaming slots reading independently-compressed blocks — does not need a throwaway compress() over dummy data to become ready. The in-memory decompress() path otherwise depends on state a forward compress() leaves on the instance: the archive layout and the data-dependent inverse metadata that is not in the raw blob (the HuffmanStage symbol count, the quantizer outlier count — which changes block to block). Carry that metadata in a small in-memory header instead:
serializeHeaderToMemory() requires a prior compress(); returns the FZM core+stage+buffer header as host bytes (no payload). The header is per-blob because the outlier count varies block to block.decompressFromMemory() fuses primeInverseFromHeader() + decompress(); output ownership follows setPoolManagedDecompOutput() exactly as decompress() does.primeInverseFromHeader(header...) then decompress(blob...)).prepareInverse(uncompressed_size) needs no header at all.examples/decode_only_slots.cpp.Records the compression pass as a replayable graph, eliminating CPU kernel-launch overhead on repeated calls. See Performance Tuning for when this is worth it.
Requirements: PREALLOCATE, non-zero input size at construction, a single-source pipeline of graph-compatible stages, and the same stream for capture and replay. Incompatible with the caller-owned compress() overload.
| Call | Purpose |
|---|---|
pipeline.printPipeline() | Print stage graph, buffer assignments, and execution levels |
pipeline.enableProfiling(true) | Enable per-stage GPU timing |
pipeline.getLastPerfResult() | Per-stage timing from the last compress/decompress |
pipeline.getPeakMemoryUsage() | Peak pool bytes from the last run |
pipeline.getCurrentMemoryUsage() | Live pool bytes right now |
pipeline.isMemPoolFallbackMode() | True if the CUDA pool fell back to cudaMalloc (e.g. vGPU) |
pipeline.reset(stream) | Free non-persistent buffers and reset state for re-use |
"codes", not the default "output".cudaFree compress output or default decompress output.finalize().The public headers are organized into three tiers by how stable they are:
| Tier | What | Headers | Guarantee |
|---|---|---|---|
| Stable | The API most users need | fzgpumodules.h, pipeline/compressor.h (Pipeline), pipeline/device_buffer.h, pipeline/config.h, pipeline/perf.h, pipeline/stat.h, fzm_format.h, the modules/**/*_stage.h stage headers, the MemoryStrategy / ErrorBoundMode enums | source-compatible within a major version |
| Extension | For custom-stage authors | stage/stage.h, stage/stage_registry.h, stage/fusion.h, mem/mempool.h (the MemoryPool interface a stage's execute() uses) | may grow at minor versions via backward-compatible defaults |
Advanced (include/advanced/) | Pipeline internals exposed for experimentation | advanced/dag.h (CompressionDAG / DAGNode / BufferInfo), advanced/fusion_planner.h, advanced/fusion_registry.h | no source-compatibility promise; may change any release |
The umbrella fzgpumodules.h no longer advertises the Advanced headers. Pipeline still depends on CompressionDAG internally, so those types remain reachable transitively — but reach for them deliberately (#include "advanced/dag.h"), knowing they are unstable. Anything under src/, the kernel implementations in modules/*.cu, allocation heuristics, pool sizing, buffer-coloring details, and logging output text may change in any release without being treated as breaking.
A major version bump is required when:
Stage virtual method in a way that breaks custom stagesMinor bumps cover backward-compatible additions (new methods, overloads, optional fields with safe defaults). Patch bumps cover bug fixes, documentation fixes, and non-behavioral cleanup.
No ABI compatibility guarantee is made across any release — recompile downstream code against the library version in use.
StageType enum values are serialized in .fzm files — existing values must never be renumbered or reused, even after a stage is removed. Adding, removing, or changing any Stage virtual method signature is a breaking change and requires a major-version bump.
Use when opening a PR that touches a public header:
include/?Stage virtual method signatures or behavioral contracts?If any answer is "yes" and the change is not backward-compatible, schedule it as a major-version bump.