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/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/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/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/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/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/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..42bf2126f
--- /dev/null
+++ b/tests/mgard-x/src/test_LevelCapRoundTrip.cpp.in
@@ -0,0 +1,153 @@
+/*
+ * 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);
+}
+
+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