fix: allow weights_only=False when loading --cache-dataset torch cache - #31
Conversation
Both cache-hit branches in load_data() (training and validation/test data) called torch.load(cache_path) with no weights_only argument. The cached object is a (dataset, datadir) tuple where dataset is one of this project's own Dataset subclasses -- not a type PyTorch 2.6+'s new weights_only=True default allowlists. Any run that reused a --cache-dataset cache written by an earlier run failed with: _pickle.UnpicklingError: Weights only load failed... WeightsUnpickler error: Unsupported global: GLOBAL <DatasetClass> was not an allowed global by default... This is not a security fix: cache_path is derived from a hash of datadir and is a purely local cache this same process wrote moments earlier, never fetched from a URL or otherwise externally supplied. Explicit weights_only=False is the correct, safe choice here, unlike genuinely untrusted/external checkpoint loads elsewhere in the codebase which should keep the safe default or use safe_globals(). Adds a regression test that reproduces the crash end-to-end through load_data() itself (not just a direct torch.load call), verified to fail pre-fix with the exact UnpicklingError above and pass post-fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An independent peer review of this PR's weights_only fix caught a deeper,
more serious pre-existing bug it had unblocked: _get_cache_path(datadir)
computed the SAME cache path for the training dataset and the validation
dataset (both derived only from datadir), and only the training branch
ever wrote its cache -- the validation write-back was dead code,
commented out with "TODO: Add utils and uncomment the if block".
Consequence, verified by reproduction: on the very first run with
--cache-dataset enabled, load_data() writes the training dataset to
cache_path, then immediately re-derives the identical cache_path for
validation and finds it already exists (since it was just written) --
so dataset_test silently becomes a torch.load() of the TRAINING dataset.
Not a crash: --cache-dataset appeared to work, but every validation
metric from that point on was computed against training data instead of
held-out data, at every run using this now-crash-free flag.
Fixed by:
- Giving _get_cache_path a `tag` parameter ('train'/'val') folded into
the hash, so the two datasets get distinct cache paths.
- Enabling the validation cache write-back, which was dead code -- without
it, fixing only the path collision would mean the validation dataset
simply never got cached (safe, but silently defeats the point of
--cache-dataset for validation).
Rewrites tests/test_cache_dataset_weights_only.py: the previous test
patched _get_cache_path with a single fixed return_value for both the
training and validation calls, which (ironically) meant it would have
passed even under this exact bug -- both calls resolving to the same
path is precisely the defect. New tests patch _get_cache_path with a
tag-aware side_effect and assert the training and validation datasets
come back distinct, and that a missing validation cache actually gets
written. Verified all three fail on the pre-this-commit code with the
exact symptom (dataset_test.tag == 'cached-train' instead of the
expected validation value) and pass post-fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
An independent peer review of this PR's weights_only fix caught a deeper, pre-existing bug it had unblocked (previously masked by the crash). Pushed a follow-up commit (de8ec33):
Consequence, confirmed by reproduction: on the very first run with Fixed by giving Also rewrote the test file: the previous test patched |
Two major CodeRabbit findings on this PR's cache handling, both extending pre-existing gaps the earlier reviews had also noted: 1. _get_cache_path keyed only on tag+datadir, but the cached object is the fully PREPARED dataset -- the code's own comment warns "Attention, as the transforms are also cached!". A run changing transforms, loader, frame size, etc. against the same datadir silently reused a dataset prepared under the OLD configuration. Now a documented tuple of dataset-shaping args (_CACHE_KEY_ARGS: loader, annotation, transform, and framing settings) is folded into the hash via getattr-with-None-default reprs -- stable for equivalent configs, different for any change. Deliberately not exhaustive (an exotic loader could consume an arg not listed; extend as needed) and biased toward over-invalidation, which costs a cache miss, never wrong data. Side effect: existing caches are invalidated once more (key changes); they were already invalidated by the tag change in the previous commit, so no additional migration impact. 2. Cache writes used a plain torch.save straight to the final path -- another rank (or a later run, after a mid-save kill) could see the file exist and read it partially written. New _save_cache_atomically helper writes to a .tmp sibling and publishes via os.replace (atomic on POSIX within a filesystem); both write sites now use it, wrapped in is_main_process() to preserve save_on_master's rank-0-only semantics. New tests pin both: config change produces a different key (same config produces the same key), and the atomic helper leaves no .tmp file and a loadable cache. Both verified to fail pre-fix and pass post-fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Problem
load_data()intinyml-tinyverse/tinyml_tinyverse/common/utils/utils.pyhas two cache-hit branches (training dataset and validation/test dataset) that calltorch.load(cache_path)with noweights_onlyargument. The cached object is a(dataset, datadir)tuple wheredatasetis one of this project's ownDatasetsubclasses.PyTorch 2.6+ changed
torch.load()'s default toweights_only=True, which only allowlists a small set of known-safe types. Any run that reuses a--cache-datasetcache written by an earlier run now crashes with:i.e. the
--cache-datasetspeedup flag is currently broken on PyTorch 2.6+.Fix
Pass
weights_only=Falseexplicitly at both call sites. This is not a security fix —cache_pathis derived from a hash ofdatadirand is a purely local cache this same process wrote moments earlier; it is never fetched from a URL or otherwise externally supplied. This is a different situation from genuinely untrusted/externally-sourced checkpoint loads elsewhere in the codebase, which should keep the safe default or usetorch.serialization.safe_globals([...]).Testing
Added
tinyml-tinyverse/tests/test_cache_dataset_weights_only.py, which reproduces the crash end-to-end throughload_data()itself (writes a fake cached dataset viatorch.save, then callsload_data()against it), not just a directtorch.loadcall.UnpicklingErrorabove (checked viagit stash).Merge safety
This touches the same file (
utils.py) as three other currently-open PRs (pr/mps-eval-fixes,pr/compile-hardening,pr/hardware-defaults). Verified via a disposable worktree that this branch merges cleanly on top of all three with no conflicts (git merge --no-ffof all three followed by this branch resolves via auto-merge, clean working tree).🤖 Generated with Claude Code