|
FZGPUModules 2.0
GPU-accelerated modular compression pipelines
|
Header: modules/structural/roibin_split/roibin_split_stage.h Class: fz::ROIBinSplitStage<TData> — TData is float or double Category: Structural
Common instantiation:
Splits a detector field into two independently compressible streams plus the metadata that ties them back together:
| direction | ports |
|---|---|
forward (1 → 3) | field → roi, bg, peaks |
inverse (3 → 1) | roi, bg, peaks → field |
roi** — the (2*hw+1)^2 box around every peak, concatenated in peak order, at full resolution and the field's element type.bg** — the background, box-averaged by bin_factor within each z-slice: ceil(nx/b) * ceil(ny/b) * nz values. b = 1 copies the field through.peaks** — the peak record table, 8 * npeaks bytes (UINT8).On the inverse, the background is un-binned first and the ROI boxes are then pasted over it, so the tight-bound values win wherever the two overlap.
Serial-crystallography frames are almost all background; the science lives in a few hundred Bragg peaks covering well under 1 % of the pixels. A single-bound compressor must apply the tight ROI bound to the entire frame, and pays for the other 99 % at that bound.
This stage turns that into a graph problem. Because it is 1 → 3, the two data streams become two DAG branches, and each branch can carry its own Quantizer at its own error bound and its own coder chain:
There is no way to say "compress this part tightly and that part loosely" to a monolithic compressor; expressing it is the point. The design follows ROIBIN-SZ (Underwood et al.).
Measured contributions of the branch-specific stages (3 EXAFEL frames, eb 10/100): TiledLorenzo on the background is worth +9.3 % (bin=1) / +13.6 % (bin=2); a 1-D predictor on the roi branch is worth +0.4 %, which is why the ROI branch has none. roi is a peak-major concatenation of boxes rather than an image, so a 2-D predictor has no meaningful stride over it — a 1-D one is valid but does not pay.
It is not derived from the data. It is the output of the experiment's own peak finder, which in a real light-source pipeline has already run upstream — the same assumption ROIBIN-SZ makes. At compress time setPeaksFile() reads it; the stage validates every record against the field bounds and throws if the file's nx/ny/nz disagree with the pipeline's dimensions, because a silent mismatch would relocate every ROI box and still produce output that round-trips.
The table is then re-emitted on the peaks port, so it lands inside the archive and is counted in the compressed size. The decompressor never needs the .roi file. At 8 B/peak this is ≈0.01 % of a frame.
Boxes belonging to nearby peaks overlap, and overlapping pixels are stored more than once. That is deliberate. It makes the output size exactly npeaks * box * sizeof(TData), known before the first kernel launch, so estimateOutputSizes() is exact and PREALLOCATE needs no slack. The alternative — a per-pixel mask plus a device-wide exclusive scan — costs a 4-byte offset per pixel (1.2 GB on a 130-frame 1480x1552 volume) to remove a redundancy that measures well under 1 % of the ROI stream. getRoiOverlapFraction() reports the measured duplicate fraction.
Duplicates are safe because scatter is idempotent: every copy of a source pixel takes the same value, passes through the same quantizer, and reconstructs to the same number, so write order cannot matter. Edge boxes are clamped, not truncated, which keeps the box size fixed; a clamped box reads and writes the same border pixel several times, which is idempotent for the same reason.
bin_factor = b > 1 replaces each b x b block with its mean. This is a resolution reduction, not an error bound. The background reconstruction error is the binning error plus the quantization error, and it is not bounded by the background branch's error bound.
bin_factor | background guarantee | how to report background fidelity |
|---|---|---|
1 | satisfies its stated bound pixel-wise | max abs error and PSNR; the bound gate is meaningful |
> 1 | no bound | PSNR only; never claim a satisfied error bound |
The stage emits a getRunNotes() warning whenever bin_factor > 1 so this cannot be lost between the run and the write-up. The ROI branch satisfies its bound in both cases — that is the invariant the science depends on.
| setter | TOML key | default | meaning |
|---|---|---|---|
setRoiHalfWidth(hw) | roi_half_width | 4 | box is (2*hw+1)^2 |
setBinFactor(b) | bin_factor | 1 | background binning; 1 disables |
setPeaksFile(path) | peaks_file | — | compress-side peak list |
| — | data_type | float32 | float32 or float64 |
Dimensions come from Pipeline::setDims(). On the decompress path they are recovered from the archive header, and later pipeline dim pushes are ignored — finalize() pushes {0,0,1} on that path and would otherwise erase them.
Downstream stages connect to the named ports:
If a downstream predictor is dimension-aware and
bin_factor > 1, give it an explicitdim_x/dim_y/dim_zoverride. The binned background is not the pipeline's input shape, andfinalize()re-pushes the global dims over anything set at construction — the predictor would then use the wrong row stride and still round-trip, silently costing ratio. See TiledLorenzoStage.
22 bytes. Peak values travel on the peaks port, not here — the 128-byte stage config slot cannot hold a few thousand records.