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 \ref architecture "Architecture Overview".
For per-stage configuration options see the \ref stages_overview "Stage Reference".
Every pipeline follows the same call sequence:
Pipeline(input_size) ← size the memory pool
setDims(x, y, z) ← spatial dims before addStage (if needed)
addStage<T>(...) ← add one or more stages
connect(downstream, upstream, "port") ← wire the data flow
finalize() ← validate, sort, (pre-)allocate
compress(d_in, ...) ← compress on GPU
decompress(d_comp, ...) ← decompress on GPU
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. |
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 |
// Default: MINIMAL strategy, pool = 1 GiB
fz::Pipeline p;
// Sized: pool = input_size × multiplier (default 3.0)
fz::Pipeline p(input_bytes);
fz::Pipeline p(input_bytes, fz::MemoryStrategy::PREALLOCATE);
fz::Pipeline p(input_bytes, fz::MemoryStrategy::PREALLOCATE, /*multiplier=*/4.0f);
// From a TOML config file (adds stages + calls finalize() internally)
fz::Pipeline p("pipeline.toml");| 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). |
// Add a stage — returns a raw pointer owned by the Pipeline
StageT* stage = pipeline.addStage<StageT>(/* stage constructor args */);
// Wire two stages (downstream reads from upstream's named output port)
pipeline.connect(downstream, upstream); // uses "output" port
pipeline.connect(downstream, upstream, "codes"); // named port
pipeline.connect(downstream, {upstream_a, upstream_b}); // multi-input
// Finalize: validate, sort, allocate
pipeline.finalize();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 \ref explicit_ownership_api "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 \ref memory_ownership_summary "Memory Ownership Summary".
The default (pool-owned) path is the simplest — the pipeline holds the buffer, so
you never cudaFree it:
void* d_compressed = nullptr;
size_t compressed_sz = 0;
pipeline.compress(d_input, input_bytes, &d_compressed, &compressed_sz, stream);
// valid until the next compress()/reset() or Pipeline destruction
void* d_output = nullptr;
size_t output_sz = 0;
pipeline.decompress(d_compressed, compressed_sz, &d_output, &output_sz, stream);
// valid until the next decompress() or Pipeline destructionThe 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](\ref 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) |
fz::BorrowedDeviceBuffer comp = pipeline.compress({d_in, in_bytes}, stream);
// comp is pool memory: no cudaFree, invalidated by the next compress()/reset()
fz::OwnedDeviceBuffer out = pipeline.decompressOwned(comp.cspan(), stream);
// out.data() / out.bytes(); freed when `out` goes out of scopedecompressBorrowed() 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.
For pipelines that pad their input, direct caller-supplied decode buffers must accommodate the padded inverse result, although the returned length describes only the logical data. A too-small buffer is rejected before execution and the error reports the required capacity. Borrowed/owned decode forms allocate that storage internally. Static file decompression copies only the logical prefix into its returned allocation; archive logical lengths do not prescribe scratch capacity.
| 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 |
// Write compressed data to a .fzm file (compress() must be called first)
pipeline.writeToFile("output.fzm", stream);
// One-shot decompress from file — no pipeline setup needed (static)
// Output is always caller-owned; caller must cudaFree *d_output
void* d_output = nullptr;
size_t output_sz = 0;
fz::Pipeline::decompressFromFile("output.fzm", &d_output, &output_sz, stream);
cudaStreamSynchronize(stream);
cudaFree(d_output);
// Instance decompress from file — output ownership follows setPoolManagedDecompOutput()
pipeline.decompressFromFileInstance("output.fzm", &d_output, &output_sz, stream);
// Read the header without decompressing
auto header = fz::Pipeline::readHeader("output.fzm");
size_t original_size = header.core.uncompressed_size;
// Load / save pipeline config as TOML
pipeline.loadConfig("pipeline.toml"); // also calls finalize()
pipeline.saveConfig("pipeline.toml"); // requires finalize() firstSee the \ref fzm_format "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:
// PRODUCER (after compress()): grab the metadata header, store it with the blob.
std::vector<uint8_t> header = producer.serializeHeaderToMemory(); // ~1 KB, no payload
// CONSUMER: a fresh, finalized pipeline of the SAME topology, never compress()ed.
Pipeline slot(block_bytes);
/* addStage/connect identically */ slot.finalize();
// One call per blob: restores this blob's metadata, decodes it, and reuses the
// slot's cached inverse DAG across calls (no per-blob DAG rebuild).
slot.decompressFromMemory(header.data(), header.size(),
d_blob, blob_size, &d_output, &output_sz, stream);serializeHeaderToMemory()requires a priorcompress(); 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()fusesprimeInverseFromHeader()+decompress(); output ownership followssetPoolManagedDecompOutput()exactly asdecompress()does.- The two steps are also available separately (
primeInverseFromHeader(header...)thendecompress(blob...)). - The lighter
prepareInverse(uncompressed_size)needs no header only when every stage can decode from its configured parameters and the raw blob alone. It records source sizes and output-buffer metadata, but does not restore stage configuration or inverse element counts. Lossless coders such as AdaptiveBitpack and Huffman still requireprimeInverseFromHeader(); attaching a linear-mode quantizer does not remove that requirement. - Worked example:
examples/decode_only_slots.cpp.
Records the compression pass as a replayable graph, eliminating CPU kernel-launch overhead on repeated calls. See [Performance Tuning](\ref performance_tuning) for when this is worth it.
fz::Pipeline pipeline(input_bytes, fz::MemoryStrategy::PREALLOCATE);
// ... addStage, connect ...
pipeline.enableGraphMode(true);
pipeline.finalize();
pipeline.warmup(stream); // JIT-compile kernels
pipeline.captureGraph(stream); // record once
pipeline.compress(d_input, input_bytes, &d_compressed, &compressed_sz, stream); // replaysRequirements: 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 |
- Lorenzo downstream port — connect to
"codes", not the default"output". - setDims() before addStage() — dimensions are pushed into the stage at add-time.
- Pool-owned pointers — never
cudaFreecompress output or default decompress output. - finalize() divides the world — no configuration changes after
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 non-experimental 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 |
Experimental modules and their headers are preserved on development; they
are unavailable in the stable-main package. Only the shipped stage catalog falls
within this source-API policy.
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:
- Removing or renaming a public class, method, enum value, or field
- Changing ownership or lifetime rules for a returned pointer
- Changing a public function's signature
- Adding, removing, or retyping a
Stagevirtual method in a way that breaks custom stages - Breaking FZM file compatibility beyond version-negotiation rules
Minor 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:
- Does this change a public header in
include/? - Does it alter pointer ownership, lifetime, or free semantics?
- Does it affect
Stagevirtual method signatures or behavioral contracts? - Does it change FZM file format compatibility or version-negotiation behavior?
- Are docs and tests updated to match the new behavior?
If any answer is "yes" and the change is not backward-compatible, schedule it as a major-version bump.