Repository navigation
MGARD-X: record the decomposition depth cap in the header and restore it on decompression - #263
Closed
gqian-coder wants to merge 2 commits into
Closed
gqian-coder wants to merge 2 commits into
gqian-coder wants to merge 2 commits into
Conversation
… 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Config::max_larget_levelcaps the number of decomposition levels (Hierarchy::initusesl_target = min(natural depth, cap)), but nothing about it reaches the file. The header'sl_targetis serialized from a member that is never set (always 0, "written for format compatibility only") andMetadataBase::InitializeConfignever touchesconfig.max_larget_level. A decompressor that only has the file (mgard-x -x, or the API with a defaultConfig) therefore rebuilds the hierarchy at full depth. The per-level quantization step (2 tol / (l_target + 1)) and the number of recomposition passes both depend on the depth, so the output is wrong, with no error.Measured on a smooth 65^3 float field (range about 3), L-inf ABS
tol = 1e-3, compressed withmax_larget_level = 1: max error 1.35 under a defaultConfig(0.97e-3 when the caller re-supplies the cap). The hybrid (BlockMGARD) path was not affected: its local/global level counts already travel in the header.A second, related defect hid the first one in tests: the hierarchy caches (
CompressorCache,RefactorCache,ReconstructorCache) are keyed on the shape alone, so a same-process compress + decompress hands the decoder the encoder's hierarchy, and a later compress of a same-shape array silently inherits whatever cap the previous call used.Changes
mgard.proto: newLevelCap { bool unlimited; uint64 max_level; }sub-message,FunctionDecomposition.level_cap = 5. A sub-message carries presence, which keeps three cases apart: absent (stream predates the field), cap 0 (single level) and unlimited.L_targetis left as it was.Metadata.hpp/Metadata.cpp:max_larget_level_recorded+max_larget_levelonMetadataBase; serialized when recorded, deserialized with the presence check (and reset when absent), printed byPrintSummary.InitializeConfigsetsconfig.max_larget_levelonly when the header recorded it.CompressionHighLevel.hpp(compress) andMDRHighLevel.hpp(MDRefactor, andload()on the request/reconstruct side): record and restore the cap. Writers always record it, "unlimited" included, so a stale cap in the reader'sConfigcannot leak in either.GPUPipelines,CPUPipelines,RefactorPipeline,ReconstructPipeline,ReconstructPipelineQoIpassconfig.max_larget_level.test_LevelCapMetadata(header round trip: cap, cap 0, unlimited overriding a stale cap, absent field leaving the caller'sConfigalone, reset on re-deserialize) andtest_LevelCapRoundTrip(end-to-end through the public API with a readerConfigthat never saw the writer's parameters; cache test through deterministic compressed sizes).doc/MGARD-X.md: themax_larget_levelbullet claimed a default of 0; it now states the real default and that the cap is persisted.Compatibility
level_cap. The reader then keeps the caller'sConfig(unlimited by default), i.e. exactly the previous behaviour: uncapped streams decode correctly, capped ones still need the cap supplied throughConfig, as nothing in such a stream records it.Results
65^3 float, range about 3, L-inf ABS
tol = 1e-3. "default" = readerConfigwith no cap set (whatmgard-x -xuses).mgard-x -xConfigCache: in one process, compress with cap 1 (228742 B), then with a default
Config: 250878 B, equal to the size afterrelease_cache(). Before the change the second call returned 228738 B, the capped hierarchy.Testing
mgard-x-tests-SERIALandmgard-x-tests-CUDA: all 237 test cases pass (1,148,625 assertions each), including the 9 new ones.mgard-xCLI.MDRefactorand restored byload(); the MDR test suites (which run uncapped) pass, but a capped refactor/reconstruct round trip was not exercised beyond that.🤖 Generated with Claude Code