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