cache: reuse smoothing results via an input+params+basis hash (the DM GP runs once, not twice) - #2
Merged
Merged
Conversation
A four-mode figure that runs spatial, dm and blend recomputed the expensive diffusion-map GP (and the spatial smooth) twice -- once for their own mode, once inside blend's branches. Memoize each pipeline step's output on the AnnData, keyed by a sha256 of its input matrix bytes, canonical parameters, and basis bytes, and reuse it on a hit. blend's two branches reproduce the spatial and dm steps verbatim, so the GP now runs once total. Matrices are stored as full-width layers (_sscache_<hash>), the tiny hash->layer index as a JSON blob in uns[spatial_smooth_cache]; both serialize with the object. Guarded: an LRU cap (cache_max_entries), a cache=False opt-out, and clear_smooth_cache(adata). A hit replays the exact float64 matrix and its resolved params, so every downstream output -- score, raw, provenance -- is byte-identical to a cache-off run. Invalidation is by construction: any input/param/basis change yields a new hash and a miss. Adds tests/test_cache.py (hit, blend-GP-runs-once + byte-identical, miss on change, layer-not-obs storage, opt-out/clear/LRU guards, h5ad round-trip).
katosh
added a commit
that referenced
this pull request
Jul 21, 2026
Since v0.1.0 the package gained three user-facing features -- `blend` composition mode (#1), the smoothing reuse cache (#2), and smooth-all-once `all_genes=True` (#3) -- so a MINOR bump to 0.2.0 is the correct SemVer step. - pyproject.toml + src/spatial_smooth/__init__.py: version 0.1.0 -> 0.2.0 - docs/source/installation.rst: the illustrative check_dependencies() output shows v0.2.0 (it prints v{__version__}) - CHANGELOG.md: new; 0.2.0 (the three features + the tutorial-now-executed and PyPI-image fixes) and a 0.1.0 baseline entry conf.py already tracks __version__, so the docs title/version follow automatically. twine check passes on the 0.2.0 sdist+wheel; the full test suite is green.
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.
The redundancy this fixes
A four-mode figure that renders
spatial,dmandblendcomputes each smoothing twice.blendruns a spatial branch and a cell-state branch independently on the raw expression, and those branches reproduce the exact same computation the standalonespatialanddmmodes already did. So the expensive diffusion-map Gaussian process (and the spatial smooth) each ran once for their own mode and once again insideblend.This PR memoizes each smoother so the identical computation is served from a cache instead of recomputed — the DM GP now runs once total across all three modes.
The design — a hash-keyed, per-step cache
A pipeline step is a pure function of three things, so the cache key (
sha256) is over exactly those:np.ascontiguousarray(matrix).tobytes()+ shape + dtype (the exact stored floats, never a re-derived or rounded copy);type(step).__name__+ its dataclass fields, sorted); andadata.obsm[step.basis](spatial coords / diffusion map).The cache lives at the per-step dispatch (
_run_pipeline), through which both thedm/spatialmodes andblend's two branches route — so caching there fixes all of them at once. Becausespatialandblend's left branch produce the identical(raw genes, step, basis)key (same fordmandblend's right branch),blend's branches hit the cache.Invalidation is by construction: change the input, a parameter, or the embedding and the hash changes → miss → recompute. A stale result can never be served.
Storage — matrices in
layers, index inunsadata.layers["_sscache_<hash>"]. A layer must be(n_obs, n_vars), so the signature columns are scattered into a full-widthfloat64layer (restNaN) and a hit gathers exactly those columns back — bit for bit,float64kept, so a replay is byte-identical to a fresh compute.adata.uns["spatial_smooth_cache"]holds only the index (a JSON blob:hash → layer-key, the gene columns, the param signature, and the resolved params), never a matrix. JSON mirrors the existingsteps_jsonprecedent, so it round-trips through.h5adcleanly.The namespaced
_sscache_prefix makes cache artifacts obvious and trivially clearable.The API — on by default, fully guarded
The operator asked for on-
adatacaching, so it is on by default, with the serialization/bloat tradeoff guarded three ways:smooth(..., cache=False)— neither read nor write the cache.smooth(..., cache_max_entries=N)— LRU cap (defaultSMOOTH_CACHE_MAX_ENTRIES = 64) bounding the.h5adgrowth caching introduces.ss.clear_smooth_cache(adata)— drops every_sscache_layer and theunsindex, leaving your stored results (obs/obsm/uns["spatial_smooth"]) untouched.A cache hit short-cuts the smoother only — the score,
raw, and provenance are still written normally, and are byte-identical to a cache-off run.Tests (
tests/test_cache.py)smooth(...)is a genuine hit — the smoother is not re-run (asserted via a call-counter on.apply) and the result is identical.blendafterspatial+dmreuses both branches — the DM GP runs exactly once across all three modes, andblend's output is byte-identical to the same three modes run withcache=False.Plus: the cached artifact is a layer (not obs) of full
(n_obs, n_vars)shape with the index a JSON string inuns; normal outputs and provenance are byte-identical cache-on vs off; the opt-out,clear_smooth_cache, and LRU-eviction guards; and an.h5adround-trip after which a reloaded object still hits.The full existing suite stays green (no regressions), including the doctest pass and the kompot-backed GP job.
Notes
#284notebook that motivated this needs ~zero change: each smoothing now runs once automatically.