pymc-cache 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymc_cache-0.1.0/.gitignore +19 -0
- pymc_cache-0.1.0/AGENTS.md +3 -0
- pymc_cache-0.1.0/CHANGELOG.md +11 -0
- pymc_cache-0.1.0/CONTRIBUTING.md +3 -0
- pymc_cache-0.1.0/LICENSE +21 -0
- pymc_cache-0.1.0/PKG-INFO +70 -0
- pymc_cache-0.1.0/README.md +41 -0
- pymc_cache-0.1.0/SECURITY.md +3 -0
- pymc_cache-0.1.0/_quarto.yml +51 -0
- pymc_cache-0.1.0/api/CacheBackend.qmd +7 -0
- pymc_cache-0.1.0/api/CacheEntry.qmd +16 -0
- pymc_cache-0.1.0/api/CacheLookup.qmd +21 -0
- pymc_cache-0.1.0/api/CachePolicy.qmd +7 -0
- pymc_cache-0.1.0/api/CacheSession.qmd +51 -0
- pymc_cache-0.1.0/api/CacheWarning.qmd +7 -0
- pymc_cache-0.1.0/api/DiskCache.qmd +52 -0
- pymc_cache-0.1.0/api/MemoryCache.qmd +21 -0
- pymc_cache-0.1.0/api/UncacheableError.qmd +7 -0
- pymc_cache-0.1.0/api/cache_key.qmd +15 -0
- pymc_cache-0.1.0/api/cleanup_cache.qmd +7 -0
- pymc_cache-0.1.0/api/index.qmd +20 -0
- pymc_cache-0.1.0/api/model_digest.qmd +10 -0
- pymc_cache-0.1.0/api/sample.qmd +21 -0
- pymc_cache-0.1.0/api/sample_smc.qmd +16 -0
- pymc_cache-0.1.0/contributing.qmd +5 -0
- pymc_cache-0.1.0/custom.scss +2 -0
- pymc_cache-0.1.0/examples/basic.py +26 -0
- pymc_cache-0.1.0/examples/custom_backend.py +36 -0
- pymc_cache-0.1.0/examples/monitor_composition.py +48 -0
- pymc_cache-0.1.0/examples/preflight.py +26 -0
- pymc_cache-0.1.0/getting-started.qmd +38 -0
- pymc_cache-0.1.0/guides/identity.qmd +28 -0
- pymc_cache-0.1.0/guides/migration.qmd +7 -0
- pymc_cache-0.1.0/guides/monitor.qmd +7 -0
- pymc_cache-0.1.0/guides/storage.qmd +16 -0
- pymc_cache-0.1.0/guides/troubleshooting.qmd +11 -0
- pymc_cache-0.1.0/guides/walkthrough.qmd +11 -0
- pymc_cache-0.1.0/index.qmd +12 -0
- pymc_cache-0.1.0/pyproject.toml +63 -0
- pymc_cache-0.1.0/reference.qmd +18 -0
- pymc_cache-0.1.0/releasing.qmd +11 -0
- pymc_cache-0.1.0/scripts/build-docs.sh +8 -0
- pymc_cache-0.1.0/scripts/check_artifact.py +67 -0
- pymc_cache-0.1.0/scripts/check_docs.py +51 -0
- pymc_cache-0.1.0/scripts/check_examples.py +26 -0
- pymc_cache-0.1.0/scripts/lock.sh +5 -0
- pymc_cache-0.1.0/scripts/smoke_test.py +41 -0
- pymc_cache-0.1.0/src/pymc_cache/__init__.py +13 -0
- pymc_cache-0.1.0/src/pymc_cache/_version.py +1 -0
- pymc_cache-0.1.0/src/pymc_cache/api.py +127 -0
- pymc_cache-0.1.0/src/pymc_cache/backends.py +262 -0
- pymc_cache-0.1.0/src/pymc_cache/cli.py +50 -0
- pymc_cache-0.1.0/src/pymc_cache/identity.py +246 -0
- pymc_cache-0.1.0/src/pymc_cache/io.py +269 -0
- pymc_cache-0.1.0/src/pymc_cache/naming.py +28 -0
- pymc_cache-0.1.0/src/pymc_cache/policy.py +65 -0
- pymc_cache-0.1.0/src/pymc_cache/py.typed +0 -0
- pymc_cache-0.1.0/src/pymc_cache/session.py +146 -0
- pymc_cache-0.1.0/tests/test_cache.py +365 -0
- pymc_cache-0.1.0/tests/test_identity.py +59 -0
- pymc_cache-0.1.0/tests/test_public_contract.py +94 -0
- pymc_cache-0.1.0/tests/test_safety.py +185 -0
- pymc_cache-0.1.0/tests/test_text_codec.py +242 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Repository guidance
|
|
2
|
+
|
|
3
|
+
This repository owns cache identity, storage, and policy only. It must not depend on cloudposterior, pymc-monitor or Modal. Keep `.extraction-source/` temporary, ignored and out of all artifacts. Follow `CONTRIBUTING.md`; preserve old external cache data. Never silently reuse an entry when a model or sampler cannot be identified safely.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- Extract independent PyMC model-aware sampling cache from cloudposterior.
|
|
6
|
+
- Add versioned, fail-closed graph and call identity; bounded memory and atomic NetCDF disk backends.
|
|
7
|
+
- Add a preflight session API, safe disk-management CLI, and PyMC 5/6 and ArviZ 0/1 support.
|
|
8
|
+
- Encode nutpie divergence messages losslessly; support nullable diagnostic warning text and floating NaN without dropping data.
|
|
9
|
+
- Pre-release identity and disk schema 3 invalidate schema-1/2 entries; old files are misses, left untouched and never overwritten. Existing caches must be sampled again.
|
|
10
|
+
|
|
11
|
+
Old cloudposterior keys are deliberately not trusted or imported; old files are not deleted.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Install [uv](https://docs.astral.sh/uv/) and Quarto. Run `bash scripts/lock.sh`, `uv run --frozen pytest -q --cov=pymc_cache --cov-fail-under=85`, `uv run --frozen ruff check src tests scripts examples`, `bash scripts/build-docs.sh`, `uv build --no-sources`, and `uv run --frozen python scripts/check_artifact.py`. `scripts/lock.sh` checks the lock without user configuration before a frozen run; update the lock intentionally with `uv lock --no-config` when dependencies change. Test both supported PyMC/ArviZ majors before proposing a release. Make one conventional commit per logical change; discuss new cache formats and identity semantics before changing them. Do not check in posterior data.
|
pymc_cache-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spencer Boucher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pymc-cache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe, model-aware caching of PyMC sampling results
|
|
5
|
+
Project-URL: Homepage, https://github.com/justmytwospence/pymc-cache
|
|
6
|
+
Project-URL: Documentation, https://justmytwospence.github.io/pymc-cache/
|
|
7
|
+
Project-URL: Repository, https://github.com/justmytwospence/pymc-cache
|
|
8
|
+
Project-URL: Issues, https://github.com/justmytwospence/pymc-cache/issues
|
|
9
|
+
Author: Spencer Boucher
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: bayesian,cache,inferencedata,mcmc,pymc
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: arviz<2,>=0.17
|
|
22
|
+
Requires-Dist: h5netcdf>=1.3
|
|
23
|
+
Requires-Dist: h5py>=3.10
|
|
24
|
+
Requires-Dist: numpy>=1.25
|
|
25
|
+
Requires-Dist: pymc<7,>=5.28
|
|
26
|
+
Requires-Dist: pytensor>=2.38
|
|
27
|
+
Requires-Dist: xarray>=2023.1
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# pymc-cache
|
|
31
|
+
|
|
32
|
+
Safe, model-aware caching of native PyMC sampling results. Python 3.11+, PyMC 5.28–6 and ArviZ 0.x–1.x. No cloud account, monitoring service or Modal dependency.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install 'pymc-cache @ git+https://github.com/justmytwospence/pymc-cache.git@<commit-sha>'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import pymc as pm
|
|
40
|
+
import pymc_cache as pc
|
|
41
|
+
|
|
42
|
+
with pm.Model() as model:
|
|
43
|
+
mu = pm.Normal("mu", 0, 1)
|
|
44
|
+
options = dict(draws=10, tune=10, chains=1, cores=1, random_seed=42,
|
|
45
|
+
progressbar=False, model=model, cache="disk")
|
|
46
|
+
trace = pc.sample(**options)
|
|
47
|
+
# The next call loads the result from .pymc-cache/ without sampling:
|
|
48
|
+
again = pc.sample(**options)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`pc.sample_smc(model=model, draws=100, cache="disk")` uses the native SMC sampler with an independent key. `cache=True` (default) uses an 8-entry in-memory LRU. `cache=False` runs the native sampler without hashing the model. Pass a directory/Path for a custom disk location; `overwrite=True` skips reads and atomically replaces an existing entry. Both backends copy results on read; memory copies on write too. Disk files are one NetCDF file per full SHA-256 key, with the key embedded in its metadata. Corruption is treated as a miss. `pc.cleanup_cache()` removes only entries owned by this package; `pymc-cache list|prune|clear --dir PATH` inspects/manages the same entries without following symlinks.
|
|
52
|
+
|
|
53
|
+
## Preflight and integrations
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
session = pc.CacheSession(cache="disk", model=model)
|
|
57
|
+
lookup = session.lookup({"draws": 10, "tune": 10, "chains": 1,
|
|
58
|
+
"cores": 1, "random_seed": 42, "progressbar": False})
|
|
59
|
+
if lookup.hit:
|
|
60
|
+
trace = lookup.result
|
|
61
|
+
else:
|
|
62
|
+
trace = pm.sample(model=model, **lookup.normalized_kwargs)
|
|
63
|
+
session.store(lookup, trace, status="complete")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A runner used with `pc.sample(..., sample_fn=runner)` must implement `runner.__pymc_cache_normalize__(model, call_kwargs)` and return **exactly** `call_kwargs`, `key_kwargs`, `sampler_identity`, and `versions`. The call arguments passed to the runner and the canonical key arguments must be explicit. The runner returns an inference result; only complete results, or adaptive results with an attested `met` convergence report, are stored. Stopped, futile, capped and incomplete results are returned but never stored. Cache identity includes the complete model graph, constants, mutable shared data, deterministics, coordinates, sampling kwargs, RNG states, operation, sampler and software versions. Unknown/custom Ops fail closed rather than risk a wrong hit. Unsafe steps/callbacks/callables require an explicit `sampler_key` identifying their implementation and configuration; the caller is responsible for changing this key when the callable changes. No code is executed from cache metadata.
|
|
67
|
+
|
|
68
|
+
Caching may hold sensitive posterior data. Treat `.pymc-cache/` as private, and use `cache=False` for confidential models when persistent data is not allowed. Cache files are not authenticated against a malicious writer; do not use a shared untrusted directory.
|
|
69
|
+
|
|
70
|
+
See [getting started](getting-started.qmd), [caching walkthrough](guides/walkthrough.qmd), [identity guide](guides/identity.qmd), [storage guide](guides/storage.qmd), [monitor composition](guides/monitor.qmd), [legacy migration](guides/migration.qmd), [troubleshooting](guides/troubleshooting.qmd), [runnable examples](examples/basic.py), and [API reference](reference.qmd). This package does not patch `pm.sample`, intercept model contexts, cache posterior predictive calls or provide cloud execution.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# pymc-cache
|
|
2
|
+
|
|
3
|
+
Safe, model-aware caching of native PyMC sampling results. Python 3.11+, PyMC 5.28–6 and ArviZ 0.x–1.x. No cloud account, monitoring service or Modal dependency.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install 'pymc-cache @ git+https://github.com/justmytwospence/pymc-cache.git@<commit-sha>'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import pymc as pm
|
|
11
|
+
import pymc_cache as pc
|
|
12
|
+
|
|
13
|
+
with pm.Model() as model:
|
|
14
|
+
mu = pm.Normal("mu", 0, 1)
|
|
15
|
+
options = dict(draws=10, tune=10, chains=1, cores=1, random_seed=42,
|
|
16
|
+
progressbar=False, model=model, cache="disk")
|
|
17
|
+
trace = pc.sample(**options)
|
|
18
|
+
# The next call loads the result from .pymc-cache/ without sampling:
|
|
19
|
+
again = pc.sample(**options)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`pc.sample_smc(model=model, draws=100, cache="disk")` uses the native SMC sampler with an independent key. `cache=True` (default) uses an 8-entry in-memory LRU. `cache=False` runs the native sampler without hashing the model. Pass a directory/Path for a custom disk location; `overwrite=True` skips reads and atomically replaces an existing entry. Both backends copy results on read; memory copies on write too. Disk files are one NetCDF file per full SHA-256 key, with the key embedded in its metadata. Corruption is treated as a miss. `pc.cleanup_cache()` removes only entries owned by this package; `pymc-cache list|prune|clear --dir PATH` inspects/manages the same entries without following symlinks.
|
|
23
|
+
|
|
24
|
+
## Preflight and integrations
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
session = pc.CacheSession(cache="disk", model=model)
|
|
28
|
+
lookup = session.lookup({"draws": 10, "tune": 10, "chains": 1,
|
|
29
|
+
"cores": 1, "random_seed": 42, "progressbar": False})
|
|
30
|
+
if lookup.hit:
|
|
31
|
+
trace = lookup.result
|
|
32
|
+
else:
|
|
33
|
+
trace = pm.sample(model=model, **lookup.normalized_kwargs)
|
|
34
|
+
session.store(lookup, trace, status="complete")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A runner used with `pc.sample(..., sample_fn=runner)` must implement `runner.__pymc_cache_normalize__(model, call_kwargs)` and return **exactly** `call_kwargs`, `key_kwargs`, `sampler_identity`, and `versions`. The call arguments passed to the runner and the canonical key arguments must be explicit. The runner returns an inference result; only complete results, or adaptive results with an attested `met` convergence report, are stored. Stopped, futile, capped and incomplete results are returned but never stored. Cache identity includes the complete model graph, constants, mutable shared data, deterministics, coordinates, sampling kwargs, RNG states, operation, sampler and software versions. Unknown/custom Ops fail closed rather than risk a wrong hit. Unsafe steps/callbacks/callables require an explicit `sampler_key` identifying their implementation and configuration; the caller is responsible for changing this key when the callable changes. No code is executed from cache metadata.
|
|
38
|
+
|
|
39
|
+
Caching may hold sensitive posterior data. Treat `.pymc-cache/` as private, and use `cache=False` for confidential models when persistent data is not allowed. Cache files are not authenticated against a malicious writer; do not use a shared untrusted directory.
|
|
40
|
+
|
|
41
|
+
See [getting started](getting-started.qmd), [caching walkthrough](guides/walkthrough.qmd), [identity guide](guides/identity.qmd), [storage guide](guides/storage.qmd), [monitor composition](guides/monitor.qmd), [legacy migration](guides/migration.qmd), [troubleshooting](guides/troubleshooting.qmd), [runnable examples](examples/basic.py), and [API reference](reference.qmd). This package does not patch `pm.sample`, intercept model contexts, cache posterior predictive calls or provide cloud execution.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Cache files contain model outputs and potentially sensitive observed data. Store them in private directories, restrict filesystem permissions, and do not point this package at an untrusted shared cache. A full SHA-256 key is an accidental-collision/identity guard, **not** a cryptographic authenticity check against a malicious writer. Unsupported custom PyTensor Ops require a caller-maintained `model_key`; unsupported sampler steps and callables require a caller-maintained `sampler_key`. Changing their behavior without changing those keys is unsafe. Report vulnerabilities privately through GitHub's private vulnerability reporting. Do not attach real model data or credentials.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
project:
|
|
2
|
+
type: website
|
|
3
|
+
output-dir: _site
|
|
4
|
+
render:
|
|
5
|
+
- index.qmd
|
|
6
|
+
- getting-started.qmd
|
|
7
|
+
- guides/*.qmd
|
|
8
|
+
- reference.qmd
|
|
9
|
+
- api/*.qmd
|
|
10
|
+
- contributing.qmd
|
|
11
|
+
- releasing.qmd
|
|
12
|
+
website:
|
|
13
|
+
title: pymc-cache
|
|
14
|
+
site-url: https://justmytwospence.github.io/pymc-cache/
|
|
15
|
+
repo-url: https://github.com/justmytwospence/pymc-cache
|
|
16
|
+
navbar:
|
|
17
|
+
left:
|
|
18
|
+
- href: getting-started.qmd
|
|
19
|
+
text: Get started
|
|
20
|
+
- href: guides/identity.qmd
|
|
21
|
+
text: Identity and safety
|
|
22
|
+
- href: guides/storage.qmd
|
|
23
|
+
text: Storage
|
|
24
|
+
- href: reference.qmd
|
|
25
|
+
text: API
|
|
26
|
+
quartodoc:
|
|
27
|
+
package: pymc_cache
|
|
28
|
+
dir: api
|
|
29
|
+
sections:
|
|
30
|
+
- title: Public API
|
|
31
|
+
contents:
|
|
32
|
+
- sample
|
|
33
|
+
- sample_smc
|
|
34
|
+
- CacheBackend
|
|
35
|
+
- MemoryCache
|
|
36
|
+
- DiskCache
|
|
37
|
+
- CacheSession
|
|
38
|
+
- CachePolicy
|
|
39
|
+
- CacheEntry
|
|
40
|
+
- CacheLookup
|
|
41
|
+
- UncacheableError
|
|
42
|
+
- CacheWarning
|
|
43
|
+
- cleanup_cache
|
|
44
|
+
- model_digest
|
|
45
|
+
- cache_key
|
|
46
|
+
format:
|
|
47
|
+
html:
|
|
48
|
+
theme: cosmo
|
|
49
|
+
toc: true
|
|
50
|
+
execute:
|
|
51
|
+
enabled: false
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CacheEntry { #pymc_cache.CacheEntry }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
CacheEntry(
|
|
5
|
+
key,
|
|
6
|
+
model_slug,
|
|
7
|
+
operation,
|
|
8
|
+
created_at,
|
|
9
|
+
size_bytes,
|
|
10
|
+
status,
|
|
11
|
+
path,
|
|
12
|
+
software_versions,
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Metadata for an owned, valid disk entry (without loading posterior arrays).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# CacheLookup { #pymc_cache.CacheLookup }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
CacheLookup(
|
|
5
|
+
backend,
|
|
6
|
+
key,
|
|
7
|
+
hit,
|
|
8
|
+
result,
|
|
9
|
+
normalized_kwargs,
|
|
10
|
+
disable_reason,
|
|
11
|
+
operation,
|
|
12
|
+
versions,
|
|
13
|
+
sampler_identity,
|
|
14
|
+
pre_run_model_digest,
|
|
15
|
+
)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Immutable record of a pre-sampling cache lookup.
|
|
19
|
+
|
|
20
|
+
``result`` is non-None only for a hit; ``key`` is usable by a later store.
|
|
21
|
+
``normalized_kwargs`` is a snapshot, including pre-sampling RNG state.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# CacheSession { #pymc_cache.CacheSession }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
CacheSession(
|
|
5
|
+
cache=True,
|
|
6
|
+
*,
|
|
7
|
+
model=None,
|
|
8
|
+
policy=None,
|
|
9
|
+
namespace='',
|
|
10
|
+
model_key=None,
|
|
11
|
+
sampler_key=None,
|
|
12
|
+
)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Compute identity and check a cache before beginning costly sampling.
|
|
16
|
+
|
|
17
|
+
Reuse the returned CacheLookup with :meth:`store` after sampling completes.
|
|
18
|
+
This API lets a remote caller avoid provisioning a container on a hit.
|
|
19
|
+
|
|
20
|
+
## Methods
|
|
21
|
+
|
|
22
|
+
| Name | Description |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| [lookup](#pymc_cache.CacheSession.lookup) | Return a hit, a storable miss, or a safe uncacheable miss. |
|
|
25
|
+
| [store](#pymc_cache.CacheSession.store) | Publish only if graph and result still match the preflight identity. |
|
|
26
|
+
|
|
27
|
+
### lookup { #pymc_cache.CacheSession.lookup }
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
CacheSession.lookup(
|
|
31
|
+
sample_kwargs,
|
|
32
|
+
*,
|
|
33
|
+
operation='sample',
|
|
34
|
+
overwrite=False,
|
|
35
|
+
versions=None,
|
|
36
|
+
sampler_identity=None,
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Return a hit, a storable miss, or a safe uncacheable miss.
|
|
41
|
+
|
|
42
|
+
### store { #pymc_cache.CacheSession.store }
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
CacheSession.store(lookup, idata, *, status='complete', effective_sampler=None)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Publish only if graph and result still match the preflight identity.
|
|
49
|
+
|
|
50
|
+
A reported sampler fallback is not stored under the preflight key; callers
|
|
51
|
+
must preflight a new identity if they want to cache that result.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# DiskCache { #pymc_cache.DiskCache }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
DiskCache(base_dir=None, model=None, *, max_entries_per_model=20)
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
One-file, full-key-verified atomic NetCDF result cache.
|
|
8
|
+
|
|
9
|
+
Entries live at ``<root>/<model>/<parameters>-<full-sha256>.nc``. Contents
|
|
10
|
+
have an independently checked full key and schema; legacy files are never
|
|
11
|
+
reused. The directory is a trusted local cache, not an untrusted file reader.
|
|
12
|
+
|
|
13
|
+
## Methods
|
|
14
|
+
|
|
15
|
+
| Name | Description |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| [clear](#pymc_cache.DiskCache.clear) | Delete only format-owned entries, preserving other files. |
|
|
18
|
+
| [delete](#pymc_cache.DiskCache.delete) | Remove matching owned entries, never arbitrary files or symlinks. |
|
|
19
|
+
| [entries](#pymc_cache.DiskCache.entries) | List this format's entries without materializing posterior data. |
|
|
20
|
+
| [prune](#pymc_cache.DiskCache.prune) | Keep the N newest entries per model; return count removed. |
|
|
21
|
+
|
|
22
|
+
### clear { #pymc_cache.DiskCache.clear }
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
DiskCache.clear()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Delete only format-owned entries, preserving other files.
|
|
29
|
+
|
|
30
|
+
### delete { #pymc_cache.DiskCache.delete }
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
DiskCache.delete(key)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Remove matching owned entries, never arbitrary files or symlinks.
|
|
37
|
+
|
|
38
|
+
### entries { #pymc_cache.DiskCache.entries }
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
DiskCache.entries()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
List this format's entries without materializing posterior data.
|
|
45
|
+
|
|
46
|
+
### prune { #pymc_cache.DiskCache.prune }
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
DiskCache.prune(keep=None)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep the N newest entries per model; return count removed.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MemoryCache { #pymc_cache.MemoryCache }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
MemoryCache(max_entries=8)
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Thread-safe, copy-isolated LRU of posterior results.
|
|
8
|
+
|
|
9
|
+
## Methods
|
|
10
|
+
|
|
11
|
+
| Name | Description |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| [clear](#pymc_cache.MemoryCache.clear) | Discard stored entries and return the number removed. |
|
|
14
|
+
|
|
15
|
+
### clear { #pymc_cache.MemoryCache.clear }
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
MemoryCache.clear()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Discard stored entries and return the number removed.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# cache_key { #pymc_cache.cache_key }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
cache_key(
|
|
5
|
+
model_identity,
|
|
6
|
+
sample_kwargs,
|
|
7
|
+
*,
|
|
8
|
+
operation='sample',
|
|
9
|
+
versions=None,
|
|
10
|
+
namespace='',
|
|
11
|
+
sampler_key=None,
|
|
12
|
+
)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Full SHA-256 identity of a model, sampler settings and environment.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Function reference {.doc .doc-index}
|
|
2
|
+
|
|
3
|
+
## Public API
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| [sample](sample.qmd#pymc_cache.sample) | Run ``pm.sample`` (or a trusted runner), reusing a matching result. |
|
|
8
|
+
| [sample_smc](sample_smc.qmd#pymc_cache.sample_smc) | Run and cache ``pm.sample_smc`` without conflating it with NUTS. |
|
|
9
|
+
| [CacheBackend](CacheBackend.qmd#pymc_cache.CacheBackend) | Minimal backend interface; extra management operations are optional. |
|
|
10
|
+
| [MemoryCache](MemoryCache.qmd#pymc_cache.MemoryCache) | Thread-safe, copy-isolated LRU of posterior results. |
|
|
11
|
+
| [DiskCache](DiskCache.qmd#pymc_cache.DiskCache) | One-file, full-key-verified atomic NetCDF result cache. |
|
|
12
|
+
| [CacheSession](CacheSession.qmd#pymc_cache.CacheSession) | Compute identity and check a cache before beginning costly sampling. |
|
|
13
|
+
| [CachePolicy](CachePolicy.qmd#pymc_cache.CachePolicy) | Select whether unseeded sampling may reuse a previous run. |
|
|
14
|
+
| [CacheEntry](CacheEntry.qmd#pymc_cache.CacheEntry) | Metadata for an owned, valid disk entry (without loading posterior arrays). |
|
|
15
|
+
| [CacheLookup](CacheLookup.qmd#pymc_cache.CacheLookup) | Immutable record of a pre-sampling cache lookup. |
|
|
16
|
+
| [UncacheableError](UncacheableError.qmd#pymc_cache.UncacheableError) | Raised when the identity of a model or sampling option cannot be established. |
|
|
17
|
+
| [CacheWarning](CacheWarning.qmd#pymc_cache.CacheWarning) | A cache was bypassed or an entry could not be persisted. |
|
|
18
|
+
| [cleanup_cache](cleanup_cache.qmd#pymc_cache.cleanup_cache) | Remove only new-format entries in a directory; do not delete the root. |
|
|
19
|
+
| [model_digest](model_digest.qmd#pymc_cache.model_digest) | Content hash of model graph, data, outputs and coordinates. |
|
|
20
|
+
| [cache_key](cache_key.qmd#pymc_cache.cache_key) | Full SHA-256 identity of a model, sampler settings and environment. |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# model_digest { #pymc_cache.model_digest }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
model_digest(model, *, model_key=None)
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Content hash of model graph, data, outputs and coordinates.
|
|
8
|
+
|
|
9
|
+
Custom Python operations require a user-maintained ``model_key``. Their
|
|
10
|
+
implementation cannot be inferred reliably from an Op's class name.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# sample { #pymc_cache.sample }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
sample(
|
|
5
|
+
model=None,
|
|
6
|
+
cache=True,
|
|
7
|
+
overwrite=False,
|
|
8
|
+
policy=None,
|
|
9
|
+
namespace='',
|
|
10
|
+
model_key=None,
|
|
11
|
+
sampler_key=None,
|
|
12
|
+
sample_fn=None,
|
|
13
|
+
**sample_kwargs,
|
|
14
|
+
)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run ``pm.sample`` (or a trusted runner), reusing a matching result.
|
|
18
|
+
|
|
19
|
+
No global PyMC function is replaced. ``cache='disk'`` persists under
|
|
20
|
+
``.pymc-cache``; ``cache=False`` always samples anew. A user-supplied
|
|
21
|
+
sample_fn must opt into safe identity or supply a trusted sampler_key.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# sample_smc { #pymc_cache.sample_smc }
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
sample_smc(
|
|
5
|
+
model=None,
|
|
6
|
+
cache=True,
|
|
7
|
+
overwrite=False,
|
|
8
|
+
policy=None,
|
|
9
|
+
namespace='',
|
|
10
|
+
model_key=None,
|
|
11
|
+
sampler_key=None,
|
|
12
|
+
**sample_kwargs,
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run and cache ``pm.sample_smc`` without conflating it with NUTS.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Contributing
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Follow the [repository contributing guide](https://github.com/justmytwospence/pymc-cache/blob/main/CONTRIBUTING.md) for installation and checks. Cache identity and persistence changes need explicit tests for false hits, incomplete results, older ArviZ and legacy data preservation. Do not commit posterior files, `.extraction-source/`, or generated site output.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Run a tiny repeatable PyMC sample twice with a private disk cache."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pymc as pm
|
|
8
|
+
|
|
9
|
+
import pymc_cache as pc
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
with pm.Model() as model:
|
|
14
|
+
pm.Normal("mu", 0, 1)
|
|
15
|
+
with tempfile.TemporaryDirectory(prefix="pymc-cache-example-") as directory:
|
|
16
|
+
options = dict(model=model, draws=10, tune=10, chains=1, cores=1,
|
|
17
|
+
random_seed=42, progressbar=False, cache=Path(directory))
|
|
18
|
+
first = pc.sample(**options)
|
|
19
|
+
second = pc.sample(**options)
|
|
20
|
+
assert first is not second
|
|
21
|
+
assert len(pc.DiskCache(directory).entries()) == 1
|
|
22
|
+
print("Reused", len(second.posterior.draw), "draws from disk")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Implement a minimal copy-isolated backend without a storage dependency."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
import pymc as pm
|
|
7
|
+
|
|
8
|
+
import pymc_cache as pc
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrivateBackend:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.entries = {}
|
|
14
|
+
|
|
15
|
+
def load(self, key, **kwargs):
|
|
16
|
+
value = self.entries.get(key)
|
|
17
|
+
return copy.deepcopy(value) if value is not None else None
|
|
18
|
+
|
|
19
|
+
def save(self, key, idata, **kwargs):
|
|
20
|
+
self.entries[key] = copy.deepcopy(idata)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
with pm.Model() as model:
|
|
25
|
+
pm.Normal("x")
|
|
26
|
+
backend = PrivateBackend()
|
|
27
|
+
options = dict(model=model, cache=backend, draws=2, tune=2, chains=1,
|
|
28
|
+
cores=1, random_seed=42, progressbar=False)
|
|
29
|
+
first = pc.sample(**options)
|
|
30
|
+
second = pc.sample(**options)
|
|
31
|
+
assert first is not second and len(backend.entries) == 1
|
|
32
|
+
print("Custom backend reused", len(second.posterior.draw), "draws")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Preflight an optional monitor runner before starting its work.
|
|
2
|
+
|
|
3
|
+
The callable is injected: pymc-cache does not import or install pymc-monitor.
|
|
4
|
+
Only pass a stable sampler_identity/versions for a runner whose semantics you own.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import pymc as pm
|
|
9
|
+
|
|
10
|
+
import pymc_cache as pc
|
|
11
|
+
from pymc_cache.policy import report_from_idata
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_with_monitor(model, monitor_run, *, cache, sampler_identity, versions, **options):
|
|
15
|
+
"""Reuse a result or call an external monitor; never store unverified adaptive runs."""
|
|
16
|
+
session = pc.CacheSession(cache=cache, model=model)
|
|
17
|
+
lookup = session.lookup(options, sampler_identity=sampler_identity, versions=versions)
|
|
18
|
+
if lookup.hit:
|
|
19
|
+
return lookup.result, True
|
|
20
|
+
result = monitor_run(model=model, **lookup.normalized_kwargs)
|
|
21
|
+
report = report_from_idata(result)
|
|
22
|
+
status = report.get("status", "unverified") if report else (
|
|
23
|
+
"unverified" if options.get("until") else "complete"
|
|
24
|
+
)
|
|
25
|
+
session.store(lookup, result, status=status, effective_sampler=sampler_identity)
|
|
26
|
+
return result, False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
# Replace pm.sample with your pymc-monitor runner after verifying its call
|
|
31
|
+
# signature, version and convergence-report contract in your installation.
|
|
32
|
+
with pm.Model() as model:
|
|
33
|
+
pm.Normal("x")
|
|
34
|
+
cache = pc.MemoryCache()
|
|
35
|
+
opts = dict(draws=2, tune=2, chains=1, cores=1, random_seed=42, progressbar=False)
|
|
36
|
+
first, hit = run_with_monitor(model, pm.sample, cache=cache,
|
|
37
|
+
sampler_identity="native-pymc-example", versions={
|
|
38
|
+
"runner": pm.__version__}, **opts)
|
|
39
|
+
assert not hit
|
|
40
|
+
second, hit = run_with_monitor(model, pm.sample, cache=cache,
|
|
41
|
+
sampler_identity="native-pymc-example", versions={
|
|
42
|
+
"runner": pm.__version__}, **opts)
|
|
43
|
+
assert hit and first is not second
|
|
44
|
+
print("Preflight skipped the runner:", hit)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|