FZGPUModules 2.0
GPU-accelerated modular compression pipelines
Loading...
Searching...
No Matches
outlier_correct_stage.h File Reference

OutlierCorrectStage<Reconstructor> — transform-agnostic sparse outlier correction that turns a coefficient-domain quantization bound into an actually GUARANTEED reconstructed-domain pointwise error bound, for any reversible transform. More...

#include "stage/stage.h"
#include "fzm_format.h"
#include "backend/types.h"
#include <array>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

Go to the source code of this file.

Classes

struct  fz::OutlierCorrectConfig
 Serialized OutlierCorrectStage config. 12 bytes. More...
 

Namespaces

namespace  fz
 

Detailed Description

OutlierCorrectStage<Reconstructor> — transform-agnostic sparse outlier correction that turns a coefficient-domain quantization bound into an actually GUARANTEED reconstructed-domain pointwise error bound, for any reversible transform.

The problem this fixes

Transform -> QuantizerStage(linear/ABS) -> Coder quantizes transform COEFFICIENTS directly. For transforms whose synthesis gain varies (CDF 9/7 is the motivating case: gain differs by decomposition level), a uniform coefficient-domain threshold does NOT translate to a uniform bound on the RECONSTRUCTED FIELD's pointwise error – measured misses up to 2.7x the requested bound on real CDF 9/7 data (see memory/speck_gpu_design.md sec.9). A candidate fix – scale each coefficient's quantization step by its level's synthesis-filter gain – was tried and REJECTED for CDF 9/7: it makes the max error WORSE, because many coefficients across levels jointly influence any given pixel, so bounding each one's isolated worst case does not bound their sum. That reasoning is transform-shape-dependent in general, so this stage does not assume a scaling fix exists for whatever transform it's paired with either.

The fix that actually works, matching what native SPERR's own Outlier_Coder does for CDF 9/7 (and generalizes to any reversible transform): quantize normally; separately compute what the reconstruction WOULD be (dequantize + inverse-transform a copy); every pixel whose error exceeds the bound gets an EXACT correction value in a sparse (index, value) list, applied as the final step of decompress. This gives a mathematically exact guarantee, not a calibrated approximation.

Genericity: the <tt>Reconstructor</tt> policy

Everything in this class – diffing, sparse pack/apply, config, serialization, port shape – is transform-agnostic. The ONLY transform-specific step is "given dequantized coefficients, produce the trial reconstruction" – that's Reconstructor::applyInverseTransform(). A Reconstructor must provide:

struct MyReconstructor {
static constexpr StageType kStageType = StageType::...;
static std::string name() { return "..."; }
// In-place: d_coeffs_inout holds dequantized coefficients on entry,
// the trial reconstruction on return. n = nx*ny*max(nz,1) elements.
// Defined in a .cu file (may call CUDA device kernels) -- keep it out
// of this header, which stays host-safe and is included broadly
// (fzgpumodules.h, .cpp translation units).
static void applyInverseTransform(float* d_coeffs_inout, int nx, int ny, int nz,
cudaStream_t stream);
};

Cdf97Reconstructor (modules/coders/cdf97_outlier_correct/) is the one instantiation that ships today; adding another reversible transform's bound-guarantee pipeline is writing one small policy struct like it, not a new stage. This is the split found while pushing back on TeeStage + Cdf97OutlierCorrectStage as too SPERR-specific to justify as Pipeline primitives (see memory/speck_gpu_design.md sec.9's design-flaw discussion) – reusability was the actual bar, not a naming change.

Port shape and why it needs the raw field bound directly (not via Tee)

This stage needs BOTH the original raw field (to compute corrections against, at compress time) AND the dequantized codes (to reconstruct a trial value from, in both directions). The raw field is bound directly to input port 0 via Pipeline::bindExternalInput() – no duplicate-copy node needed; see Pipeline::bindExternalInput()'s doc comment for why a dedicated fan-out stage (TeeStage, now removed) turned out to be unnecessary scaffolding once buildInverseDAG()'s actual mirroring rules were traced precisely. Forward inputs: [raw_field, codes]. Forward outputs: [correction_stream (archived leaf), codes_passthrough -> coder]. Per the DAG's bijective inverse contract (inverse output k reconstructs forward input k), inverse outputs are: [corrected field, codes passthrough]; inverse inputs are: [archived correction stream, coder's decoded codes].

The inverse-transform that recovers the trial/candidate reconstruction runs identically in both directions (compress-time to detect outliers, decompress-time to reconstruct before applying corrections) – it is NOT a forward/inverse pair in the Stage sense, which is why it's called directly here via the Reconstructor policy rather than delegated to another DAG-wired stage's own inverse: buildInverseDAG()'s mirroring assumes a stage's inverse output reconstructs its OWN forward input, and "run the same forward-direction computation again in both pipeline phases" doesn't fit that shape for any stage, transform-specific or not.

Scope

float coefficients only. ABS-mode linear quantization only (the only mode any current pipeline pairs this with); error_bound here MUST equal the paired QuantizerStage's own error_bound – set both from the same value when building the pipeline.

File layout (mirrors Cdf97Stage's own .h/.cu split)

This header declares the class only – no CUDA device code, safe to include from a plain .cpp translation unit (fzgpumodules.h does). Member definitions (execute(), the pimpl Impl) live in outlier_correct_stage_impl.cuh, included only from .cu files that explicit-instantiate a concrete Reconstructor (see cdf97_outlier_correct_stage.cu).