Declare only the sampler stats the settings store - #75
Conversation
The zarr writer creates an array for every declared stat. Stats switched off by store_gradient / store_unconstrained / store_transformed are never written, so their arrays stay at the NaN fill value - but the four that are draw-dimensioned (gradient, unconstrained_draw, transformed_position, transformed_gradient) are allocated at full width, and while they cost almost nothing in the store (all-NaN compresses), anything reading the trace materializes them: on a 40-parameter, 200-draw, 2-chain fit they are 0.5MB of a 1.4MB netCDF conversion. Filter them out where the settings and the stat names meet, in the Settings::stat_names default method, so stat_types/stat_dims_all and every storage backend inherit it and cannot drift from what the sampler actually emits. disabled_stats() defaults to empty, so implementors outside this crate are unaffected. The divergence- and mass-matrix-dimensioned stats need no filtering: their primary dimension is already zero-length when they are off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I think this also requires a change when we iterate over the stats, to skip the unexpected ones. And maybe this would be a great opportunity to also fix pymc-devs/nutpie#315? |
#315 definitely seems like something this should handle. |
Filtering the declared stat names in the previous commit was only half of it:
the chain still emits every stat, so a stat switched off by store_gradient /
store_unconstrained / store_transformed reached the backends undeclared. The
arrow backend zips incoming values against its declared columns and panics
("Draw name mismatch"), which with the all-false defaults means any default
arrow run - its own store_warmup test included.
Filter both stats and expanded draws against the declared names at the single
point where the sampler hands them to storage, so backends receive exactly what
they allocated for.
That also makes under-declaring posterior variables safe, which is what
nutpie#315 needs: a Math whose expanded vector declares fewer names than it
produces values for - a model restricted to var_names - now has the rest
dropped instead of panicking in the zarr backend ("Unknown posterior variable
name"). nutpie can keep its full variable list for slicing the expand buffer
and simply leave the unwanted ones out of Storable::names.
Tested through arrow (positional, catches shifts) and zarr (name-keyed, what
nutpie uses); both tests fail without the filter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both done in 62ee78f. On skipping the unexpected stats when we iterate. You were right, and it was worse than a loose end: the previous commit changed what the backends allocate but not what the chain emits, so a stat switched off by Since the three The fix, for both halves at once: filter the values against the declared names at the single point where the sampler hands them to storage, for stats and for expanded draws alike. Backends then receive exactly what they allocated for, and On #315. That falls out of the same change. The obstacle was not that a model could not express "do not store this" — So nutpie can keep its full Testing. Two regression tests, arrow (positional, catches column shifts) and zarr (name-keyed, and what nutpie uses), driven by a test model that expands two variables and declares one. Both fail without the filter, with the two panics quoted above. 45 unit + 4 integration + 2 doctests green under Still outstanding from the description: I have not built nutpie against a patched nuts-rs, so the end-to-end effect on a real trace is still unverified. And the |
The test model emitted the declared name first, so arrow's positional zip paired it with its one builder and stopped before reaching the undeclared one - the assertion held with or without the draw filter, and only the stat filter was really under test. Emit the undeclared value first. Also reword two doc comments that credited the dropping to stat_names / data_names rather than to the sampler, and trim them to the 1-2 lines the repo uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Correction to my previous comment: the CI suggestion in it was wrong, and I should have checked before making it. CI had in fact already caught this — 847b9e2 is sitting red on this PR ( Two follow-up commits since:
Two things I checked while re-reviewing, both worth separating from this PR: The commit-message claim about the other six stats is correct. I measured it rather than trusting it: with default settings the event-dimensioned stats are But there is a real, pre-existing data-loss bug next door, which I found while checking the above and can send separately if you want it. Taking the max over the fields of a dimension instead of a representative would fix it; note the lottery persists even with Happy to fold that into this PR or open a separate one — your call. I have also not squashed 847b9e2 into 62ee78f, which would take CI green across the whole branch and let git-cliff pick the change up (both subjects are non-conventional, so as it stands the changelog would not mention that four |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #75 +/- ##
===========================================
- Coverage 83.77% 67.29% -16.48%
===========================================
Files 8 34 +26
Lines 1923 9580 +7657
===========================================
+ Hits 1611 6447 +4836
- Misses 312 3133 +2821 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Codecov flagged 17 uncovered lines on this PR. Three of them were the default body of `disabled_stats`: `Settings` is sealed, all six implementors override it, so the default was unreachable. Making the method required also turns a future settings type that forgets to filter into a compile error rather than a silent full-width stat. Eight more were the `FlowNutsSettings` / `FlowMclmcSettings` impls, which `all_settings_smoke` skips because it cannot build a flow chain from the test math. `disabled_stats` needs no `Math`, so it can be asserted directly for all six, which also pins the store_* mapping in one place. The zarr test discarded its trace binding, so `assert!(matches!(..))` says the same thing more directly. Patch coverage 90.17% -> 99.43%; the one line left is arrow's `panic!` arm on a failed sample, the idiom the neighbouring tests use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What / why
Follow-up to pymc-devs/nutpie#335, where
@aseyboldt pointed out that this belongs here rather than in nutpie's Python layer, so that a
zarr store loaded directly — without going through nutpie — is already in the right shape. He
also expected the disabled
store_*stats to cost nothing. That's mostly true, and the partwhere it isn't is what this changes.
The zarr writer creates an array for every declared stat. A stat switched off by
store_gradient/store_unconstrained/store_transformedis never written, so its arraysimply stays at the NaN fill value.
Six of the ten are genuinely free: their primary dimension is divergence- or
mass-matrix-shaped, which is already zero-length when the corresponding option is off. They
need no filtering and this PR does not touch them.
The other four —
gradient,unconstrained_draw,transformed_position,transformed_gradient— are draw-dimensioned and get allocated at full width. In the storethat is nearly free, since all-NaN compresses to about a kilobyte. The cost lands on every
reader: xarray materializes them at full width on load and again on conversion. On a
40-parameter, 200-draw, 2-chain fit they are 0.5 MB of a 1.4 MB netCDF conversion, and the
ratio grows with the parameter count.
The change
Filter the disabled stats where the settings and the stat names meet — in the
Settings::stat_namesdefault method — so that
stat_types,stat_dims_alland every storage backend inherit thefiltering and cannot drift from what the sampler actually emits:
A
disabled_point_statshelper maps the threestore_*flags onto the names they suppress,mirroring
TransformedPoint::extract_statsso the two lists can be read against each other,and the six
Settingsimplementors overridedisabled_stats()with it.Compatibility
disabled_stats()has a default returning an emptyVec, so the trait gains a method withoutbreaking implementors outside this crate — their behaviour is unchanged unless they opt in.
Nothing changes for a run with all
store_*options on.Testing
cargo checkclean; the existing suite passes (41 unit tests + 2 doctests).The 0.5 MB / 1.4 MB figure above was measured from the Python side, on a trace that still
carries the four arrays — i.e. it measures what they cost a reader, not the effect of this
patch. I have not yet built nutpie against a patched nuts-rs to confirm the arrays disappear
end-to-end; if that matters for review I can do it.
Note on scope
This covers the
store_*half of nutpie#335. Thesave_warmuphalf and thechain/drawcoordinate variables are separate and not in this PR — happy to follow up with those here too if
that's where you'd like them, which I read your comment on #335 as suggesting.