Skip to content

Reduce memory footprints for 1D compression, improve domain decomposition, and add data compatibility mode - #262

Merged
JieyangChen7 merged 11 commits into
CODARcode:masterfrom
JieyangChen7:mgard-x
Oct 14, 2026
Merged

JieyangChen7 merged 11 commits into
CODARcode:masterfrom
JieyangChen7:mgard-x

Conversation

@JieyangChen7

Copy link
Copy Markdown
Collaborator

No description provided.

JieyangChen7 and others added 11 commits October 1, 2026 14:15
MGARD_ENABLE_MDR used find_package(MPI REQUIRED), so a machine without MPI
could not build MDR-X at all, even though only the parallel pmdr-x-qoi and
pmdr-x-qoi-io executables use MPI. Look MPI up without REQUIRED and build
those two executables only when it is found; the MDR-X library, mdr-x and
mdr-x-qoi build either way. The configure summary reports the result as
"pmdr-x (MPI): ON/OFF" under MDR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
_level_volumes and _level_volumes_reciprocal were dense
(l_target + 1) x D x max_width arrays, but level l only fills
level_shape[l][d] entries per dimension. In 1D max_width is N and
l_target is ~log2(N), so the two arrays alone took ~50x the input size
(1600 MB each for a 2^24-element float array) and dominated the 1D
compression footprint (peak 120x the input).

Pack each (level, dim) segment back to back and add a (l_target + 1) x D
_level_volume_offsets table (Hierarchy::level_volume_offsets()); the
total is sum_l sum_d level_shape[l][d], ~2N in 1D. The levelwise
quantizer kernel reads level_volumes[offset(level, d) + idx[d]], which is
the same element as before: idx[d] < level_shape[level][d] for a node of
that level. EstimateMemoryFootprint uses the packed size.

1D float, SERIAL, peak device memory: 2^24 elements 7680 -> 1792 MB
(multi-dim) and 7680 -> 2121 MB (hybrid). Compressed output is
byte-identical to before across 1D-4D, float/double, s = inf/0/1,
rel/abs, multi-dim and hybrid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CompressorCache and RefactorCache inserted with
(*hierarchy_cache)[key] = hierarchy, a copy assignment that deep-copies
every device array (Hierarchy declared a copy constructor, so it had no
implicit move). For a moment two full hierarchies were live: 1792 MB
instead of 896 MB for a 2^24-element 1D float input.

Give Hierarchy defaulted move constructor/assignment (Array's move hands
over the buffers and leaves the source empty; moving into a populated
Array frees it first) and an explicitly defaulted copy assignment, which
declaring the moves would otherwise delete. Both caches now insert with
std::move(hierarchy); shape is read before the move.

mgard-x output is byte-identical across 1D-4D, float/double,
s = inf/0/1, rel/abs, multi-dim and hybrid; mdr-x refactored components
and reconstruction errors are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ition

The hybrid compressor used a full Hierarchy of the input grid, of which
the block-local stage reads only the shape, plus a second Hierarchy for
the global stage that HybridHierarchyDataRefactor::Adapt rebuilt on every
call (once per subdomain). In 1D the input-sized hierarchy alone was
~14x the input.

HybridHierarchy derives from Hierarchy. Its base part is shape-only (new
protected Hierarchy::init_shape_only: one level equal to the input shape,
no device arrays), so l_target()/level_shape()/total_num_elems() keep
working for the pipelines and the cache. It stores the input shape, the
local/global level counts, the padded fine/coarse block grid of each
local level (computed once by compute_local_shapes, replacing four copies
of the 8 -> 5 rule), and owns the global Hierarchy, built once, only when
M > 0, on the coarsest local grid (the input grid when L == 0).

- HybridHierarchyCompressor::HierarchyType is HybridHierarchy; the
  compressor, hybrid/block-local refactors and both quantizers take a
  HybridHierarchy& and read grids and the global hierarchy from it.
  check_config() rejects a config whose L/M differ from the hierarchy's.
- CompressorCache::InHierarchyCache takes the Config and drops entries
  for which matches_config() fails (always true for a plain Hierarchy).
- DomainDecomposer builds OperatorType::HierarchyType and sizes it with
  the new static HierarchyType::EstimateMemoryFootprint(shape, config).
- Hybrid tests construct a HybridHierarchy.

Hybrid peak device memory, 1D float 2^24 elements (SERIAL): L=1 2121 ->
1225 MB, L=1/M=1 2913 -> 1705 MB, L=2/M=2 2643 -> 1563 MB, L=0/M=2
3465 -> 1897 MB; with L=1 the hierarchy allocates no device memory.
SERIAL output is byte-identical across 1D-4D, float/double, s = inf/0,
multi-dim and hybrid L1/L2/L1G1/L2G2/L0G2. SERIAL and CUDA (RTX PRO 6000
Blackwell) unit tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max_larget_level capped the levels of the multi-dimensional Hierarchy
and num_global_refactoring_level set the levels of the hybrid global
stage, which is itself a multi-dimensional Hierarchy capped the same way.
Merge them into num_global_refactoring_level: it now caps every
multilevel hierarchy (multi-dim, MDR-X, hybrid global stage). Both level
counts are now SIZE; defaults are num_local_refactoring_level = 1 and
num_global_refactoring_level = 3 (previously: all levels for multi-dim,
0 global levels for hybrid).

Non-hybrid headers did not record the level count -- the reader rebuilt
the hierarchy from the shape -- so a non-default cap could not be
decompressed. Record it for every compressed and MDR-X stream in a new
optional FunctionDecomposition.num_global_refactoring_levels. On read it
sets the Config; streams without it (everything written before) were
decomposed with all levels and are read that way. Hybrid streams still
take L/M from their HybridHierarchy message.

The mgard-x CLI -ll/-gl now default to the Config defaults instead of
hard-coded 1/0, and -gl also applies to multi-dim compression.

Behavior change: default compression now uses at most 3 global levels.
Under rel 1e-3, multi-dim L-inf CR rises (1D 2.89 -> 3.44, 3D
2.57 -> 2.85); multi-dim s = 0 in 1D drops (4.08 -> 3.65); all bounds
hold. Hybrid with an s-norm bound now warns that it falls back from 3
global levels to 0 by default.

Files from the previous build (multi-dim and hybrid, 1D-4D,
float/double, SERIAL and CUDA) decompress byte-identically, and new
compression with matching levels reproduces the old output; old MDR-X
streams reconstruct identically. SERIAL and CUDA unit tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Serializer: Serialize aligns each value to sizeof(T) but Deserialize
  did not skip the padding. A subdomain stored uncompressed whose byte
  count is not a multiple of 8 (e.g. a 9x3x5x25 float block) shifted
  every later subdomain size field, and decompression failed or read
  garbage. Deserialize now aligns the same way.
- MDR reconstruction sized level_signs to the raw per-level element
  count, while the bitplane decoder writes a sign for every slot of its
  last 32-value batch. The refactoring Resize already rounded up; the
  reconstruction Resize now does too (it takes the reconstructor for its
  BATCH_SIZE). The overrun corrupted the heap on SERIAL (free(): invalid
  pointer) and wrote out of bounds on CUDA whenever a later subdomain
  grew a level's buffer.
- MDR reconstruction copied the last subdomain from
  abs((current_buffer - 1) % 3), which is 1 instead of 2 when
  current_buffer is 0: with a multiple of 3 subdomains the last one was
  written back from the wrong buffer and the error bound failed.
- MDR refactor/reconstruct sized their reused buffers from subdomain 0;
  size them from the largest subdomain, as the compression pipelines do.
- Zstd: read the stored size as size_t (it was truncated to 32 bits) and
  check ZSTD_decompress, so a corrupt stream fails instead of handing
  garbage to the Huffman decoder.
- Compression output buffer: reserve room for non-uniform coordinates,
  which the header stores as doubles, and fail with
  OutputTooLargeFailure instead of overflowing when the header does not
  fit.
- Metadata: delete[] for the coordinate copies allocated with new[].
- mgard-x CLI: check the status of decompress() in both paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pipeline

Data written by MGARD 1.6.x differs from the current format in many
places (hierarchy geometry and level volumes, Huffman layout, basis,
defaults such as an unlimited level cap). Rather than branch on the
version throughout the pipeline, add a complete copy of the 1.6.0
decompression path in its own tree and namespace (include/mgard-x-160,
src/mgard-x-160, namespace mgard_x_160), kept faithful to 1.6.0.

- Compression always writes the current format; only decompression is
  routed. DynamicAPI's decompress overloads and release_cache first read
  just the header versions (new MetadataBase::DeserializeVersion, which
  does not validate the rest of the header) and send streams written by
  1.6.x or earlier (WrittenByV160OrEarlier) to mgard_x_160 through
  Bridge.cpp.in, which converts the Config and results between the two
  namespaces. The compatibility decoder starts from the 1.6.0 defaults
  and takes from the caller only the settings 1.6.0 took.
- CMake option MGARD_ENABLE_DATA_COMPATIBILITY (default OFF; it roughly
  doubles the MGARD-X build). Without it, 1.6.0 data is reported with
  the new status DataCompatibilityNotEnabledFailure.
- The bug fixes made to the current pipeline also apply to the
  compatibility one: MaxDim slab offsets along a dimension other than the
  slowest, zstd size truncation and unchecked decompression, and the
  Serialize/Deserialize alignment mismatch. Files that 1.6.0 itself
  could not decode because of these now decode.

Verified against a build of MGARD 1.6.0: 61 CLI-written files (1D-5D,
float/double, huffman/huffman-zstd, s = inf/0/1, rel/abs) decompress
byte-identically on SERIAL and within 1.5e-6 of range on CUDA; 68
API-only files (reorder, non-uniform, SingleDim, MaxDim and Block
decomposition, CPU lossless, double) and 60 zstd files likewise. Current
data is unaffected by the routing, and an OFF build reports 1.6.0 data
with the new status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…composition

The three domain decomposition types become one: the domain is cut into a
grid of blocks of a common, possibly non-cubic shape, so a 400x300x200
array with 100x300x200 blocks is the old MaxDim split and cubic blocks
the old Block split. Along each dimension block i starts at
i * block_shape[d]; a remainder narrower than 3 nodes is merged into the
block before it, so every block is a valid hierarchy.

Config::domain_decomposition (and block_size, domain_decomposition_dim,
domain_decomposition_sizes) is replaced by:
- domain_decomposition_mode = Auto (default): one block when the data
  fits the memory limit; otherwise the largest block extent (the slowest
  dimension on ties) is halved until the largest block, merged remainder
  included, fits. It fails if even 3-node blocks do not fit.
- domain_decomposition_mode = Manual with domain_decomposition_block_size:
  extents larger than the data are clamped; compression fails with an
  error if the size count does not match the dimensions, any extent is
  below 3, or the largest block does not fit the memory limit. Shape
  adjustment is rejected in Manual mode.

DomainDecomposer is rewritten around the block grid (subdomain_start /
subdomain_shape / block_shape replace the per-type branches and
dim_subdomain_offset), and its per-dimension copy loops are replaced by
one generic N-D box copy: dimensions contiguous on both sides are merged,
then the box is a single Copy1D/Copy2D/Copy3D, looping only over
dimensions beyond the third. This also fixes the old MaxDim offsets for a
dimension other than the slowest, which copied past the end of the
array.

RuntimeX: MemoryManager::CopyND is renamed Copy2D, and Copy3D is added
(cudaMemcpy3DAsync / hipMemcpy3DAsync; a loop of Copy2D on the other
backends). SYCL and Kokkos Copy2D now honour the leading dimensions.

Format: DomainDecomposition gains repeated block_shape and is written as
BLOCK (NOOP when not decomposed). Older MAX_DIMENSION and BLOCK headers
are read as the equivalent block shape; VARIABLE is no longer supported.
Metadata stores domain_decomposition_block_shape instead of
ddtype/dim/size.

CLIs: mgard-x and mdr-x take -dd auto|manual and
-dd-size <D> <b_1> ... <b_D>. The MDR QoI tools always used one
subdomain per variable (now Manual blocks of shape[0] / 3) and drop -dd.

Tested on SERIAL and CUDA: compression of 1D-4D data with Auto (fitting
and memory-limited), MaxDim-like, cubic, merged-remainder, last-dimension,
clamped, 2D and 4D blocks (incl. uncompressed blocks) stays within the
error bound, also when decompressed standalone; MDR refactor/reconstruct
of the same layouts stays within 1e-2/1e-4/1e-6; files with the previous
MaxDim and Block headers decode within the bound; Manual-mode errors are
reported; unit tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release notes: add MGARD 1.6.x data compatibility, the generic block
domain decomposition (Auto/Manual), Copy3D, MDR-X ZSTD bitplane
compression, the breaking Config changes (domain decomposition settings,
num_global_refactoring_level), the memory reductions, optional MPI for
MDR, and the recent fixes.

MGARD-X guide: document MGARD_ENABLE_DATA_COMPATIBILITY and decompressing
data from earlier versions, the domain decomposition mode and block size,
and the DataCompatibilityNotEnabledFailure status. Bring the CLI section
in line with the current mgard-x options, and fix stale Config entries
(projection_mode, huff_block_size default, nonexistent num_dev).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_device_type mapped "sycl" to device_type::HIP (copied from the hip
case), so -d sycl failed with a missing-backend error in SYCL-only builds
and ran on the AMD GPU in builds with both HIP and SYCL. Affects mgard-x,
mdr-x, mdr-x-qoi, pmdr-x-qoi and pmdr-x-qoi-io.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Formatting only: line wrapping, comment reflow, and one long string
literal split into adjacent literals. Covers the mgard-x-160
compatibility tree, the domain decomposer, Metadata, the device adapters,
the executables, and Hierarchy/HybridHierarchy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JieyangChen7
JieyangChen7 merged commit 1e735ef into CODARcode:master Oct 14, 2026
6 checks passed
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.

1 participant