From c2e1dfee7b967cf5bafe3e52a83ed3da5b68c446 Mon Sep 17 00:00:00 2001 From: gqian-coder Date: Wed, 14 Oct 2026 03:26:19 -0400 Subject: [PATCH 1/2] mgard-x: record the decomposition depth cap in the header and restore it on decompression Config::max_larget_level caps the number of decomposition levels, but nothing about it reached the file: the header's l_target was serialized from a member that is never set, and InitializeConfig never touched config.max_larget_level. A decompressor that only has the file (mgard-x -x, or the API with a default Config) rebuilt the hierarchy at full depth, so the per-level quantization step and the number of recomposition passes no longer matched the writer's and the output was wrong without any error (max error 1.35 at tol 1e-3 on a range-3 field compressed with max_larget_level = 1). Add a LevelCap sub-message to FunctionDecomposition that records either "unlimited" or the exact cap. Presence keeps three cases apart: absent (stream predates the field), cap 0 (single level) and unlimited. Compression and MDR refactoring always record it, unlimited included, so a stale cap in the reader's Config cannot leak in either; InitializeConfig and the MDR load() restore it only when the header recorded it, so streams written before this change behave exactly as before (full depth with a default Config; a capped legacy stream still needs the cap through Config, as nothing in it records the cap). Older readers ignore the new field. PrintSummary reports it; the doc bullet claimed a default of 0. Co-Authored-By: Claude Fable 5.1 --- doc/MGARD-X.md | 2 +- .../CompressionHighLevel.hpp | 6 + include/mgard-x/MDRHighLevel/MDRHighLevel.hpp | 9 ++ include/mgard-x/Metadata/Metadata.hpp | 10 ++ src/mgard-x/Metadata/Metadata.cpp | 44 +++++- src/mgard.proto | 16 ++ tests/mgard-x/CMakeLists.txt | 2 + .../mgard-x/src/test_LevelCapMetadata.cpp.in | 137 ++++++++++++++++++ .../mgard-x/src/test_LevelCapRoundTrip.cpp.in | 122 ++++++++++++++++ 9 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 tests/mgard-x/src/test_LevelCapMetadata.cpp.in create mode 100644 tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in diff --git a/doc/MGARD-X.md b/doc/MGARD-X.md index dee9fa35f..5e5e7a75b 100644 --- a/doc/MGARD-X.md +++ b/doc/MGARD-X.md @@ -183,7 +183,7 @@ An executable ```mgard-x``` will be built after building the MGARD-X library. To + ```mgard_x::hybrid_projection_mode_type::Hierarchical```: remove the L2 projection (L∞ only) + ```fuse_decompose_quantize```: controls fused local decomposition and quantization for the hybrid hierarchy (***Default: true***) + ```fuse_dequantize_recompose```: controls fused local dequantization and recomposition for the hybrid hierarchy (***Default: true***) - + ```max_larget_level```: controls max level of multi-level decomposition (***Default: 0 (no limit)***) + + ```max_larget_level```: caps the multi-level decomposition at levels 0 to ```max_larget_level```; the cap is recorded in the compressed stream and restored on decompression (***Default: no limit***) + ```max_memory_footprint```(for high-level APIs only): controls maximum memory footprint in bytes (***Default: inf (no limit)***) + ```adjust_shape```(for high-level APIs only): controls whether or not to enable shape adjustment for better decomposition performance (***Default: false***) ## Using high-level APIs diff --git a/include/mgard-x/CompressionHighLevel/CompressionHighLevel.hpp b/include/mgard-x/CompressionHighLevel/CompressionHighLevel.hpp index 9ea2438b2..f28178a39 100644 --- a/include/mgard-x/CompressionHighLevel/CompressionHighLevel.hpp +++ b/include/mgard-x/CompressionHighLevel/CompressionHighLevel.hpp @@ -239,6 +239,12 @@ general_compress_pipeline(std::vector shape, T tol, T s, // FillForCompression does not carry the BlockDelta block size; set it here so // it is persisted in (and restored from) the metadata header. m.block_delta_block_size = config.block_delta_block_size; + // Nor the cap on the decomposition depth. The shape alone does not determine + // the number of levels once a cap is set, and the decompressor has to rebuild + // the exact hierarchy, so record it -- "unlimited" included, so a stale cap + // in the decompressor's Config cannot leak in either. + m.max_larget_level = config.max_larget_level; + m.max_larget_level_recorded = true; // Same for the hybrid (BlockMGARD) parameters: the decompressor cannot // reconstruct the buffer layout or the per-block quantization steps without // them. diff --git a/include/mgard-x/MDRHighLevel/MDRHighLevel.hpp b/include/mgard-x/MDRHighLevel/MDRHighLevel.hpp index 3a45cab19..b896136f9 100644 --- a/include/mgard-x/MDRHighLevel/MDRHighLevel.hpp +++ b/include/mgard-x/MDRHighLevel/MDRHighLevel.hpp @@ -54,6 +54,11 @@ void load(Config &config, Metadata &metadata) { config.huff_dict_size = metadata.huff_dict_size; config.huff_block_size = metadata.huff_block_size; config.total_num_bitplanes = metadata.number_bitplanes; + // As in MetadataBase::InitializeConfig: a recorded depth cap always wins; + // an unrecorded one (older stream) leaves the caller's value alone. + if (metadata.max_larget_level_recorded) { + config.max_larget_level = (SIZE)metadata.max_larget_level; + } } template @@ -130,6 +135,10 @@ void MDRefactor(std::vector shape, const void *original_data, if (log::level & log::TIME) timer_each.start(); Metadata m; + // The reconstructor rebuilds the hierarchy from the header; see the matching + // lines in CompressionHighLevel.hpp. + m.max_larget_level = config.max_larget_level; + m.max_larget_level_recorded = true; if (uniform) { m.FillForMDR( (T)0.0, config.decomposition, config.lossless, config.huff_dict_size, diff --git a/include/mgard-x/Metadata/Metadata.hpp b/include/mgard-x/Metadata/Metadata.hpp index 5549156a5..c5f6afc04 100644 --- a/include/mgard-x/Metadata/Metadata.hpp +++ b/include/mgard-x/Metadata/Metadata.hpp @@ -10,6 +10,7 @@ #include "../Utilities/Types.h" #include "MGARDConfig.hpp" #include +#include #include #ifndef MGARD_X_METADATA @@ -36,6 +37,15 @@ struct MetadataBase { enum decomposition_type decomposition; uint32_t l_target = 0; + // Cap on the decomposition depth (Config::max_larget_level) the stream was + // written with. Every stream written since this was added records it, "no + // cap" included, so a reader never depends on the caller's Config for it. + // Streams from before leave `max_larget_level_recorded` false, and + // InitializeConfig then keeps whatever the caller configured (unlimited by + // default) -- the depth those streams were written with unless the writer + // capped it out of band. + bool max_larget_level_recorded = false; + uint64_t max_larget_level = std::numeric_limits::max(); // no cap // Hybrid (block-local + global) hierarchy, a.k.a. BlockMGARD. Only // meaningful when `decomposition == decomposition_type::Hybrid`; these are diff --git a/src/mgard-x/Metadata/Metadata.cpp b/src/mgard-x/Metadata/Metadata.cpp index 05f0afcfb..f01809cf5 100644 --- a/src/mgard-x/Metadata/Metadata.cpp +++ b/src/mgard-x/Metadata/Metadata.cpp @@ -266,6 +266,15 @@ void MetadataBase::InitializeConfig(Config &config) { config.roi_tolerance_map = hybrid_roi_tolerance_map; } } + // The decomposition depth has to match the writer's exactly (the per-level + // quantization step and the number of recomposition passes depend on it), so + // a recorded cap always wins over the caller's. Streams written before the + // cap was recorded leave the caller's value alone: unlimited by default, + // which is how those streams were written unless the writer capped the depth + // out of band -- then the caller has to keep supplying the same cap. + if (max_larget_level_recorded) { + config.max_larget_level = (SIZE)max_larget_level; + } } void MetadataBase::PrintSummary() { @@ -335,6 +344,14 @@ void MetadataBase::PrintSummary() { std::cout << "disabled\n"; } } + std::cout << "Max level: "; + if (!max_larget_level_recorded) { + std::cout << "not recorded (full depth unless the writer capped it)\n"; + } else if (max_larget_level == std::numeric_limits::max()) { + std::cout << "unlimited\n"; + } else { + std::cout << max_larget_level << "\n"; + } std::cout << "Domain Decomposition: "; if (domain_decomposed) { if (ddtype == domain_decomposition_type::MaxDim) { @@ -561,10 +578,20 @@ std::vector MetadataBase::Serialize() { SerializeROITolerances(hybrid_roi_tolerance_map, roi); } } - // Not populated by MGARD-X: the reader rebuilds the hierarchy (and hence - // the level count) from the shape, and under domain decomposition there is - // no single value to record. Written for format compatibility only. + // Not populated by MGARD-X: under domain decomposition every subdomain has + // its own level count, so there is no single value to record. Written for + // format compatibility only; the depth travels as `level_cap` below. function_decomposition.set_l_target(l_target); + if (max_larget_level_recorded) { + mgard::pb::LevelCap &level_cap = + *function_decomposition.mutable_level_cap(); + if (max_larget_level == std::numeric_limits::max()) { + level_cap.set_unlimited(true); + } else { + level_cap.set_unlimited(false); + level_cap.set_max_level(max_larget_level); + } + } } { // Quantization @@ -896,6 +923,17 @@ void MetadataBase::Deserialize( "in MGARD-X."); } l_target = function_decomposition.l_target(); + if (function_decomposition.has_level_cap()) { + const mgard::pb::LevelCap &level_cap = function_decomposition.level_cap(); + max_larget_level_recorded = true; + max_larget_level = level_cap.unlimited() + ? std::numeric_limits::max() + : level_cap.max_level(); + } else { + // Written before the cap was recorded; see InitializeConfig. + max_larget_level_recorded = false; + max_larget_level = std::numeric_limits::max(); + } } { // Quantization diff --git a/src/mgard.proto b/src/mgard.proto index 0a20ab806..f9835a783 100644 --- a/src/mgard.proto +++ b/src/mgard.proto @@ -135,6 +135,15 @@ message HybridHierarchy { Projection projection = 5; } +// MGARD-X cap on the number of decomposition levels. Levels are numbered from +// 0 (coarsest) to `max_level`, so the hierarchy has at most `max_level + 1` +// levels; a shape that does not support that many stops earlier. +message LevelCap { + // Set when the writer did not cap the depth; `max_level` is then unused. + bool unlimited = 1; + uint64 max_level = 2; +} + message FunctionDecomposition { enum Transform { MULTILEVEL_COEFFICIENTS = 0; } enum Hierarchy { @@ -150,6 +159,13 @@ message FunctionDecomposition { uint64 L_target = 3; // Required when `hierarchy == HYBRID_HIERARCHY`, absent otherwise. HybridHierarchy hybrid_hierarchy = 4; + // Cap on the decomposition depth the stream was written with (MGARD-X + // `Config::max_larget_level`). The reader has to rebuild the hierarchy with + // the same depth: the per-level quantization step and the number of + // recomposition passes both depend on it. Absent from streams written before + // it was recorded; those were decomposed to the full depth their shape allows + // unless the writer capped it out of band, which a reader cannot detect. + LevelCap level_cap = 5; } message Quantization { diff --git a/tests/mgard-x/CMakeLists.txt b/tests/mgard-x/CMakeLists.txt index 7cd53ea12..7d855e856 100644 --- a/tests/mgard-x/CMakeLists.txt +++ b/tests/mgard-x/CMakeLists.txt @@ -127,6 +127,8 @@ foreach(DEVICE_TYPE IN LISTS DEVICE_TYPE_LIST) mgard_x_add_test(test_DataRefactoring ${DEVICE_TYPE}) mgard_x_add_test(test_HybridHierarchy ${DEVICE_TYPE}) mgard_x_add_test(test_HybridMetadata ${DEVICE_TYPE}) + mgard_x_add_test(test_LevelCapMetadata ${DEVICE_TYPE}) + mgard_x_add_test(test_LevelCapRoundTrip ${DEVICE_TYPE}) mgard_x_add_test(test_KernelFusionSwitch ${DEVICE_TYPE}) mgard_x_add_test(test_Compressor ${DEVICE_TYPE}) mgard_x_add_test(test_Zstd ${DEVICE_TYPE}) diff --git a/tests/mgard-x/src/test_LevelCapMetadata.cpp.in b/tests/mgard-x/src/test_LevelCapMetadata.cpp.in new file mode 100644 index 000000000..6fd3c1f25 --- /dev/null +++ b/tests/mgard-x/src/test_LevelCapMetadata.cpp.in @@ -0,0 +1,137 @@ +/* + * Copyright 2026, Oak Ridge National Laboratory. + * MGARD-X: MultiGrid Adaptive Reduction of Data Portable across GPUs and CPUs + */ + +#include "catch2/catch_test_macros.hpp" + +#include +#include + +#include "mgard-x/Config/Config.h" +#include "mgard-x/Metadata/Metadata.hpp" +#include "mgard-x/RuntimeX/RuntimeX.h" + +namespace mgard_x { + +using TestDevice = @DEVICE_TYPE@; + +namespace { + +constexpr uint64_t kUnlimited = std::numeric_limits::max(); + +// A well-formed plain (non-hybrid) compression header to hang the cap on. +Metadata base_metadata() { + Metadata m; + m.FillForCompression( + error_bound_type::ABS, (float)1e-3, + std::numeric_limits::infinity(), (float)1.0, + decomposition_type::MultiDim, lossless_type::Huffman, + /*huff_dict_size=*/8192, /*huff_block_size=*/1024, + std::vector{64, 64, 64}, /*domain_decomposed=*/false, + domain_decomposition_type::MaxDim, /*domain_decomposed_dim=*/0, + /*domain_decomposed_size=*/0); + return m; +} + +Metadata round_trip(Metadata &out) { + std::vector bytes = out.MetadataBase::Serialize(); + Metadata in; + in.MetadataBase::Deserialize(bytes); + return in; +} + +} // namespace + +TEST_CASE("a cap on the decomposition depth survives a metadata round trip", + "[LevelCapMetadata]") { + Metadata out = base_metadata(); + out.max_larget_level = 2; + out.max_larget_level_recorded = true; + + Metadata in = round_trip(out); + REQUIRE(in.max_larget_level_recorded); + REQUIRE(in.max_larget_level == 2); + + // The point of recording it: a Config that never saw the compression + // parameters comes back with the writer's cap. + Config config; + in.InitializeConfig(config); + REQUIRE(config.max_larget_level == 2); +} + +TEST_CASE("a cap of zero is a single level, not an absent cap", + "[LevelCapMetadata]") { + Metadata out = base_metadata(); + out.max_larget_level = 0; + out.max_larget_level_recorded = true; + + Metadata in = round_trip(out); + REQUIRE(in.max_larget_level_recorded); + REQUIRE(in.max_larget_level == 0); + + Config config; + in.InitializeConfig(config); + REQUIRE(config.max_larget_level == 0); +} + +TEST_CASE("an unlimited depth is recorded and overrides a stale cap", + "[LevelCapMetadata]") { + Metadata out = base_metadata(); + out.max_larget_level = kUnlimited; + out.max_larget_level_recorded = true; + + Metadata in = round_trip(out); + REQUIRE(in.max_larget_level_recorded); + REQUIRE(in.max_larget_level == kUnlimited); + + // E.g. a Config reused from compressing a different, capped, array. + Config config; + config.max_larget_level = 1; + in.InitializeConfig(config); + REQUIRE(config.max_larget_level == std::numeric_limits::max()); +} + +TEST_CASE("a stream without a recorded cap leaves the caller's Config alone", + "[LevelCapMetadata]") { + // Streams written before the cap was recorded carry no level_cap field; + // not recording it reproduces exactly that header. + Metadata out = base_metadata(); + out.max_larget_level_recorded = false; + + Metadata in = round_trip(out); + REQUIRE_FALSE(in.max_larget_level_recorded); + REQUIRE(in.max_larget_level == kUnlimited); + + // A default Config decompresses at full depth, which is how such streams + // were written unless the writer capped the depth out of band ... + Config fresh; + in.InitializeConfig(fresh); + REQUIRE(fresh.max_larget_level == std::numeric_limits::max()); + + // ... in which case the caller keeps supplying that cap, as before. + Config capped; + capped.max_larget_level = 3; + in.InitializeConfig(capped); + REQUIRE(capped.max_larget_level == 3); +} + +TEST_CASE("deserializing a stream without the cap resets a previously read one", + "[LevelCapMetadata]") { + Metadata capped = base_metadata(); + capped.max_larget_level = 1; + capped.max_larget_level_recorded = true; + std::vector capped_bytes = capped.MetadataBase::Serialize(); + + Metadata legacy = base_metadata(); + std::vector legacy_bytes = legacy.MetadataBase::Serialize(); + + Metadata in; + in.MetadataBase::Deserialize(capped_bytes); + REQUIRE(in.max_larget_level_recorded); + in.MetadataBase::Deserialize(legacy_bytes); + REQUIRE_FALSE(in.max_larget_level_recorded); + REQUIRE(in.max_larget_level == kUnlimited); +} + +} // namespace mgard_x diff --git a/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in b/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in new file mode 100644 index 000000000..2f6ed6e49 --- /dev/null +++ b/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in @@ -0,0 +1,122 @@ +/* + * Copyright 2026, Oak Ridge National Laboratory. + * MGARD-X: MultiGrid Adaptive Reduction of Data Portable across GPUs and CPUs + */ + +// End to end: the cap on the decomposition depth a stream was compressed with +// reaches the decompressor through the header, so a reader that knows nothing +// but the stream reconstructs within the error bound. + +#include "catch2/catch_test_macros.hpp" + +#include +#include +#include +#include + +#include "compress_x.hpp" + +namespace mgard_x { + +namespace { + +Config device_config() { + Config config; + config.dev_type = device_type::@DEVICE_TYPE@; + config.log_level = log::ERR; + return config; +} + +// 33 -> 17 -> 9 -> 5 -> 3 -> 2: six levels when the depth is not capped. +const std::vector kShape = {33, 33, 33}; +const double kTol = 1e-3; + +// Smooth, range about [-1.5, 1.5], so a wrong hierarchy is off by O(1). +std::vector smooth_field() { + std::vector data(kShape[0] * kShape[1] * kShape[2]); + for (SIZE k = 0; k < kShape[0]; k++) { + for (SIZE j = 0; j < kShape[1]; j++) { + for (SIZE i = 0; i < kShape[2]; i++) { + double z = (double)k / (kShape[0] - 1); + double y = (double)j / (kShape[1] - 1); + double x = (double)i / (kShape[2] - 1); + data[(k * kShape[1] + j) * kShape[2] + i] = + (float)(std::sin(2 * M_PI * x) * std::cos(2 * M_PI * y) * + std::sin(M_PI * z) + + 0.5 * x * y); + } + } + } + return data; +} + +std::vector compress_with(const std::vector &data, + Config config) { + std::vector compressed(2 * data.size() * sizeof(float)); + void *compressed_ptr = compressed.data(); + size_t compressed_size = compressed.size(); + REQUIRE(compress(kShape.size(), data_type::Float, kShape, kTol, + std::numeric_limits::infinity(), + error_bound_type::ABS, data.data(), compressed_ptr, + compressed_size, config, + true) == compress_status_type::Success); + compressed.resize(compressed_size); + return compressed; +} + +double decompress_error(const std::vector &compressed, + const std::vector &data, Config config) { + std::vector decompressed(data.size()); + void *decompressed_ptr = decompressed.data(); + REQUIRE(decompress(compressed.data(), compressed.size(), decompressed_ptr, + config, true) == compress_status_type::Success); + double error = 0; + for (size_t i = 0; i < data.size(); i++) { + error = std::max(error, (double)std::fabs(data[i] - decompressed[i])); + } + return error; +} + +// Compress with `writer`, decompress with `reader`, return the max abs error. +double round_trip_error(const std::vector &data, Config writer, + Config reader) { + return decompress_error(compress_with(data, writer), data, reader); +} + +// The bound holds up to float rounding of the reconstruction; a hierarchy +// rebuilt with the wrong depth misses it by three orders of magnitude. +const double kSlack = 1.001; + +} // namespace + +TEST_CASE("a capped decomposition depth is restored from the header", + "[mgard-x][@DEVICE_TYPE@][LevelCapRoundTrip]") { + std::vector data = smooth_field(); + Config writer = device_config(); + writer.max_larget_level = 1; + // The reader knows nothing but the stream. + Config reader = device_config(); + REQUIRE(round_trip_error(data, writer, reader) <= kTol * kSlack); +} + +TEST_CASE("a single-level decomposition is restored from the header", + "[mgard-x][@DEVICE_TYPE@][LevelCapRoundTrip]") { + std::vector data = smooth_field(); + Config writer = device_config(); + writer.max_larget_level = 0; + Config reader = device_config(); + REQUIRE(round_trip_error(data, writer, reader) <= kTol * kSlack); +} + +TEST_CASE( + "an unlimited depth in the header overrides a stale cap in the reader", + "[mgard-x][@DEVICE_TYPE@][LevelCapRoundTrip]") { + std::vector data = smooth_field(); + Config writer = device_config(); + // E.g. a Config reused from decompressing a capped stream. + Config reader = device_config(); + reader.max_larget_level = 1; + REQUIRE(round_trip_error(data, writer, reader) <= kTol * kSlack); +} + +} // namespace mgard_x From e08798cfa5fbaf298cf49a52f778eae38d57b69a Mon Sep 17 00:00:00 2001 From: gqian-coder Date: Wed, 14 Oct 2026 03:26:19 -0400 Subject: [PATCH 2/2] mgard-x: key the hierarchy caches on the depth cap as well as the shape CompressorCache, RefactorCache and ReconstructorCache keyed a cached hierarchy on its shape alone. The same shape under a different Config::max_larget_level has a different number of levels, so a decompress (or a later compress) of a same-shape array silently reused a hierarchy built for another cap. Within one process this is self-consistent and only shows in the output size, which is why a same-process round trip never exposed the missing header field fixed in the previous commit. Make the cap part of the key and pass it from the Config at every lookup, insert and get in the compression and MDR pipelines. The end-to-end test checks that alternating capped and uncapped compressions of the same array reproduce the sizes obtained from an empty cache. Co-Authored-By: Claude Fable 5.1 --- .../CompressionHighLevel/CPUPipelines.hpp | 24 ++++++++------ .../CompressionHighLevel/GPUPipelines.hpp | 28 ++++++++++------ .../CompressionLowLevel/CompressorCache.hpp | 33 ++++++++++++------- .../Reconstructor/ReconstructorCache.hpp | 33 ++++++++++++------- .../mgard-x/MDR-X/Refactor/RefactorCache.hpp | 33 ++++++++++++------- .../MDRHighLevel/ReconstructPipeline.hpp | 15 +++++---- .../MDRHighLevel/ReconstructPipelineQoI.hpp | 15 +++++---- .../mgard-x/MDRHighLevel/RefactorPipeline.hpp | 15 +++++---- .../mgard-x/src/test_LevelCapRoundTrip.cpp.in | 31 +++++++++++++++++ 9 files changed, 156 insertions(+), 71 deletions(-) diff --git a/include/mgard-x/CompressionHighLevel/CPUPipelines.hpp b/include/mgard-x/CompressionHighLevel/CPUPipelines.hpp index 9677ece15..819b9cdec 100644 --- a/include/mgard-x/CompressionHighLevel/CPUPipelines.hpp +++ b/include/mgard-x/CompressionHighLevel/CPUPipelines.hpp @@ -18,15 +18,17 @@ enum compress_status_type compress_pipeline_cpu( Cache::cache.device_compressed_buffer; if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } } log::info("Adjust device buffers and hierarchy"); @@ -37,8 +39,8 @@ enum compress_status_type compress_pipeline_cpu( {domain_decomposer.subdomain_compressed_buffer_size( domain_decomposer.largest_subdomain())}); - HierarchyType &hierarchy = - Cache::cache.GetHierarchyCache(domain_decomposer.subdomain_shape(i)); + HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( + domain_decomposer.subdomain_shape(i), config.max_larget_level); compressor[i].Adapt(hierarchy, config, 0); } @@ -290,15 +292,17 @@ enum compress_status_type decompress_pipeline_cpu( Cache::cache.device_compressed_buffer; if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } } @@ -310,8 +314,8 @@ enum compress_status_type decompress_pipeline_cpu( {domain_decomposer.subdomain_compressed_buffer_size( domain_decomposer.largest_subdomain())}); - HierarchyType &hierarchy = - Cache::cache.GetHierarchyCache(domain_decomposer.subdomain_shape(i)); + HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( + domain_decomposer.subdomain_shape(i), config.max_larget_level); compressor[i].Adapt(hierarchy, config, 0); } std::vector compressed_size(domain_decomposer.num_subdomains(), 0); diff --git a/include/mgard-x/CompressionHighLevel/GPUPipelines.hpp b/include/mgard-x/CompressionHighLevel/GPUPipelines.hpp index 36ae915d4..5e04a5540 100644 --- a/include/mgard-x/CompressionHighLevel/GPUPipelines.hpp +++ b/include/mgard-x/CompressionHighLevel/GPUPipelines.hpp @@ -18,15 +18,17 @@ enum compress_status_type compress_pipeline_gpu( Cache::cache.device_compressed_buffer; if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } } log::info("Adjust device buffers"); @@ -45,7 +47,8 @@ enum compress_status_type compress_pipeline_gpu( domain_decomposer.largest_subdomain())}); HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(domain_decomposer.largest_subdomain())); + domain_decomposer.subdomain_shape(domain_decomposer.largest_subdomain()), + config.max_larget_level); log::info("Adapt Compressor to hierarchy"); compressor.Adapt(hierarchy, config, 0); @@ -93,7 +96,8 @@ enum compress_status_type compress_pipeline_gpu( int next_buffer = (current_buffer + 1) % 2; int next_queue = (current_queue + 1) % 3; HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(curr_subdomain_id)); + domain_decomposer.subdomain_shape(curr_subdomain_id), + config.max_larget_level); log::info("Adapt Compressor to hierarchy"); compressor.Adapt(hierarchy, config, current_queue); @@ -277,15 +281,17 @@ enum compress_status_type decompress_pipeline_gpu( Cache::cache.device_compressed_buffer; if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } } @@ -305,7 +311,8 @@ enum compress_status_type decompress_pipeline_gpu( domain_decomposer.largest_subdomain())}); HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(domain_decomposer.largest_subdomain())); + domain_decomposer.subdomain_shape(domain_decomposer.largest_subdomain()), + config.max_larget_level); log::info("Adapt Compressor to hierarchy"); compressor.Adapt(hierarchy, config, 0); @@ -365,7 +372,8 @@ enum compress_status_type decompress_pipeline_gpu( int next_buffer = (current_buffer + 1) % 2; int next_queue = (current_queue + 1) % 3; HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(curr_subdomain_id)); + domain_decomposer.subdomain_shape(curr_subdomain_id), + config.max_larget_level); log::info("Adapt Compressor to hierarchy"); compressor.Adapt(hierarchy, config, current_queue); diff --git a/include/mgard-x/CompressionLowLevel/CompressorCache.hpp b/include/mgard-x/CompressionLowLevel/CompressorCache.hpp index 9a335f087..949b38e2f 100644 --- a/include/mgard-x/CompressionLowLevel/CompressorCache.hpp +++ b/include/mgard-x/CompressionLowLevel/CompressorCache.hpp @@ -34,23 +34,31 @@ class CompressorBundle { bool initialized = false; - std::string GenHierarchyKey(std::vector shape) { + // A hierarchy is determined by its shape together with the cap on its depth + // (Config::max_larget_level): the same shape under a different cap has a + // different number of levels. Both are part of the key; keying on the shape + // alone let a decompress (or a later compress) silently reuse a hierarchy + // that was built for another cap. + std::string GenHierarchyKey(std::vector shape, SIZE max_larget_level) { std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << std::setfill('0') << std::setw(20) << shape[i] << std::fixed; } + ss << "|" << max_larget_level; return ss.str(); } - bool InHierarchyCache(std::vector shape, bool uniform) { + bool InHierarchyCache(std::vector shape, bool uniform, + SIZE max_larget_level) { if (!initialized) { return false; } else if (!uniform) { return false; - } else if (hierarchy_cache->find(GenHierarchyKey(shape)) == - hierarchy_cache->end()) { + } else if (hierarchy_cache->find(GenHierarchyKey( + shape, max_larget_level)) == hierarchy_cache->end()) { return false; - } else if ((*hierarchy_cache)[GenHierarchyKey(shape)].data_structure() == + } else if ((*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)] + .data_structure() == data_structure_type::Cartesian_Grid_Non_Uniform) { return false; } else { @@ -58,23 +66,26 @@ class CompressorBundle { } } - void InsertHierarchyCache(HierarchyType hierarchy) { + void InsertHierarchyCache(HierarchyType hierarchy, SIZE max_larget_level) { std::vector shape = hierarchy.level_shape(hierarchy.l_target()); - (*hierarchy_cache)[GenHierarchyKey(shape)] = hierarchy; + (*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)] = hierarchy; std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << shape[i] << " "; } - log::info("Add hierarchy with shape ( " + ss.str() + ") into cache"); + log::info("Add hierarchy with shape ( " + ss.str() + ") and max level " + + std::to_string(max_larget_level) + " into cache"); } - HierarchyType &GetHierarchyCache(std::vector shape) { + HierarchyType &GetHierarchyCache(std::vector shape, + SIZE max_larget_level) { std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << shape[i] << " "; } - log::info("Get hierarchy with shape ( " + ss.str() + ") from cache"); - return (*hierarchy_cache)[GenHierarchyKey(shape)]; + log::info("Get hierarchy with shape ( " + ss.str() + ") and max level " + + std::to_string(max_larget_level) + " from cache"); + return (*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)]; } void Release() { diff --git a/include/mgard-x/MDR-X/Reconstructor/ReconstructorCache.hpp b/include/mgard-x/MDR-X/Reconstructor/ReconstructorCache.hpp index f405f70fa..02725cfa4 100644 --- a/include/mgard-x/MDR-X/Reconstructor/ReconstructorCache.hpp +++ b/include/mgard-x/MDR-X/Reconstructor/ReconstructorCache.hpp @@ -28,23 +28,31 @@ class ReconstructorBundle { bool initialized = false; - std::string GenHierarchyKey(std::vector shape) { + // A hierarchy is determined by its shape together with the cap on its depth + // (Config::max_larget_level): the same shape under a different cap has a + // different number of levels. Both are part of the key; keying on the shape + // alone let a decompress (or a later compress) silently reuse a hierarchy + // that was built for another cap. + std::string GenHierarchyKey(std::vector shape, SIZE max_larget_level) { std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << std::setfill('0') << std::setw(20) << shape[i] << std::fixed; } + ss << "|" << max_larget_level; return ss.str(); } - bool InHierarchyCache(std::vector shape, bool uniform) { + bool InHierarchyCache(std::vector shape, bool uniform, + SIZE max_larget_level) { if (!initialized) { return false; } else if (!uniform) { return false; - } else if (hierarchy_cache->find(GenHierarchyKey(shape)) == - hierarchy_cache->end()) { + } else if (hierarchy_cache->find(GenHierarchyKey( + shape, max_larget_level)) == hierarchy_cache->end()) { return false; - } else if ((*hierarchy_cache)[GenHierarchyKey(shape)].data_structure() == + } else if ((*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)] + .data_structure() == data_structure_type::Cartesian_Grid_Non_Uniform) { return false; } else { @@ -52,23 +60,26 @@ class ReconstructorBundle { } } - void InsertHierarchyCache(HierarchyType hierarchy) { + void InsertHierarchyCache(HierarchyType hierarchy, SIZE max_larget_level) { std::vector shape = hierarchy.level_shape(hierarchy.l_target()); - (*hierarchy_cache)[GenHierarchyKey(shape)] = hierarchy; + (*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)] = hierarchy; std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << shape[i] << " "; } - log::info("Add hierarchy with shape ( " + ss.str() + ") into cache"); + log::info("Add hierarchy with shape ( " + ss.str() + ") and max level " + + std::to_string(max_larget_level) + " into cache"); } - HierarchyType &GetHierarchyCache(std::vector shape) { + HierarchyType &GetHierarchyCache(std::vector shape, + SIZE max_larget_level) { std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << shape[i] << " "; } - log::info("Get hierarchy with shape ( " + ss.str() + ") from cache"); - return (*hierarchy_cache)[GenHierarchyKey(shape)]; + log::info("Get hierarchy with shape ( " + ss.str() + ") and max level " + + std::to_string(max_larget_level) + " from cache"); + return (*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)]; } void Release() { diff --git a/include/mgard-x/MDR-X/Refactor/RefactorCache.hpp b/include/mgard-x/MDR-X/Refactor/RefactorCache.hpp index c6aa73351..788987503 100644 --- a/include/mgard-x/MDR-X/Refactor/RefactorCache.hpp +++ b/include/mgard-x/MDR-X/Refactor/RefactorCache.hpp @@ -28,23 +28,31 @@ class RefactorBundle { bool initialized = false; - std::string GenHierarchyKey(std::vector shape) { + // A hierarchy is determined by its shape together with the cap on its depth + // (Config::max_larget_level): the same shape under a different cap has a + // different number of levels. Both are part of the key; keying on the shape + // alone let a decompress (or a later compress) silently reuse a hierarchy + // that was built for another cap. + std::string GenHierarchyKey(std::vector shape, SIZE max_larget_level) { std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << std::setfill('0') << std::setw(20) << shape[i] << std::fixed; } + ss << "|" << max_larget_level; return ss.str(); } - bool InHierarchyCache(std::vector shape, bool uniform) { + bool InHierarchyCache(std::vector shape, bool uniform, + SIZE max_larget_level) { if (!initialized) { return false; } else if (!uniform) { return false; - } else if (hierarchy_cache->find(GenHierarchyKey(shape)) == - hierarchy_cache->end()) { + } else if (hierarchy_cache->find(GenHierarchyKey( + shape, max_larget_level)) == hierarchy_cache->end()) { return false; - } else if ((*hierarchy_cache)[GenHierarchyKey(shape)].data_structure() == + } else if ((*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)] + .data_structure() == data_structure_type::Cartesian_Grid_Non_Uniform) { return false; } else { @@ -52,23 +60,26 @@ class RefactorBundle { } } - void InsertHierarchyCache(HierarchyType hierarchy) { + void InsertHierarchyCache(HierarchyType hierarchy, SIZE max_larget_level) { std::vector shape = hierarchy.level_shape(hierarchy.l_target()); - (*hierarchy_cache)[GenHierarchyKey(shape)] = hierarchy; + (*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)] = hierarchy; std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << shape[i] << " "; } - log::info("Add hierarchy with shape ( " + ss.str() + ") into cache"); + log::info("Add hierarchy with shape ( " + ss.str() + ") and max level " + + std::to_string(max_larget_level) + " into cache"); } - HierarchyType &GetHierarchyCache(std::vector shape) { + HierarchyType &GetHierarchyCache(std::vector shape, + SIZE max_larget_level) { std::stringstream ss; for (int i = 0; i < shape.size(); i++) { ss << shape[i] << " "; } - log::info("Get hierarchy with shape ( " + ss.str() + ") from cache"); - return (*hierarchy_cache)[GenHierarchyKey(shape)]; + log::info("Get hierarchy with shape ( " + ss.str() + ") and max level " + + std::to_string(max_larget_level) + " from cache"); + return (*hierarchy_cache)[GenHierarchyKey(shape, max_larget_level)]; } void Release() { diff --git a/include/mgard-x/MDRHighLevel/ReconstructPipeline.hpp b/include/mgard-x/MDRHighLevel/ReconstructPipeline.hpp index 5e21d8d5c..cc373c7ab 100644 --- a/include/mgard-x/MDRHighLevel/ReconstructPipeline.hpp +++ b/include/mgard-x/MDRHighLevel/ReconstructPipeline.hpp @@ -27,19 +27,21 @@ void reconstruct_pipeline( Cache::cache.device_subdomain_buffer; MDRData *mdr_data = Cache::cache.mdr_data; if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } } - HierarchyType &hierarchy = - Cache::cache.GetHierarchyCache(domain_decomposer.subdomain_shape(0)); + HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( + domain_decomposer.subdomain_shape(0), config.max_larget_level); log::info("Adjust device buffers"); mdr_data[0].Resize(reconstructor, hierarchy, 0); @@ -78,7 +80,8 @@ void reconstruct_pipeline( int next_buffer = (current_buffer + 1) % 3; int next_queue = (current_queue + 1) % 3; HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(curr_subdomain_id)); + domain_decomposer.subdomain_shape(curr_subdomain_id), + config.max_larget_level); log::info("Adapt Refactor to hierarchy"); reconstructor.Adapt(hierarchy, config, current_queue); total_size += hierarchy.total_num_elems() * sizeof(T); diff --git a/include/mgard-x/MDRHighLevel/ReconstructPipelineQoI.hpp b/include/mgard-x/MDRHighLevel/ReconstructPipelineQoI.hpp index ad56ed208..75c9cb1c9 100644 --- a/include/mgard-x/MDRHighLevel/ReconstructPipelineQoI.hpp +++ b/include/mgard-x/MDRHighLevel/ReconstructPipelineQoI.hpp @@ -102,14 +102,16 @@ void reconstruct_pipeline_qoi( log::info("Adjust device buffers"); if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } mdr_data[id].Resize(refactored_metadata.metadata[id], 0); device_subdomain_buffer[id].resize(domain_decomposer.subdomain_shape(id), @@ -120,8 +122,8 @@ void reconstruct_pipeline_qoi( } } - HierarchyType &hierarchy = - Cache::cache.GetHierarchyCache(domain_decomposer.subdomain_shape(0)); + HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( + domain_decomposer.subdomain_shape(0), config.max_larget_level); reconstructor.Adapt(hierarchy, config, 0); int current_buffer = 0; @@ -157,7 +159,8 @@ void reconstruct_pipeline_qoi( (current_buffer + 1) % domain_decomposer.num_subdomains(); int next_queue = (current_queue + 1) % domain_decomposer.num_subdomains(); HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(curr_subdomain_id)); + domain_decomposer.subdomain_shape(curr_subdomain_id), + config.max_larget_level); log::info("Adapt Refactor to hierarchy"); reconstructor.Adapt(hierarchy, config, current_queue); if (iter == 1) diff --git a/include/mgard-x/MDRHighLevel/RefactorPipeline.hpp b/include/mgard-x/MDRHighLevel/RefactorPipeline.hpp index ae7de5a18..68abc0ab7 100644 --- a/include/mgard-x/MDRHighLevel/RefactorPipeline.hpp +++ b/include/mgard-x/MDRHighLevel/RefactorPipeline.hpp @@ -29,7 +29,8 @@ void refactor_pipeline( MDRData *mdr_data = Cache::cache.mdr_data; if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(0), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.ClearHierarchyCache(); } @@ -37,14 +38,15 @@ void refactor_pipeline( for (SIZE id = 0; id < domain_decomposer.num_subdomains(); id++) { if (!Cache::cache.InHierarchyCache(domain_decomposer.subdomain_shape(id), - domain_decomposer.uniform)) { + domain_decomposer.uniform, + config.max_larget_level)) { Cache::cache.InsertHierarchyCache( - domain_decomposer.subdomain_hierarchy(id)); + domain_decomposer.subdomain_hierarchy(id), config.max_larget_level); } } - HierarchyType &hierarchy = - Cache::cache.GetHierarchyCache(domain_decomposer.subdomain_shape(0)); + HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( + domain_decomposer.subdomain_shape(0), config.max_larget_level); log::info("Adjust device buffers"); refactor.Adapt(hierarchy, config, 0); @@ -71,7 +73,8 @@ void refactor_pipeline( int next_buffer = (current_buffer + 1) % 2; int next_queue = (current_queue + 1) % 3; HierarchyType &hierarchy = Cache::cache.GetHierarchyCache( - domain_decomposer.subdomain_shape(curr_subdomain_id)); + domain_decomposer.subdomain_shape(curr_subdomain_id), + config.max_larget_level); log::info("Adapt Refactor to hierarchy"); refactor.Adapt(hierarchy, config, current_queue); total_size += hierarchy.total_num_elems() * sizeof(T); diff --git a/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in b/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in index 2f6ed6e49..42bf2126f 100644 --- a/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in +++ b/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in @@ -119,4 +119,35 @@ TEST_CASE( REQUIRE(round_trip_error(data, writer, reader) <= kTol * kSlack); } +TEST_CASE("the hierarchy cache tells depth caps apart", + "[mgard-x][@DEVICE_TYPE@][LevelCapRoundTrip]") { + // Keyed on the shape alone, the cache handed a compress (or decompress) + // the hierarchy left behind by the previous call for the same shape, cap + // or no cap. Within one process that is self-consistent, so it only shows + // in the output size: the same input and Config must compress to the same + // number of bytes whether or not the other cap's hierarchy is cached. + std::vector data = smooth_field(); + Config uncapped = device_config(); + Config capped = device_config(); + capped.max_larget_level = 1; + + // Reference sizes, each from a cache that holds nothing for this shape. + release_cache(uncapped); + size_t uncapped_size = compress_with(data, uncapped).size(); + release_cache(uncapped); + size_t capped_size = compress_with(data, capped).size(); + REQUIRE(capped_size != uncapped_size); + + // Alternate without releasing: each call must get its own cap's hierarchy. + REQUIRE(compress_with(data, uncapped).size() == uncapped_size); + REQUIRE(compress_with(data, capped).size() == capped_size); + REQUIRE(compress_with(data, uncapped).size() == uncapped_size); + + // And a decompress right after a compress with the other cap. + std::vector capped_stream = compress_with(data, capped); + compress_with(data, uncapped); + REQUIRE(decompress_error(capped_stream, data, device_config()) <= + kTol * kSlack); +} + } // namespace mgard_x