Skip to content

Add 12 CPU modules + tests; fix 20 pre-existing bugs - #142

Open
ayzk wants to merge 31 commits into
fzfrom
fz-dev
Open

Add 12 CPU modules + tests; fix 20 pre-existing bugs#142
ayzk wants to merge 31 commits into
fzfrom
fz-dev

Conversation

@ayzk

@ayzk ayzk commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

New modules

Group Module
Decomposition PaSTRIDecomposition (pattern-based prediction, ERI), MultiLevelDecomposition (+ MGARDDecomposition alias), SPERRDecomposition (composable)
Preprocessor MGARDTransform, SPERRTransform
Quantizer ClusterQuantizer, GranularBitRoundQuantizer (Bit Grooming), LevelQuantizer, LogDomainQuantizer, OutlierQuantizer
Utils MultiLevelErrorBound, MultiLevelQuantization

All inherit their group's interface. No interface class definition or Config was changed.

MGARD / SPERR naming

The fused implementations are performance-validated and keep driving ALGO_MGARD / ALGO_SPERR, renamed to MGARDFusedDecomposition / SPERRFusedDecomposition. The composable compositions take the default names.

Fused vs composable on miranda pressure (144 MB, LinearQuantizer both sides):

relEB MGARD fused CR MGARD composable CR SPERR fused CR SPERR composable CR
1e-2 119.71 119.70 776.59 776.50
1e-3 32.29 32.30 211.48 210.83
1e-4 15.38 15.38 71.60 71.27

Max absolute error identical in every case. SPERR composable is ~19% faster both ways; MGARD composable is ~6% slower to decompress.

Bug fixes

All pre-existing, found mostly by the new composition tests and by running the suite under AddressSanitizer, GCC/libstdc++, and a memory-capped Linux container.

Correctness

Module Bug
ArithmeticEncoder signed >> 20 sign-extends, corrupting ~half of streams
BitshuffleEncoder only ORs bits into a malloc'd buffer
LevelQuantizer quadratic curve silently exceeded the error bound
BitTruncationQuantizer negative bins for double
TimeSeriesDecomposition null-reference-frame path violated the bound 1.94x
MGARDFusedDecomposition violated the error bound unconditionally, up to 6.25x eb; now bounded by an OutlierQuantizer pass
quantizers uid collisions (now covered by a uniqueness test)

Memory safety

Module Bug
Lossless_bypass::compress unconditional heap overflow when the destination is smaller than the source (ASan; aborts only under glibc)
RunlengthEncoder and 6 other modules no size_est() → heap overrun
HuffmanEncoder stateNum narrowing overflow
HuffmanEncoderV2 unbounded maxval allocation from a corrupted stream
RegressionPredictor::load, ComposedPredictor::load opposite-direction remaining_length accounting
MemoryUtil::read bounds check was an assert, compiled out of Release
KmeansUtil reserve + operator[] UB, off-by-one OOB read
quantizers recover_unpred() had no bounds check
H5Z filter compressed buffer sized from the raw chunk size, too small when a block expands

Build and test infrastructure

  • 24 headers were not self-contained under libstdc++ (they compiled only under libc++)
  • .gitignore test -> /test — it was hiding every test file in the tree
  • The HDF5 filter test's small-chunk mode asked for 8-element chunks over the whole field. On a 1D field of 280M elements that is 35M chunks, and the HDF5 chunk index alone took the process to 15.3 GiB of a runner's 16 GiB. Restricted to a leading slice of at most 4096 chunks: peak 9.6 GiB, coverage intact (a pre-fix filter still fails it on every algorithm).
  • cdvalueHelper's name-to-enum map stopped at ALGO_BIOMDXTC, so ALGO_SVD, ALGO_ZFP, ALGO_SPERR and ALGO_MGARD could not be requested through the HDF5 filter at all
  • Integration output was captured rather than streamed, so a killed job discarded every test's output; that is what made three earlier failures unreadable
  • integration_test.yml now also runs on PRs to fz, and one run per branch/PR at a time

Other

  • NoPredictionDecomposition / BlockwiseDecomposition no longer hardcode To = int; deduced via quantizer_bin_t
  • get_out_range() returns (0, 0) where there is no usable bin range; SZGenericCompressor rejects a range that cannot fit in preprocess_encode's int
  • block_data::values() gives the block's values in unpadded layout
  • sz_dev.hpp was missing 8 modules
  • Module acceptance gate: SZ3_ModuleContract (group contracts, installed as include/SZ3/testing/ModuleContract.hpp), SZ3_ModulesJson, and tools/test/check_headers.py
  • tools/bench/module_bench (BUILD_SZ3_BENCH=ON) measures a composition's CR, error and throughput on a dataset
  • ZFP 24 warnings and SVD 2 warnings fixed

Verification

  • clang/libc++ and GCC/libstdc++, Release + BUILD_TESTING=ON: 0 errors, 0 warnings, 186/186 tests
  • 85/85 headers self-contained under both standard libraries
  • 8 algorithms byte-identical before/after every change in this PR
  • The HDF5 filter round-trips all 11 algorithms in all three chunk modes on the in-tree smoke file;
    ALGO_ZFP, ALGO_MGARD and ALGO_SPERR also on a 256x384x384 field
  • The small-chunk mode measured in a 16 GiB container at hacc's exact size (280,953,867 elements):
    15.30 GiB and OOM-killed before, 9.64 GiB and passing after

🤖 Generated with Claude Code

ayzk and others added 2 commits August 30, 2026 20:28
* Drop 31 no-op ALWAYS_INLINE keywords

Benchmarking on Anvil (EPYC 7763; GCC 10.2/11.2/14.2, AOCC clang 12) and
locally (GCC 16.2, AppleClang) shows the attribute changes nothing at 31 of
the 49 sites: the compilers already inline these, and removing the keyword
either yields a byte-identical binary or measures zero.

What the data says to keep, and why this is not a bulk removal:

  LinearQuantizer (5) + BlockwiseIterator (9) carry the entire effect.
  Downgrading just those two groups costs ~7.5%; downgrading everything
  else together costs ~0.2%. No single group crosses GCC's inlining
  threshold on its own, so per-group numbers hide this -- it only shows up
  when the two are isolated. They keep the attribute.

  Config.hpp to_lower keeps its inline: it is the only namespace-scope
  non-template among the sites, and without it the library stops linking.

  interp_akima / interp_pchip keep theirs: both are multi-line with
  branches, both currently have no callers, so the byte-identical result
  for them is vacuous and says nothing about future use.

  QuantOptimization.hpp lorenzo_predict_3d keeps its attribute; the
  function is dead code today but is expected to come back.

Deleted rather than downgraded to plain inline: 27 of the 31 are in-class
definitions (implicitly inline) and 4 are namespace-scope templates (already
weak/COMDAT), so no keyword is needed to preserve linkage.

Included here are the two on ComposedPredictor::predict/estimate_error,
which is where GCC 16 actually hard-errors: the class is itself a
PredictorInterface and may nest, so speculative devirtualization recurses
into itself until the inline budget is exhausted -- and a failed
always_inline is an error, not a warning. That is what broke the MinGW CI
job on the fz branch.

Verified: builds clean under GCC 16.2 and AppleClang; ALGO_LORENZO_REG,
ALGO_INTERP_LORENZO, ALGO_INTERP and ALGO_NOPRED all round-trip within the
error bound on the in-tree smoke file and on miranda 384x384x256.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix SDRBench dataset URLs: Globus endpoint moved

All eight integration-test datasets return 404. SDRBench moved to a new
Globus endpoint, changing both the host and the path prefix:

  g-8d6b0.fd635.8443.data.globus.org/ds131.2/Data-Reduction-Repo/raw-data/
  g-d0cd3f.fd635.8443.data.globus.org/raw-data/

exaalt-copper and exaalt-helium were also renamed upstream, from
SDRBENCH-exaalt-* to SDRBENCH-EXAALT-*, so the host swap alone still left
those two at 404.

New URLs taken from https://sdrbench.github.io/datasets.html and confirmed
reachable (HTTP 206 on a range request) for all eight. The integration test
matrix covers exactly these datasets, so CI on this PR is the real check
that each archive still unpacks to the filenames datasets.json expects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
MGARD/SPERR fused paths kept as *FusedDecomposition and still drive
ALGO_MGARD / ALGO_SPERR; the composable compositions take the default names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 31, 2026 20:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands SZ3’s modular “fz” toolkit by adding several new CPU-side modules (quantizers, decompositions, preprocessors, and utilities), plus substantial new unit tests, and it fixes a set of pre-existing correctness issues in encoders/quantizers/decompositions without changing interface base classes or Config.

Changes:

  • Add new composable modules (e.g., MultiLevelDecomposition/MGARD alias, PaSTRIDecomposition, new quantizers including LevelQuantizer, LogDomainQuantizer, etc.) and supporting utilities (MultiLevelErrorBound, MultiLevelQuantization).
  • Add/expand unit tests for the new quantizers/decompositions and for serialization/UID correctness.
  • Fix multiple pre-existing bugs across encoders/quantizers/decompositions and adjust wiring/docs/CI accordingly.

Reviewed changes

Copilot reviewed 51 out of 52 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/test/modules/test_quantizer.cpp Expands quantizer coverage; adds UID uniqueness test; updates BitTruncation bins to uint64_t.
tools/test/modules/test_quantizer_log.cpp New tests for LogDomainQuantizer (relative-bound behavior, serialization, edge cases).
tools/test/modules/test_quantizer_level.cpp New, thorough tests for merged LevelQuantizer curves and bound guarantees.
tools/test/modules/test_pastri.cpp New unit tests for PaSTRIDecomposition including error-bound and save/load roundtrips.
tools/test/modules/test_decomposition.cpp Updates decomposition tests to target fused MGARD/SPERR implementations.
include/SZ3/utils/MultiLevelQuantization.hpp Adds helpers to walk multi-resolution coefficient slabs and drive per-level quantizers.
include/SZ3/utils/MultiLevelErrorBound.hpp Adds geometric per-level absolute error-bound scheduling utility.
include/SZ3/utils/KmeansUtil.hpp Fixes UB sampling bug (reserve/[] and inclusive bound), but still needs a size_t distribution fix.
include/SZ3/utils/BlockwiseIterator.hpp Adds block_data::values() helper for unpadded view of block data (currently rebuilds each call).
include/SZ3/quantizer/Quantizer.hpp Adds quantizer_bin_t helper to deduce quantizer output type for decompositions.
include/SZ3/quantizer/QuadraticLevelQuantizer.hpp Removes old quadratic-only LUT quantizer (superseded by LevelQuantizer).
include/SZ3/quantizer/OutlierQuantizer.hpp Adds sparse outlier-correction quantizer; get_out_range() likely needs to signal “no range”.
include/SZ3/quantizer/LogDomainQuantizer.hpp Adds log-domain quantizer guaranteeing pointwise relative error bounds with verbatim fallback.
include/SZ3/quantizer/LinearQuantizer.hpp Adds bounds check on recover_unpred() to prevent buffer overrun during decode.
include/SZ3/quantizer/LevelQuantizer.hpp Adds merged non-uniform LUT quantizer with quadratic/log curves and unconditional bound via unpredictable path.
include/SZ3/quantizer/GranularBitRoundQuantizer.hpp Adds “bit grooming” style quantizer that rounds mantissa bits to preserve significant digits.
include/SZ3/quantizer/ClusterQuantizer.hpp Adds cluster/codebook quantizer and a helper to derive lattice levels via k-means.
include/SZ3/quantizer/BitTruncationQuantizer.hpp Switches bin type to uint64_t to avoid negative bins and align with (0,0) “no range” signaling.
include/SZ3/preprocessor/SPERRTransform.hpp Adds standalone SPERR conditioning + wavelet preprocessor (composable transform half).
include/SZ3/preprocessor/PreFilter.hpp Fixes pointer iteration by using dims to compute element count.
include/SZ3/preprocessor/MGARDTransform.hpp Adds standalone MGARD multigrid transform preprocessor with level geometry utilities.
include/SZ3/encoder/ZFPEncoder.hpp Fixes includes and loop types for warnings/correctness.
include/SZ3/encoder/RunlengthEncoder.hpp Adds size_est() based on bin count to prevent buffer overruns.
include/SZ3/encoder/HuffmanEncoder.hpp Adds guard against excessively wide bin ranges that would overflow/over-allocate state tables.
include/SZ3/encoder/BitshuffleEncoder.hpp Fixes uninitialized output buffer (OR-only loops) and narrows bit ops to correct unsigned types.
include/SZ3/encoder/ArithmeticEncoder.hpp Fixes sign-extension bug by shifting an unsigned value.
include/SZ3/decomposition/ZFPDecomposition.hpp Fixes loop/index types and std::min typing to remove warnings/bugs.
include/SZ3/decomposition/TimeSeriesDecomposition.hpp Fixes timestep-0 reconstruction usage for prediction to preserve error-bound correctness.
include/SZ3/decomposition/SVDDecomposition.hpp Fixes warnings and simplifies shuffle permutation handling.
include/SZ3/decomposition/SPERRFusedDecomposition.hpp Adds renamed fused SPERR decomposition (transform+quant+SPECK stream in state).
include/SZ3/decomposition/PaSTRIDecomposition.hpp Adds PaSTRI ERI decomposition with serialization and unconditional bound via verbatim fallback.
include/SZ3/decomposition/NoPredictionDecomposition.hpp Generalizes bin type using quantizer_bin_t instead of hard-coding int.
include/SZ3/decomposition/MultiLevelDecomposition.hpp Adds composable multilevel transform+schedule+per-level-quantizer decomposition (MGARD alias).
include/SZ3/decomposition/MGARDFusedDecomposition.hpp Renames and clarifies the fused MGARD decomposition implementation.
include/SZ3/decomposition/BlockwiseDecomposition.hpp Generalizes bin type using quantizer_bin_t instead of hard-coding int.
include/SZ3/compressor/ZFPCompressor.hpp Fixes loop types for correctness/warnings.
include/SZ3/compressor/SZGenericCompressor.hpp Enforces encoder stateNum fits int; instructs decompositions to return (0,0) if no usable range.
include/SZ3/api/sz_dev.hpp Exposes new modules via sz_dev.hpp and updates includes/formatting.
include/SZ3/api/impl/SZAlgoSPERR.hpp Switches algorithm wiring to SPERRFusedDecomposition.
include/SZ3/api/impl/SZAlgoMGARD.hpp Switches algorithm wiring to MGARDFusedDecomposition and updates doc comment.
docs/claude-skills/fz-overview/SKILL.md Updates module listings/names to match new decompositions/quantizers.
docs/claude-skills/fz-compose-pipeline/SKILL.md Updates example wiring names for MGARD/SPERR fused variants.
docs/claude-skills/fz-add-module/SKILL.md Updates naming examples and UID list to reflect new/renamed quantizers/decompositions.
CLAUDE.md Updates module lists and MGARD pipeline description to match current implementation.
.gitignore Stops ignoring the entire test directory by changing test to /test.
.github/workflows/integration_test.yml Runs integration workflow on PRs targeting fz branch as well as master.
Suppressed comments (1)

include/SZ3/utils/KmeansUtil.hpp:314

  • get_cluster() builds std::uniform_int_distribution<> (i.e., int) and casts num (a size_t) down to int. For num > INT_MAX this truncates/overflows and can yield out-of-bounds indices; for num == 0 it also produces an invalid upper bound. Use a size_t distribution over [0, num-1].

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +225 to +227
std::pair<To, To> get_out_range() const override {
return std::make_pair(std::numeric_limits<To>::lowest(), std::numeric_limits<To>::max());
}
Comment on lines +239 to +246
const T *values() {
if (padding == 0 || internal_buffer.empty()) {
return data_padding;
}
unpadded_buffer.resize(num);
copy_data_with_padding(unpadded_buffer.data(), ds, data_padding, ds_padding, dims);
return unpadded_buffer.data();
}
Comment on lines +130 to +135
quantizers_.clear();
quantizers_.reserve(level_ebs.size());
for (double level_eb : level_ebs) {
quantizers_.push_back(make_quantizer_(level_eb));
}

ayzk and others added 25 commits August 31, 2026 13:25
libstdc++ does not provide std::make_shared transitively; the new test
translation units changed the include order enough to expose it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Group-level contract checks (ModuleContract.hpp) and a header self-containment
check, both runnable and wired into CI; docs/MODULE_ACCEPTANCE.md records what a
module must satisfy to enter the library. Each check comes from a defect class
that has shipped here.

Fixes found by the two checks: 9 headers were not self-contained, and
MemoryUtil::read guarded remaining_length with assert, which Release compiles
out, so a truncated stream read past the buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check only ran under libc++ locally, which provides more transitively; 17
headers were missing <limits>, <cmath>, <cstddef>, def.hpp or a project header.
CI now runs the check on the macOS job as well, so a clean run under one
standard library no longer passes for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RegressionPredictor::load debited remaining_length by the decoded bin count
(coeff_size * sizeof(int)) while decode() advances the cursor by the much
smaller encoded byte count, so the budget ran out early. Harmless while the
guard in MemoryUtil::read was an assert compiled out of Release builds; now
that it throws, the HDF5 filter path hit it on miranda velocity fields at
eb=1e-4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sizeof(T) * num * 2 is below SZ_compress's own minimum for a small chunk, so an
explicit chunk of a few hundred elements aborted with "The buffer for
compressed data is not large enough". The integration test now covers an
explicit small chunk alongside the full and auto-chunked cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six decompositions did not override size_est(), so SZGenericCompressor sized
its buffer from 0 while the quantizer's unpredictable list could be arbitrarily
large; three are fixed here and the rest already had one.

The contract also records MGARD's bound limitation: it quantizes coefficients
per level and never checks the reconstruction, so the transform's own round-off
passes through and the absolute bound holds only while max|x| * epsilon<T> << eb.

Also restores the H5Z filter's buffer headroom on top of SZ_compress's minimum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compress() memcpy'd srcLen bytes into dst regardless of dstCap, so any payload
larger than the caller's buffer was an unconditional heap overflow.
Lossless_zstd already checks and throws; bypass now does the same.

Caught by AddressSanitizer on Linux: ALGO_BIOMDXTC pairs its codec with
Lossless_bypass, so nothing shrinks the payload, and the HDF5 filter's buffer
was sized from SZ_compress's minimum, which assumes it does. glibc reported it
as "munmap_chunk(): invalid pointer"; macOS's allocator did not abort, which is
why it only showed up in CI. The filter's headroom is restored here too - it
was lost restoring a backup during an earlier A/B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
decode() advances the cursor but leaves remaining_length untouched, so the
bound check was too loose for everything read after it. Same defect class as
RegressionPredictor::load, in the opposite direction.

MemoryUtil::read now compares by division so the size computation cannot
overflow on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contracts now cover the Lossless group as well; its destination-capacity check
is what Lossless_bypass was missing. Adds tests for SZGenericCompressor's two
guards on get_out_range, which had none.

GranularBitRoundQuantizer's tests move out of test_quantizer_cluster.cpp into
their own file.

tools/bench/module_bench.cpp (BUILD_SZ3_BENCH=ON) measures a composition's
compression ratio, achieved error and throughput on a real dataset and prints
CSV. Module characteristics can only be measured, not authored, so this is the
piece the composition metadata will need.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
modules.json was missing 8 modules; CLAUDE.md's quantizer list predated five of
them and had no preprocessor list at all. Both now also point at the acceptance
gate and the benchmark tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ModuleContract.hpp moves to include/SZ3/testing/ so it ships with the library:
the gate is meant to apply to modules submitted from outside this tree, which
cannot reach a header under tools/test/.

modules.json gains a `composition` block on the 13 modules that constrain what
they can be composed with -- an encoder that cannot derive its own bin range, a
decomposition that only works with one encoder, bins that are not a countable
domain, the regime an error bound holds in, a measured bad pairing. These are
the facts a composition engine needs before any measured characteristic, and
each one is already enforced by a test or by the code. SZ3_ModulesJson checks
the file against the tree so the two cannot drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MGARD quantized coefficients per level and never checked the reconstruction, so
the synthesis round-off -- which scales with the field's magnitude -- passed
through. For float at eb = 1e-2 a magnitude of 1e6 reached 1.17x eb and 1e7
reached 6.25x; every other decomposition checks the value it writes back and
diverts to an unpredictable list.

Both MGARD compositions now replay the decoder's inverse transform and record
what still misses into an OutlierQuantizer, carried in save() the way
LinearQuantizer carries its unpredictable list.

Costs an inverse transform and a copy of the input on the compress side:
miranda velocityx 301 MB, 500 -> 293 MB/s compressing, decompression and
compression ratio unchanged (201.35 vs 201.36 at relative eb 1e-2).

Changes the ALGO_MGARD stream only; the other seven algorithms are byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bound now holds unconditionally, but not for free everywhere: outliers cost
nothing where the transform already met the bound, trade compression for the
bound where it did not, and degenerate to near-lossless on a field that leaves
the per-level quantizer saturated. The measured numbers are in the header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every push added a full matrix without retiring the one in flight, so the long
integration jobs of older runs were evicted mid-test instead of finishing --
hacc and exaalt-helium died at 13-17 and 25-30 minutes across three runs while
the same jobs completed in 2h10m and 46m on a branch that was not being pushed
to repeatedly.

master and fz still keep every commit's build result; only feature branches and
superseded pull-request runs are cancelled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ings

Bug fixes only. Nothing here changes the compressed format or an interface
signature, and every algorithm's output is byte-identical to master's (verified
over 31 dataset/algorithm/error-bound combinations, plus decompressing files
master produced).

From the open pull requests, reviewed line by line:

  #131  Config::load read one byte past the end of the config blob
  #132  bounds-check the whole decompression path (see the two skips below)
  #133  shift-by-64 UB for a single-symbol Huffman tree
  #134  bounds-check HuffmanEncoderV2 tree loading
  #135  bounds-check XtcBasedEncoder's magicInts index; plug a leaked buffer
  #137  remaining_length accounting after Huffman decode in both predictors
  #138  non-finite float to int64_t cast UB in LinearQuantizer
  #139  scratch buffers leaked when compression throws; OMP chunk capacity was
        missing room for the size header Lossless_zstd writes

Two parts of #132 are deliberately not taken:

  - It moves the quant_inds count from after the encoder's tree to before it so
    the tree is immediately followed by its encoded stream. That is a compressed
    format change and it is not versioned, so files written by any released SZ3
    fail to decode. The bound it was buying is recovered instead by letting the
    caller supply it: HuffmanEncoder::set_decode_bound(), called by
    SZGenericCompressor once it has consumed the count, and by both predictors.
    load() no longer guesses the bound from its own buffer -- the tree and the
    stream are not required to share a buffer, and test_encoder.cpp puts them in
    separate ones.
  - It reinterprets LosslessInterface::decompress's `dstLen` as an input
    capacity when the caller provides the buffer. The declared contract is that
    it is an output, and callers do pass uninitialised values, so this fails
    nondeterministically (it broke LosslessTest.LosslessBypass here). The
    self-allocating branch, where a non-zero value is opt-in, is kept.

Also fixed while reviewing:

  - HuffmanEncoderV2 sized its dense tables from an unbounded `maxval` read
    straight from the stream; #134 bounds the node count but not this.
  - HuffmanEncoderV2's node count is an int holding a value read as 64-bit, so
    the new bound is checked with an explicit sign test rather than an implicit
    conversion.
  - test_lossless.cpp passed an uninitialised size to decompress().

From the fz branch, verified to reproduce here:

  Lossless_bypass::compress ignored its destination capacity, so any payload
  larger than the caller's buffer was an unconditional heap overflow;
  TimeSeriesDecomposition violated its error bound 1.94x on the
  null-reference-frame path; ArithmeticEncoder sign-extended a shift, corrupting
  about half of all streams; HuffmanEncoder's stateNum narrowing overflowed on a
  wide bin range; RunlengthEncoder and two decompositions had no size_est(), so
  the compressor sized its buffer from 0; KmeansUtil had reserve+operator[] UB
  and an off-by-one read; the HDF5 filter sized its buffer below SZ_compress's
  own minimum; 24 headers were not self-contained under libstdc++ or libc++;
  .gitignore's bare `test` pattern hid every test file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Taking the fz-branch fixes file by file overwrote them. The magicInts index
and the packed-data size both come from the compressed stream and were used
unchecked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
num_elements * sizeof(T1) can overflow on its own, which is the computation the
check is supposed to guard. Matches the scalar overload and PR #132.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Takes the eight reviewed bug-fix pull requests. Where the same defect was fixed
on both branches, the master-fixes wording wins (its messages name the buffer
and use out_of_range); where fz has the module the fix belongs to, fz wins.

git merged both branches' identical additions twice in BlockwiseIterator
(values()) and RunlengthEncoder (size_est), which does not compile; the
duplicates are removed.

PR #132's bound on the internal decompression buffer is dropped here, and needs
dropping on master too: it bounds that buffer by SZ_compress_size_bound, which
is the size of the *output* buffer. compress() sizes the internal one as
max(1000, 2 * (decomposition.size_est() + encoder.size_est() + sizeof(Q) *
bins)), so for a 64-bit bin type it is far larger and valid streams are
rejected -- BitplaneEncoder, BitTruncationQuantizer and FixedPointQuantizer all
hit it. master's in-tree modules all emit int bins, so its own tests cannot
reach this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It bounds that buffer by SZ_compress_size_bound, which is the size of the
*output* buffer. compress() sizes the internal one as max(1000, 2 *
(decomposition.size_est() + encoder.size_est() + sizeof(Q) * bins)), so for a
64-bit bin type it is far larger and valid streams are rejected. Every module in
this tree emits int bins, so the bound happens to hold and these tests cannot
reach it -- the fz branch has three modules that do (BitplaneEncoder,
BitTruncationQuantizer, FixedPointQuantizer) and all three failed.

A corrupted declared size is still caught, after the allocation, by the zstd
frame check and the size comparison this PR adds to Lossless_zstd::decompress.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #132 rejects N outside [1, 4] and dimensions whose product differs from the
element count. Both hold for a config read from the end of a compressed stream,
and neither holds for the HDF5 filter's cd_values: those carry placeholder zeros
that set_local fills in later, as cdvalueHelper.py says in as many words. The
check killed the whole filter -- 80 of cesm-atm's 160 integration cases aborted
with "invalid number of dimensions", across every algorithm.

The single-argument overload, which is what the filter and the OpenMP path use,
now says so explicitly and skips the content checks; the bounded overload used
by SZ_decompress keeps them.

Verified against the real cesm-atm field through the HDF5 filter: five
algorithms pass, and removing the guard reproduces the abort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check PR #132 adds for N outside [1, 4] and for dimensions whose product
differs from the element count holds for a config read from the end of a
compressed stream, and not for the HDF5 filter's cd_values: those carry
placeholder zeros that set_local fills in later, as cdvalueHelper.py says in as
many words. It killed the whole filter -- 80 of cesm-atm's 160 and 96 of
exaalt-copper's 192 integration cases aborted with "invalid number of
dimensions", across every algorithm.

Verified against the real cesm-atm field through the HDF5 filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A bare "-j" means unlimited parallel compilation. This tree now has 56
translation units against master's 9, and they pull in MGARD, SPERR and Eigen,
so the peak at link time is enough to get a 15 GB runner terminated: hacc died
three times at exit 143 immediately after "100% Linking CXX executable
sz3ToHDF5", with no error of its own, 86 GB of disk still free and every other
dataset passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Python block-buffers stdout into the log pipe, so everything the driver prints --
including the captured output of every test it runs -- is lost when the job is
terminated. The log then stops at the last write from a subprocess, which is the
build, and says nothing about where the run actually died. hacc has failed four
times with exit 143 and a log that ends at the same line; that line is where the
visible output stops, not where the failure is, and two hypotheses I drew from it
were wrong for exactly that reason.

Runs the driver with -u, and flushes around dataset preparation so the phase is
identifiable even without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
capture_output=True prints the child's output only after it exits, so a case that
never returns leaves no trace of how far it got. hacc has now failed five times;
the log ends at the "command:" line for its first case -- a single 1.12 GB field
written as one chunk -- and everything that case printed is still invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mode asked for 8-element chunks over the whole field. On a 1D field of 280
million elements that is 35 million chunks, and the HDF5 chunk index alone took
the process to 15.3 GiB of a CI runner's 16 GiB -- hacc and exaalt-helium were
killed there. Restricting the mode to a leading slice of at most 4096 chunks
brings the peak to 9.6 GiB and leaves the mode's coverage intact: an unfixed
filter still fails it on every algorithm at ordinary dataset sizes.

Also stop tracking a stray __pycache__ entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ayzk and others added 2 commits September 1, 2026 06:57
The helper's name-to-enum map stopped at ALGO_BIOMDXTC, so ALGO_SVD, ALGO_ZFP,
ALGO_SPERR and ALGO_MGARD could not be requested through the HDF5 filter at all.
All four round-trip correctly through the filter, on the in-tree smoke file and
on a 256x384x384 field, in every chunk mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The filter has to size its output buffer from SZ_compress_size_bound rather than
from the raw chunk size, and nothing in the suite exercised that: a chunk small
enough for the compressed block to exceed it never appeared. This adds a third
chunk mode that asks for 8-element chunks, which the pre-fix filter rejects with
"buffer not large enough" on every algorithm.

The mode is restricted to a leading slice of at most 4096 chunks. Over a whole
field it would mean tens of millions of chunks, and the HDF5 chunk index alone
takes the process past a CI runner's memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ayzk ayzk changed the title Add 12 CPU modules + tests; fix 10 pre-existing bugs Add 12 CPU modules + tests; fix 20 pre-existing bugs Sep 1, 2026
ayzk and others added 2 commits September 1, 2026 09:16
Bug fixes only: no compressed-format change and no interface signature change.

Covers the reviewed content of #131, #133, #134, #135, #137, #138 and #139, plus the
findings ported from the fz branch. #132 is only partly covered -- see the PR body for
the four exclusions and the measurement behind each.

31 dataset x algorithm x error-bound combinations are byte-identical to master, and every
master-produced file still decompresses to the same bytes.
master now carries the consolidated bug fixes (#144). Three conflicts, all in
commentary: fz-dev had a stale one-line comment above Config::load's fuller doc
block (dropped), one condition differed only in line wrapping (took master's
single line, which fits the 120-column limit), and fz-dev's note on the dropped
decompression-buffer bound names the modules that actually emit 64-bit bins, so
it is the accurate wording here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants