cloudposterior 0.6.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.
- cloudposterior-0.6.0/.github/workflows/publish.yml +40 -0
- cloudposterior-0.6.0/.github/workflows/test.yml +63 -0
- cloudposterior-0.6.0/.gitignore +23 -0
- cloudposterior-0.6.0/CLAUDE.md +106 -0
- cloudposterior-0.6.0/LICENSE +21 -0
- cloudposterior-0.6.0/PKG-INFO +403 -0
- cloudposterior-0.6.0/README.md +365 -0
- cloudposterior-0.6.0/cloudposterior/__init__.py +3 -0
- cloudposterior-0.6.0/cloudposterior/_idata.py +184 -0
- cloudposterior-0.6.0/cloudposterior/api.py +1532 -0
- cloudposterior-0.6.0/cloudposterior/backends/__init__.py +93 -0
- cloudposterior-0.6.0/cloudposterior/backends/modal_backend.py +792 -0
- cloudposterior-0.6.0/cloudposterior/cache.py +152 -0
- cloudposterior-0.6.0/cloudposterior/config.py +146 -0
- cloudposterior-0.6.0/cloudposterior/dashboard.py +689 -0
- cloudposterior-0.6.0/cloudposterior/display.py +492 -0
- cloudposterior-0.6.0/cloudposterior/naming.py +95 -0
- cloudposterior-0.6.0/cloudposterior/notify.py +170 -0
- cloudposterior-0.6.0/cloudposterior/progress.py +234 -0
- cloudposterior-0.6.0/cloudposterior/remote/__init__.py +0 -0
- cloudposterior-0.6.0/cloudposterior/remote/worker.py +833 -0
- cloudposterior-0.6.0/cloudposterior/serialize.py +142 -0
- cloudposterior-0.6.0/cloudposterior/wordhash.py +28 -0
- cloudposterior-0.6.0/examples/basics.ipynb +509 -0
- cloudposterior-0.6.0/examples/basics.py +185 -0
- cloudposterior-0.6.0/examples/caching.ipynb +1117 -0
- cloudposterior-0.6.0/examples/caching.py +227 -0
- cloudposterior-0.6.0/examples/monitoring.ipynb +506 -0
- cloudposterior-0.6.0/examples/monitoring.py +157 -0
- cloudposterior-0.6.0/examples/parallelism.ipynb +499 -0
- cloudposterior-0.6.0/examples/parallelism.py +236 -0
- cloudposterior-0.6.0/pyproject.toml +69 -0
- cloudposterior-0.6.0/tests/conftest.py +40 -0
- cloudposterior-0.6.0/tests/smoke_test.py +48 -0
- cloudposterior-0.6.0/tests/test_adaptive.py +55 -0
- cloudposterior-0.6.0/tests/test_async_offload.py +33 -0
- cloudposterior-0.6.0/tests/test_cache.py +134 -0
- cloudposterior-0.6.0/tests/test_caching_local.py +234 -0
- cloudposterior-0.6.0/tests/test_config_autosize.py +62 -0
- cloudposterior-0.6.0/tests/test_dashboard.py +98 -0
- cloudposterior-0.6.0/tests/test_display.py +118 -0
- cloudposterior-0.6.0/tests/test_fidelity.py +30 -0
- cloudposterior-0.6.0/tests/test_idata_compat.py +70 -0
- cloudposterior-0.6.0/tests/test_keepalive.py +103 -0
- cloudposterior-0.6.0/tests/test_lru_prune.py +92 -0
- cloudposterior-0.6.0/tests/test_map.py +332 -0
- cloudposterior-0.6.0/tests/test_modal_e2e.py +127 -0
- cloudposterior-0.6.0/tests/test_naming.py +92 -0
- cloudposterior-0.6.0/tests/test_new_ops.py +124 -0
- cloudposterior-0.6.0/tests/test_notify.py +119 -0
- cloudposterior-0.6.0/tests/test_notify_validation.py +40 -0
- cloudposterior-0.6.0/tests/test_predictive.py +41 -0
- cloudposterior-0.6.0/tests/test_progress.py +130 -0
- cloudposterior-0.6.0/tests/test_resize_warning.py +79 -0
- cloudposterior-0.6.0/tests/test_sampler_dispatch.py +108 -0
- cloudposterior-0.6.0/tests/test_serialize.py +87 -0
- cloudposterior-0.6.0/tests/test_step_methods.py +87 -0
- cloudposterior-0.6.0/tests/test_worker_local.py +130 -0
- cloudposterior-0.6.0/tests/test_wrap.py +96 -0
- cloudposterior-0.6.0/uv.lock +4213 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # OIDC token for PyPI Trusted Publishing (no stored secrets)
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
20
|
+
|
|
21
|
+
- name: Install Python
|
|
22
|
+
run: uv python install 3.12
|
|
23
|
+
|
|
24
|
+
- name: Build package
|
|
25
|
+
run: uv build --no-sources
|
|
26
|
+
|
|
27
|
+
# Catch a wheel/sdist that's missing bundled files (remote/worker.py, the
|
|
28
|
+
# embedded dashboard HTML) before anything is uploaded -- versions on PyPI
|
|
29
|
+
# are immutable, so a broken artifact would burn the version number.
|
|
30
|
+
- name: Smoke test (wheel)
|
|
31
|
+
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
|
|
32
|
+
|
|
33
|
+
- name: Smoke test (source distribution)
|
|
34
|
+
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
|
|
35
|
+
|
|
36
|
+
# Trusted Publishing (OIDC) + automatic PEP 740 attestations. uv build does
|
|
37
|
+
# not generate attestations, so the upload stays on the PyPA action (kept on
|
|
38
|
+
# its maintained moving tag, release/v1, per PyPA guidance).
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
16
|
+
- name: Lint with ruff
|
|
17
|
+
run: uvx ruff check cloudposterior/ tests/
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
25
|
+
# Exercise both arviz majors: PyMC 5 ships arviz 0.x, PyMC 6 requires
|
|
26
|
+
# arviz 1.x (the DataTree rewrite). Modal e2e tests are not run in CI.
|
|
27
|
+
pymc: ["5", "6"]
|
|
28
|
+
exclude:
|
|
29
|
+
# PyMC 5 is legacy; don't gate the release on it under the newest
|
|
30
|
+
# Python. 3.13 is still covered by the PyMC 6 leg.
|
|
31
|
+
- python-version: "3.13"
|
|
32
|
+
pymc: "5"
|
|
33
|
+
# PyMC 6 needs arviz 1.x, whose compatible nutpie (>=0.16.10) ships no
|
|
34
|
+
# Python 3.11 wheel -- the combination is unsatisfiable. PyMC 6 is
|
|
35
|
+
# covered on 3.12 and 3.13.
|
|
36
|
+
- python-version: "3.11"
|
|
37
|
+
pymc: "6"
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
41
|
+
|
|
42
|
+
- name: Install uv
|
|
43
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
44
|
+
|
|
45
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
46
|
+
run: uv python install ${{ matrix.python-version }}
|
|
47
|
+
|
|
48
|
+
- name: Install dependencies
|
|
49
|
+
run: uv sync --python ${{ matrix.python-version }}
|
|
50
|
+
|
|
51
|
+
# Pin pymc AND arviz per leg so each major is exercised deterministically
|
|
52
|
+
# (an unpinned resolve drifts to the newest arviz for both legs).
|
|
53
|
+
- name: Run tests (PyMC ${{ matrix.pymc }})
|
|
54
|
+
run: |
|
|
55
|
+
if [ "${{ matrix.pymc }}" = "6" ]; then
|
|
56
|
+
uv run --python ${{ matrix.python-version }} \
|
|
57
|
+
--with "pymc>=6,<7" --with "arviz>=1.1,<2.0" --with "nutpie>=0.13" \
|
|
58
|
+
python -m pytest tests/ -v --cov=cloudposterior --cov-report=term-missing
|
|
59
|
+
else
|
|
60
|
+
uv run --python ${{ matrix.python-version }} \
|
|
61
|
+
--with "pymc>=5,<6" --with "arviz>=0.18,<1.0" --with "nutpie>=0.13" \
|
|
62
|
+
python -m pytest tests/ -v --cov=cloudposterior --cov-report=term-missing
|
|
63
|
+
fi
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.eggs/
|
|
7
|
+
*.egg
|
|
8
|
+
.env
|
|
9
|
+
.env.local
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
*.nc
|
|
13
|
+
.modal/
|
|
14
|
+
.cloudposterior/
|
|
15
|
+
|
|
16
|
+
# Local worktrees
|
|
17
|
+
worktrees/
|
|
18
|
+
|
|
19
|
+
# Local Claude Code settings and plans
|
|
20
|
+
.claude/
|
|
21
|
+
|
|
22
|
+
# marimo export session/layout artifacts
|
|
23
|
+
__marimo__/
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this project is
|
|
6
|
+
|
|
7
|
+
cloudposterior lets you run PyMC MCMC sampling on cloud VMs (currently Modal) with one line of code. It intercepts `pm.sample()` via a context manager (`cp.cloud`) and adds cloud execution, automatic caching, live progress display, and phone notifications.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync # install all deps (including dev group)
|
|
13
|
+
uv run pytest tests/ -v # run free local tests (default)
|
|
14
|
+
uv run pytest tests/ -v --run-modal # also run paid Modal e2e tests
|
|
15
|
+
uv run pytest tests/test_cache.py -v # run one test file
|
|
16
|
+
uv run pytest tests/test_cache.py::test_name -v # run single test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Tests marked `@pytest.mark.modal` (`tests/test_modal_e2e.py`) hit real Modal infrastructure and incur cloud costs. They are skipped unless `--run-modal` is passed. See `tests/conftest.py` for the marker plumbing.
|
|
20
|
+
|
|
21
|
+
CI runs pytest on Python 3.11 and 3.12 (free tests only -- Modal tests are not in CI).
|
|
22
|
+
|
|
23
|
+
## Example notebooks
|
|
24
|
+
|
|
25
|
+
Each example in `examples/` exists in two formats that must be kept in sync:
|
|
26
|
+
|
|
27
|
+
- `*.ipynb` -- the Jupyter version. This is the artifact GitHub renders (with embedded outputs), so it is what users see in the repo.
|
|
28
|
+
- `*.py` -- the marimo version (`uv run marimo edit examples/<name>.py`). This is the source you edit and pair on.
|
|
29
|
+
|
|
30
|
+
**Sync rule: whenever you change one format, update the other in the same change.** They are equivalent notebooks, not independent files -- an edit to a marimo `.py` cell must be mirrored into the matching `.ipynb`, and vice versa.
|
|
31
|
+
|
|
32
|
+
- marimo `.py` -> `.ipynb`: `uv run marimo export ipynb examples/<name>.py -o examples/<name>.ipynb`. Add `--include-outputs` to refresh the rendered outputs, but note it re-executes the notebook (real Modal sampling, so cost + time -- only do this deliberately).
|
|
33
|
+
- `.ipynb` -> marimo `.py`: `uvx marimo convert examples/<name>.ipynb -o examples/<name>.py`, then re-apply the marimo cleanups (drop trailing `;` output-suppression, make each cell's final expression the value to render). Do NOT add a PEP 723 script-metadata block -- `cloudposterior` is a local editable install and would fail to resolve from PyPI; these run in the project venv.
|
|
34
|
+
- After editing either format, run `uv run marimo check examples/<name>.py` before committing.
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
### Request flow
|
|
39
|
+
|
|
40
|
+
1. `cp.cloud(model)` context manager monkeypatches five PyMC functions to route through `api.py`: `pm.sample` (→ `_run_sample` / `_run_sample_persistent`), `pm.sample_prior_predictive` / `pm.sample_posterior_predictive` (→ `_run_predictive`), `pm.sample_smc` (→ `_run_smc`), and `pm.compute_log_likelihood` (→ `_run_idata_op`). The latter three reuse a blocking (non-streaming) remote-op template; only `pm.sample` streams per-draw progress.
|
|
41
|
+
2. Model + observed data are serialized separately (cloudpickle + lz4 for the model, numpy + lz4 for data) in `serialize.py`
|
|
42
|
+
3. Cache key is computed from the serialized bytes + sample kwargs (`cache.py`)
|
|
43
|
+
4. If remote: `ModalBackend` (`backends/modal_backend.py`) submits a `SamplingPayload` to Modal, which runs `remote/worker.py` in a container with version-matched dependencies
|
|
44
|
+
5. If local: the original `pm.sample` is called directly
|
|
45
|
+
6. Progress events stream back via msgpack and are rendered by display sinks (an anywidget that animates live in both Jupyter and marimo notebooks, Rich for terminals) in `display.py`
|
|
46
|
+
7. Results are cached and returned as `az.InferenceData`
|
|
47
|
+
|
|
48
|
+
### Key abstractions
|
|
49
|
+
|
|
50
|
+
- **`ComputeBackend` / `SamplingJob`** (`backends/__init__.py`): Abstract interface for compute providers. Only Modal is implemented; designed for future providers.
|
|
51
|
+
- **`RemoteEnvironment`** (`backends/__init__.py`): Persistent execution environment with data pre-loaded (via Modal Volumes). Accepts multiple sampling jobs without re-uploading data. Provisioned via `ComputeBackend.provision()`.
|
|
52
|
+
- **`CacheBackend`** (`cache.py`): Protocol with `MemoryCache` (default, session-scoped) and `DiskCache` (persistent, human-readable directory tree under `.cloudposterior/`).
|
|
53
|
+
- **`ProgressEvent`** (`progress.py`): Union type of `PhaseUpdate` and `SamplingProgress` that flows through display sinks.
|
|
54
|
+
- **`SamplingPayload`** (`serialize.py`): Dataclass bundling serialized model, data, version manifest, and sample kwargs for transport.
|
|
55
|
+
|
|
56
|
+
### Persistent containers and volumes
|
|
57
|
+
|
|
58
|
+
When `remote=True`, containers stay warm for 20 minutes. Model payloads are stored in a project-scoped Modal Volume so only sample kwargs are sent per-call:
|
|
59
|
+
|
|
60
|
+
1. Model is serialized once in `__enter__()` and uploaded to a Volume at `{model_slug}/{data_slug}/payload-{hash}.bin`
|
|
61
|
+
2. A `modal.Cls`-based sampler loads the payload from the mounted Volume (fast local read)
|
|
62
|
+
3. Each `pm.sample()` call sends only kwargs + a path string -- no model/data bytes on the wire
|
|
63
|
+
4. If the model changes between calls, the new payload is uploaded to the Volume (KB, fast)
|
|
64
|
+
5. Volume is project-scoped (`cp-{project}`) -- cleaned up via `cp.cleanup_volumes(project=...)`
|
|
65
|
+
|
|
66
|
+
The provisioned env is **kept warm past the `with` block** (not torn down in `__exit__`): it's held in `api._LIVE_ENVS` keyed by `(project, model_slug)` and reused by later runs of the same model, so the dashboard stays browsable and re-runs skip cold start. It's torn down by `cp.cleanup_volumes()` / `session.destroy()` / an `atexit` hook (Modal's `scaledown_window` idles the container out ~20 min regardless). Each run clears the control `Dict["stop"]` flag so a reused env doesn't inherit a stale stop.
|
|
67
|
+
|
|
68
|
+
### Naming conventions (two layers)
|
|
69
|
+
|
|
70
|
+
Human-readable names for browsability, machine hashes for correctness:
|
|
71
|
+
|
|
72
|
+
| System | Human-readable (cosmetic) | Machine-correct (identity) |
|
|
73
|
+
|--------|--------------------------|---------------------------|
|
|
74
|
+
| Local disk cache | `{model_slug}/{data_slug}/{params}.nc` | `compute_cache_key()` SHA-256 |
|
|
75
|
+
| Remote Volume | `{model_slug}/{data_slug}/payload-{hash}.bin` | `payload_hash()` SHA-256 prefix |
|
|
76
|
+
| Notifications | `{model_slug}-{random_wordhash}` | N/A |
|
|
77
|
+
|
|
78
|
+
Shared utilities in `naming.py`: `model_slug()`, `data_slug()`, `payload_hash()`, `wordhash()`
|
|
79
|
+
|
|
80
|
+
### Live dashboard (`notify="dashboard"` or `notify=True` with `remote=True`)
|
|
81
|
+
|
|
82
|
+
`dashboard.py` contains `DashboardSink` (writes progress to a Modal Dict) and `DASHBOARD_HTML` (self-contained page with JS polling). Two `@modal.fastapi_endpoint` functions serve the HTML and progress JSON. The dashboard URL includes the model name for readability (e.g., `radon-intercepts-a3f7b2-dev.modal.run`).
|
|
83
|
+
|
|
84
|
+
When `notify=True` and `remote=True`, defaults to dashboard. When local, defaults to ntfy (start + complete notifications only).
|
|
85
|
+
|
|
86
|
+
### Remote worker
|
|
87
|
+
|
|
88
|
+
`remote/worker.py` runs inside Modal containers. It is never imported locally -- Modal serializes and executes it. It deserializes the model, runs sampling while streaming per-chain stats via a queue, and returns lz4-compressed NetCDF. `_sample_and_stream` branches three ways by sampler:
|
|
89
|
+
|
|
90
|
+
- **`pymc`**: `pm.sample(nuts_sampler="pymc", callback=...)` -- the per-draw callback fills the progress queue. The `nuts_sampler="pymc"` is explicit so PyMC 6 doesn't auto-select nutpie (which would reject the callback).
|
|
91
|
+
- **`nutpie`** (the default): runs nutpie's background sampler (`blocking=False`) with its native `progress_callback`; the generator loop polls the stop Dict and calls `handle.abort()` to stop early (keeping the partial trace), then `handle.wait()` for the final result.
|
|
92
|
+
- **`numpyro`/`blackjax`**: `pm.sample(nuts_sampler=..., progressbar=False)` with **no callback** -- PyMC's external NUTS samplers run inside JAX with no per-draw hook (and PyMC 6 raises if a callback is passed). These report phase-level progress only.
|
|
93
|
+
|
|
94
|
+
Besides streaming sampling, the worker has **blocking (non-streaming) entries** that load the model and return lz4 NetCDF directly: `run_prior_predictive` / `run_posterior_predictive`, `run_smc` (`pm.sample_smc`), and `run_compute_log_likelihood`. They mount as `@modal.method()`s on the persistent `Sampler` Cls and are driven client-side by `_run_blocking_op`.
|
|
95
|
+
|
|
96
|
+
**Custom `step=` over the wire**: a step instance pickled separately from the model has value variables in a different graph than the worker's Volume-loaded model (PyMC raises "not a value variable in the model"). So `intercepted_sample` ships a combined `{model, step}` blob via `serialize_model_with_step`; `_unpack_model_payload` on the worker detects the dict, re-injects the step, and forces the pymc sampler (so the per-draw callback / live progress still work).
|
|
97
|
+
|
|
98
|
+
### Samplers and arviz compatibility
|
|
99
|
+
|
|
100
|
+
- **Default sampler**: `api._default_sampler()` picks nutpie for fully continuous models (PyMC's own default, ~2x faster) and the pymc sampler when there are discrete free RVs; locally it falls back to pymc when nutpie isn't installed. nutpie is always installed in CPU remote images (`_build_pip_specs`).
|
|
101
|
+
- **Callback constraint**: PyMC's per-draw `callback` only fires for `nuts_sampler="pymc"`; PyMC 6 *raises* if a callback is passed to an external sampler. Only attach a callback on the pymc path (worker + `_run_local`).
|
|
102
|
+
- **`_idata.py`**: thin shims so the codebase works on **both arviz 0.x (PyMC 5) and arviz 1.x (PyMC 6, DataTree)** -- `.groups()` vs `.groups`, removed `convert_to_inference_data`, changed `ess(method="tail")`, the dict-valued `sample_stats` attr nutpie writes, and object-dtype data vars from SMC's `sample_stats` (`beta`/`accept_rate`/`log_marginal_likelihood`, which even native `to_netcdf` rejects) -- both handled by `sanitize_inference_data`. `add_group` merges a remotely-computed group (e.g. `log_likelihood`) into a local idata in place. Always go through these helpers instead of calling arviz idata methods directly.
|
|
103
|
+
|
|
104
|
+
### Auto-sizing
|
|
105
|
+
|
|
106
|
+
`RemoteConfig._auto()` in `config.py` inspects the model's observed data size, parameter count, and chain count to right-size VM resources (CPU cores and memory).
|
|
@@ -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,403 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudposterior
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Run PyMC MCMC sampling on cloud VMs with one line of code. Caching, live progress, and phone notifications included.
|
|
5
|
+
Project-URL: Homepage, https://github.com/justmytwospence/cloudposterior
|
|
6
|
+
Project-URL: Repository, https://github.com/justmytwospence/cloudposterior
|
|
7
|
+
Project-URL: Issues, https://github.com/justmytwospence/cloudposterior/issues
|
|
8
|
+
Author: Spencer Boucher
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bayesian,cloud,mcmc,modal,pymc,sampling
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
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: anywidget>=0.9
|
|
22
|
+
Requires-Dist: arviz<2.0,>=0.17
|
|
23
|
+
Requires-Dist: cloudpickle>=3.0
|
|
24
|
+
Requires-Dist: coolname>=4.0
|
|
25
|
+
Requires-Dist: fastapi>=0.100
|
|
26
|
+
Requires-Dist: ipywidgets>=8.0
|
|
27
|
+
Requires-Dist: lz4>=4.0
|
|
28
|
+
Requires-Dist: modal>=1.0
|
|
29
|
+
Requires-Dist: msgpack>=1.0
|
|
30
|
+
Requires-Dist: numpy>=1.24
|
|
31
|
+
Requires-Dist: pymc>=5
|
|
32
|
+
Requires-Dist: qrcode>=7.0
|
|
33
|
+
Requires-Dist: requests>=2.28
|
|
34
|
+
Requires-Dist: rich>=13.0
|
|
35
|
+
Provides-Extra: nutpie
|
|
36
|
+
Requires-Dist: nutpie>=0.13; extra == 'nutpie'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# cloudposterior
|
|
40
|
+
|
|
41
|
+
**Stop waiting for MCMC. Start shipping posteriors.**
|
|
42
|
+
|
|
43
|
+
cloudposterior lets you run PyMC models on cloud VMs without changing your sampling code. One extra line gives you cloud compute, automatic caching, and phone notifications -- while `pm.sample()` stays exactly the same.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import cloudposterior as cp
|
|
47
|
+
|
|
48
|
+
with cp.cloud(model, remote=True):
|
|
49
|
+
idata = pm.sample(draws=5000, chains=8) # 8 cores in the cloud, zero config
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why?
|
|
55
|
+
|
|
56
|
+
You've built a hierarchical model. It's beautiful. But sampling takes 45 minutes on your laptop, your fans sound like a jet engine, and you can't use your machine for anything else.
|
|
57
|
+
|
|
58
|
+
cloudposterior fixes this:
|
|
59
|
+
|
|
60
|
+
- **Ship sampling to the cloud** with one line. Your model runs on a VM with as many cores and as much RAM as it needs.
|
|
61
|
+
- **Never re-run the same model twice.** Results are cached automatically -- re-execute a notebook cell and get your posterior back instantly.
|
|
62
|
+
- **Monitor from anywhere.** Get live progress notifications on your phone while your model samples.
|
|
63
|
+
|
|
64
|
+
All three features work independently. Use any combination, or just the caching.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv add cloudposterior
|
|
72
|
+
|
|
73
|
+
# For cloud execution (optional):
|
|
74
|
+
uv add modal && uv run modal setup
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import pymc as pm
|
|
79
|
+
import cloudposterior as cp
|
|
80
|
+
|
|
81
|
+
with pm.Model() as my_model:
|
|
82
|
+
mu = pm.Normal("mu", 0, 5)
|
|
83
|
+
sigma = pm.HalfNormal("sigma", 5)
|
|
84
|
+
pm.Normal("obs", mu, sigma, observed=data)
|
|
85
|
+
|
|
86
|
+
# This is the only line you add:
|
|
87
|
+
with cp.cloud(my_model, remote=True, cache="disk"):
|
|
88
|
+
idata = pm.sample(draws=2000, chains=4)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Second time you run that cell? Instant. The result is already cached.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Features
|
|
96
|
+
|
|
97
|
+
### Cloud execution
|
|
98
|
+
|
|
99
|
+
Offload MCMC to cloud VMs. No Docker, no infrastructure, no config files. [Modal](https://modal.com) handles containers, scaling, and cleanup.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
with cp.cloud(model, remote=True):
|
|
103
|
+
idata = pm.sample(draws=5000, chains=8)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Your model is serialized with cloudpickle, shipped to a container with version-matched dependencies (PyMC, PyTensor, numpy -- all pinned to your exact local versions), sampled there, and the trace is compressed and sent back. The container image is built once and cached, so subsequent runs start in seconds.
|
|
107
|
+
|
|
108
|
+
### Smart resource sizing
|
|
109
|
+
|
|
110
|
+
cloudposterior inspects your model and sampling config to right-size the VM automatically:
|
|
111
|
+
|
|
112
|
+
- **CPU cores** matched to your chain count (8 chains = 8 cores)
|
|
113
|
+
- **Memory** scaled to your observed data size and parameter count
|
|
114
|
+
|
|
115
|
+
No guessing, no over-provisioning. A small model gets 4 cores and 4GB. A hierarchical model with large datasets gets 8+ cores and 16GB+. The progress display shows what was chosen:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
cloudposterior -- Modal (auto-sized: 8 cores, 8GB)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Want explicit control? Use a preset:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
with cp.cloud(model, remote=True, instance="xlarge"): # 32 cores, 64GB
|
|
125
|
+
...
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The container is sized on the **first** `pm.sample()` call inside `with cp.cloud(...)` and stays that size for the duration of the block. If a later call uses different `chains`/`draws` that the auto-sizer would have provisioned differently, you'll get a warning -- start a new `cp.cloud()` block to resize.
|
|
129
|
+
|
|
130
|
+
### Automatic caching
|
|
131
|
+
|
|
132
|
+
Re-running a notebook cell? If the model, data, and sampling config haven't changed, cloudposterior returns the cached result instantly. No wasted compute. Caching is **on by default**.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
with cp.cloud(model):
|
|
136
|
+
idata = pm.sample(draws=2000) # samples normally
|
|
137
|
+
|
|
138
|
+
with cp.cloud(model):
|
|
139
|
+
idata = pm.sample(draws=2000) # instant -- cached
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For persistence across kernel restarts, use disk caching:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
with cp.cloud(model, cache="disk"):
|
|
146
|
+
idata = pm.sample(draws=2000)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Results are stored in a human-readable directory tree:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
.cloudposterior/
|
|
153
|
+
├── radon_intercepts/
|
|
154
|
+
│ └── draws2000_tune1000_chains4-a3f7b2c9.nc
|
|
155
|
+
└── radon_slopes/
|
|
156
|
+
└── draws2000_tune1000_chains4-7c2e5fa8.nc
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Model names come from `pm.Model(name="radon_intercepts")`. The hash suffix ensures uniqueness when non-displayed parameters (like `random_seed`) differ.
|
|
160
|
+
|
|
161
|
+
### Monitoring
|
|
162
|
+
|
|
163
|
+
Two ways to monitor sampling:
|
|
164
|
+
|
|
165
|
+
**Live dashboard** (on by default for remote) -- convergence diagnostics, trace plots, and a stop button:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
with cp.cloud(model, remote=True):
|
|
169
|
+
idata = pm.sample(draws=5000, chains=8)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Scan the QR code or open the URL on your phone. No app install needed.
|
|
173
|
+
|
|
174
|
+
**Push notifications** -- get notified when sampling starts and completes via [ntfy](https://ntfy.sh):
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
with cp.cloud(model, notify=True): # auto topic
|
|
178
|
+
with cp.cloud(model, notify="my-channel"): # custom topic
|
|
179
|
+
with cp.cloud(model, remote=True, notify=True): # remote (dashboard on by default) + ntfy
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With `remote=True`, the dashboard is on by default; `notify=True` adds ntfy push notifications on top.
|
|
183
|
+
|
|
184
|
+
### Live progress display
|
|
185
|
+
|
|
186
|
+
Both Jupyter notebooks and terminals show real-time, in-place progress for every phase:
|
|
187
|
+
|
|
188
|
+
1. Serialization
|
|
189
|
+
2. Upload
|
|
190
|
+
3. Container provisioning
|
|
191
|
+
4. MCMC sampling -- per-chain progress bars, divergences, step size, grad evals, speed, ETA
|
|
192
|
+
5. Result download
|
|
193
|
+
|
|
194
|
+
Notebooks get a live anywidget display that animates in-cell in both Jupyter and marimo. Terminals get a Rich TUI. Progress bars turn red when chains diverge, just like PyMC's native display. During a remote run a **Stop** button appears in the cell -- click it to abort early and keep the partial trace.
|
|
195
|
+
|
|
196
|
+
### Samplers
|
|
197
|
+
|
|
198
|
+
cloudposterior defaults to **nutpie** -- PyMC's recommended NUTS sampler, roughly 2x faster on CPU -- for fully continuous models, and falls back to PyMC's built-in sampler for models with discrete variables. Override per call:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
with cp.cloud(model, remote=True):
|
|
202
|
+
idata = pm.sample() # nutpie (default for continuous models)
|
|
203
|
+
idata = pm.sample(nuts_sampler="pymc") # PyMC's sampler (handles discrete vars)
|
|
204
|
+
idata = pm.sample(nuts_sampler="numpyro") # JAX sampler (GPU auto-provisioned)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Live per-chain progress, convergence diagnostics (rank-normalized R-hat and ESS), and the stop button work with **nutpie** and **pymc**. JAX samplers (`numpyro`, `blackjax`) run entirely inside a compiled graph with no per-draw hook, so they report phase-level progress only.
|
|
208
|
+
|
|
209
|
+
Custom step methods work too -- pass `step=pm.Metropolis()` (or `Slice`, `DEMetropolis`, a `CompoundStep`, ...) and cloudposterior routes to PyMC's sampler and ships the step alongside the model so it samples correctly in the cloud, with live progress. Discrete models that rely on PyMC's automatic step assignment need no `step=` at all -- just call `pm.sample()`.
|
|
210
|
+
|
|
211
|
+
Works with both **PyMC 5 and PyMC 6** (PyMC 6 ships arviz 1.x's DataTree); the versions installed in the remote container are matched to your local environment.
|
|
212
|
+
|
|
213
|
+
### Adaptive sampling
|
|
214
|
+
|
|
215
|
+
Stop as soon as the chains converge instead of guessing a draw count -- `draws` becomes the cap:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
with cp.cloud(model, remote=True, until={"r_hat": 1.01, "ess": 400}):
|
|
219
|
+
idata = pm.sample(draws=20000) # stops early once every parameter clears the target
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`until=True` uses the Vehtari (2021) defaults shown above. Works with **nutpie** and **pymc** (the samplers with a per-draw hook).
|
|
223
|
+
|
|
224
|
+
### Parallel fitting
|
|
225
|
+
|
|
226
|
+
Fit many models at once on a single warm container -- ideal for model comparison and prior sensitivity:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
import arviz as az
|
|
230
|
+
|
|
231
|
+
idatas = cp.map([pooled, hierarchical, per_county], {"draws": 1000})
|
|
232
|
+
az.compare(dict(zip(["pooled", "hier", "county"], idatas)))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`sample_kwargs` is a shared dict or a list aligned with the models. Results return in input order. See [examples/parallelism.ipynb](examples/parallelism.ipynb).
|
|
236
|
+
|
|
237
|
+
### Predictive checks and model comparison
|
|
238
|
+
|
|
239
|
+
`pm.sample_prior_predictive()` and `pm.sample_posterior_predictive()` are intercepted too, so prior/posterior predictive checks (and GP `.conditional()` predictions, which run through posterior predictive) execute on the same cloud container.
|
|
240
|
+
|
|
241
|
+
`pm.compute_log_likelihood()` is intercepted as well -- compute pointwise log-likelihoods in the cloud, then run `az.loo` / `az.waic` / `az.compare` locally:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
with cp.cloud(model, remote=True):
|
|
245
|
+
idata = pm.sample(draws=2000)
|
|
246
|
+
pm.compute_log_likelihood(idata) # adds the log_likelihood group, in the cloud
|
|
247
|
+
az.loo(idata)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`pm.sample_smc()` (Sequential Monte Carlo) runs in the cloud too, for multimodal posteriors and model evidence.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## What runs in the cloud
|
|
255
|
+
|
|
256
|
+
cloudposterior runs the **entire MCMC workflow** on the cloud container -- posterior sampling (NUTS and step methods, all four backends), prior/posterior predictive checks, SMC, and log-likelihood for model comparison. Optimization-based inference (variational inference, MAP) and a few non-`InferenceData` utilities are not yet routed to the cloud; they still run locally as usual.
|
|
257
|
+
|
|
258
|
+
| Supported in the cloud | Not yet (runs locally) |
|
|
259
|
+
|------------------------|------------------------|
|
|
260
|
+
| `pm.sample()` -- NUTS (`nutpie`, `pymc`, `numpyro`, `blackjax`) | `pm.fit()` -- variational inference (ADVI, etc.) |
|
|
261
|
+
| `pm.sample()` with custom `step=` (Metropolis, Slice, DEMetropolis, ...) | `pm.find_MAP()` -- MAP point estimation |
|
|
262
|
+
| `pm.sample_smc()` -- Sequential Monte Carlo | `pm.compute_deterministics()` |
|
|
263
|
+
| `pm.sample_prior_predictive()` / `pm.sample_posterior_predictive()` | `pm.draw()` |
|
|
264
|
+
| `pm.compute_log_likelihood()` -- LOO/WAIC/`az.compare` | pymc-extras (`fit_pathfinder`, `fit_laplace`) |
|
|
265
|
+
|
|
266
|
+
Two `pm.sample` details can't be matched exactly for remote execution and warn rather than silently diverge: `return_inferencedata=False` (a `MultiTrace` can't be transported, so you get an `InferenceData`) and a per-draw `callback=` (it can't run against local state inside a container -- use `remote=False` for that). VI, MAP, and the non-`InferenceData` utilities are a planned follow-up.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Composable features
|
|
271
|
+
|
|
272
|
+
| Feature | Default | Control |
|
|
273
|
+
|---------|---------|---------|
|
|
274
|
+
| Caching | **on** (in-memory) | `cache=True` / `False` / `"disk"` / `Path(...)` |
|
|
275
|
+
| Cloud execution | off | `remote=True` / `False` |
|
|
276
|
+
| Live dashboard | **on** (when remote) | `dashboard=True` / `False` |
|
|
277
|
+
| Push notifications | off | `notify=True` / `"topic"` / `{"server": ..., "topic": ...}` |
|
|
278
|
+
| Adaptive early-stop | off | `until=True` / `{"r_hat": ..., "ess": ...}` (remote) |
|
|
279
|
+
|
|
280
|
+
Mix and match:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
with cp.cloud(model): # local + memory cache
|
|
284
|
+
with cp.cloud(model, cache="disk"): # local + disk cache
|
|
285
|
+
with cp.cloud(model, remote=True): # cloud + dashboard
|
|
286
|
+
with cp.cloud(model, remote=True, cache="disk", notify=True): # everything
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Configuration
|
|
292
|
+
|
|
293
|
+
### Instance presets
|
|
294
|
+
|
|
295
|
+
| Name | CPUs | Memory |
|
|
296
|
+
|----------|------|--------|
|
|
297
|
+
| `small` | 4 | 8 GB |
|
|
298
|
+
| `medium` | 8 | 16 GB |
|
|
299
|
+
| `large` | 16 | 32 GB |
|
|
300
|
+
| `xlarge` | 32 | 64 GB |
|
|
301
|
+
| `gpu` | 8 | 16 GB + A100 |
|
|
302
|
+
|
|
303
|
+
### Environment variables
|
|
304
|
+
|
|
305
|
+
| Variable | Description |
|
|
306
|
+
|----------|-------------|
|
|
307
|
+
| `CLOUDPOSTERIOR_NTFY_TOPIC` | Default ntfy topic |
|
|
308
|
+
| `CLOUDPOSTERIOR_NTFY_SERVER` | Custom ntfy server (default: `https://ntfy.sh`) |
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Cloud backend
|
|
313
|
+
|
|
314
|
+
Cloud execution currently uses [Modal](https://modal.com). Modal provides fast container spin-up, automatic dependency packaging, and a generous free tier.
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
uv add modal
|
|
318
|
+
modal setup # one-time browser auth
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The backend is abstracted behind a `ComputeBackend` interface. Support for additional providers (AWS, GCP, SSH to your own machines) is planned.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## How it works
|
|
326
|
+
|
|
327
|
+
1. **Serialize** -- The model is serialized with cloudpickle + lz4 on the first `pm.sample()` call inside `with cp.cloud(...)`. Cloudpickle bundles your observed data into the model object, so there's a single payload, not two. A version manifest captures your exact package versions.
|
|
328
|
+
2. **Upload once** -- The serialized payload is uploaded to a Modal Volume the first time. Subsequent calls with the same model skip the upload entirely. Old payloads from past edits are pruned automatically.
|
|
329
|
+
3. **Sample** -- `pm.sample()` runs remotely. The container loads the payload from the mounted Volume (fast local read) and streams per-chain progress back in real time via msgpack. Only sample kwargs and a path string are sent over the wire per call.
|
|
330
|
+
4. **Return** -- The InferenceData trace is compressed as NetCDF, sent back, and cached.
|
|
331
|
+
|
|
332
|
+
The container stays warm for ~20 minutes **after the `with` block exits**, so a re-run of the same model is near-instant and the live dashboard stays browsable in the meantime (open it on your phone, walk away, come back and check). It idles out on its own and is torn down when the kernel exits; stop it immediately with `cp.cleanup_volumes()` or `session.destroy()`.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Cleanup
|
|
337
|
+
|
|
338
|
+
Model payloads are stored in a project-scoped Modal Volume. The following also stops any kept-warm container/dashboard for the project:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
cp.cleanup_volumes() # delete the current project's volume
|
|
342
|
+
cp.cleanup_volumes(project="my-research") # delete a specific project's volume
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
For a one-shot teardown that also stops a warm container immediately, use the context manager's `destroy()`:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
session = cp.cloud(model, remote=True)
|
|
349
|
+
with session:
|
|
350
|
+
idata = pm.sample(draws=2000)
|
|
351
|
+
session.destroy() # stop the container and delete the project volume
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Explicit API
|
|
357
|
+
|
|
358
|
+
If you prefer not to use the context manager, `cp.sample()` runs a single remote sampling job (always cloud, no persistent container reuse):
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
idata = cp.sample(model, draws=2000, chains=4)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
For repeated sampling with the same model, the `cp.cloud()` context manager is cheaper -- it keeps the container warm and only sends kwargs after the first call.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Example
|
|
369
|
+
|
|
370
|
+
Clone and run locally (Jupyter or marimo) for the full live progress display.
|
|
371
|
+
|
|
372
|
+
- [examples/basics.ipynb](examples/basics.ipynb) -- cloud execution and GPU acceleration with the Minnesota Radon dataset
|
|
373
|
+
- [examples/caching.ipynb](examples/caching.ipynb) -- local and disk caching, model iteration
|
|
374
|
+
- [examples/monitoring.ipynb](examples/monitoring.ipynb) -- live dashboard and push notifications
|
|
375
|
+
- [examples/parallelism.ipynb](examples/parallelism.ipynb) -- fit many models in parallel with `cp.map` and compare them with LOO
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Tests
|
|
380
|
+
|
|
381
|
+
The default suite is fast and free -- it covers serialization, caching, naming, kwarg validation, and runs `cp.cloud(model)` end-to-end against a local PyMC sampler:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
uv run pytest tests/ -v
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
A separate suite of end-to-end tests hits real Modal infrastructure to verify the cloud path. These cost a small amount of Modal credit per run and are skipped by default. Opt in with `--run-modal`:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
uv run pytest tests/test_modal_e2e.py -v --run-modal
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Modal tests provision the smallest possible instance, sample 20 draws on a 2-RV model, and use isolated per-test project volumes that are cleaned up at teardown.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Status
|
|
398
|
+
|
|
399
|
+
Early proof of concept. Works end-to-end with 75+ passing tests, but expect rough edges. Contributions and feedback welcome.
|
|
400
|
+
|
|
401
|
+
## License
|
|
402
|
+
|
|
403
|
+
MIT
|