Surviving spot preemption#
Added in version v0.2: The preempt-snapshot hook, the RESUME_FROM_URL entry-point, and
the sidecar manifest landed in v0.2 #8. See
maddening.core.simulation.checkpoint and
maddening.cloud.entrypoint.make_preempt_snapshot_hook().
Changed in version v0.4.0: download_and_load_state moved to maddening.cloud.resume
(URL transport is a deployment concern; the core checkpoint module
stays dependency-free). The old
maddening.core.simulation.checkpoint.download_and_load_state import
still works but emits a DeprecationWarning and is removed in 1.0.
Spot VMs are cheap and disposable — until the cloud provider yanks yours at 30 seconds’ notice and your simulation state vapourises. v0.2 wires up three things so that doesn’t happen:
Snapshot on preemption — a
CloudSessioncallback writesstate.npz+ a sidecar manifest the moment the preemption monitor fires.Resume from URL — the cloud entry-point reads
RESUME_FROM_URLand pulls the state in before the FastAPI server starts.Integrity manifest — every snapshot ships a sidecar with a SHA-256 hash and schema version so a corrupted resume fails loudly instead of silently mangling state.
The 30-second tour#
from maddening.cloud.session import CloudSession
from maddening.cloud.entrypoint import make_preempt_snapshot_hook
# Wire the hook
hook = make_preempt_snapshot_hook(
server, # has .gm (GraphManager)
snapshot_path="/mnt/snapshots/sim.npz",
extra_meta={"commit": "abc123", "cluster": "runpod-spot-7"},
)
session = CloudSession(on_preempted=hook)
session.launch(cfg)
# ... time passes, spot gets reclaimed ...
# hook(info) fires automatically; sim.npz + sim.npz.manifest.json land on disk
The orchestrator’s responsibility from that point is to upload both files to durable storage (S3, GCS, a Selkies volume) and relaunch the VM with:
RESUME_FROM_URL="https://my-bucket.s3.amazonaws.com/sim.npz" \
python -m maddening.cloud.entrypoint
The entry-point downloads the .npz and the .manifest.json,
verifies the hash + schema version, then loads the state before
binding the HTTP port.
The manifest schema#
{
"schema_version": 1,
"sha256": "41949865eaecffb496dc45c62ff400b01e11f51958c27599d96e12d6de80ca59",
"size_bytes": 810,
"extra": {
"session_id": "...",
"stage_at_snapshot": "preempted",
"commit": "abc123",
"cluster": "runpod-spot-7"
}
}
schema_version— bumps when the on-disk.npzkey layout changes. Readers refuse mismatched versions instead of silently producing wrong state.sha256— full hash of the.npzbody. Tampering or partial download →CheckpointIntegrityError.extra— caller-supplied dict. The snapshot hook auto-populatessession_idandstage_at_snapshot; merge anything else via theextra_meta=argument.
Warning
The snapshot is not an automatic upload to cloud storage — that’s the orchestrator’s job. See “What’s still on you, the orchestrator” below for the upload step and why MADDENING doesn’t do it for you.
Supported URL schemes#
RESUME_FROM_URL and the underlying
maddening.cloud.resume.download_and_load_state()
accept:
Scheme |
Behaviour |
|---|---|
|
Local file copy. Useful for local testing and shared-filesystem clusters. The path is percent-decoded ( |
|
HTTP GET via the stdlib |
|
Same as |
Bare POSIX path ( |
Treated as |
|
Read through fsspec (v0.4.0). Install |
An empty URL raises ValueError("empty checkpoint URL"); any scheme
outside the table raises ValueError("Unsupported URL scheme …").
The manifest URL and presigned URLs#
The sidecar manifest is fetched from the checkpoint URL with
.manifest.json appended to the path component; the query string
and fragment are preserved. So
https://bucket.s3.amazonaws.com/run/sim.npz?X-Amz-Signature=…
looks for
https://bucket.s3.amazonaws.com/run/sim.npz.manifest.json?X-Amz-Signature=…
A presigned URL only authorises the one object it was signed for, so that derived URL is rejected by S3/GCS/Azure. With presigned storage, presign the manifest too and hand both URLs over:
RESUME_FROM_URL="https://…/sim.npz?X-Amz-Signature=A" \
RESUME_MANIFEST_URL="https://…/sim.npz.manifest.json?X-Amz-Signature=B" \
python -m maddening.cloud.entrypoint
or, from Python, download_and_load_state(gm, url, manifest_url=…).
The entry point logs URLs with the query string replaced by
<redacted> so the signatures never reach the container log.
Temporary files#
Without a dest_dir=, the checkpoint and manifest are downloaded into
a fresh per-call temporary directory that is removed again when the
call returns or raises. Pass dest_dir= to keep the downloaded files.
Downloads are streamed to disk in 1 MiB chunks, so the resume host
does not need the whole checkpoint in memory.
What’s still on you, the orchestrator#
The MADDENING layer deliberately stops at “write the local file” and “read a URL”. That gives you room to choose:
Where to push the snapshot — S3, GCS, Azure Blob, Selkies volume, NFS, a raw HTTP server. The hook writes locally; you upload. Typical pattern: set
MADDENING_SNAPSHOT_DIRto a bind-mounted volume that survives the VM, then have the orchestrator pick the latest file from there.How to discover the latest snapshot — by filename convention, by reading the manifest’s
extra.session_id, by listing the bucket sorted by mtime — your call.What presigned URL to hand to the next VM — RunPod, AWS, GCP all support short-lived URLs; pass that as
RESUME_FROM_URLon the relaunch.
With the fsspec schemes (v0.4.0) the upload/presign steps are
optional: an orchestrator that can grant the pod bucket credentials
hands over one RESUME_FROM_URL=s3://… and the CLI calls go away.
The local-file-plus-presigned-URL pattern above still works and needs
no extra dependencies.
The full preempt-resume contract#
Hook fires on
CloudSession._on_preemption_signal()(called by the SkyPilot preemption monitor thread).Hook calls
save_state_with_manifest()with the configured snapshot path + extra meta.Hook returns; the
CloudSessioncontinues into teardown.(orchestrator) picks up the local snapshot + manifest, uploads.
(orchestrator) relaunches the VM with
RESUME_FROM_URL=....New VM’s entrypoint reads
RESUME_FROM_URLand callsresume_from_url()→download_and_load_state().download_and_load_statefetches the.npz+.manifest.jsoninto a per-call temp dir (so concurrent resumes don’t collide), then callsload_state_with_manifestwhich verifies the hash and schema version, then restores the state.FastAPI server binds the port. The new VM picks up where the old one left off.
If anything in steps 6-7 fails, the entry-point logs and
continues with the in-memory (fresh) state — a failed resume
should not block a healthy server from starting. An HTTP source
that never answers is cut off after MADDENING_RESUME_TIMEOUT
seconds (default 60) so it cannot hold the port closed forever. On
success the log line names the manifest’s schema_version,
size_bytes, the first 12 hex digits of sha256, and
extra.session_id / extra.stage_at_snapshot. Lab convention:
have your orchestrator notify you if RESUME_FROM_URL was set but
the manifest didn’t apply.
Note
The stock entry point does not load a graph yet (MADDENING_GRAPH_USD
is a stub), and a checkpoint can only be restored into a graph with
the same nodes. When RESUME_FROM_URL is set and the server’s graph
has no nodes, the entry point logs
“resume is impossible until a graph is loaded” and starts fresh
instead of failing later with a node-mismatch error. Embedders that
build the SimulationServer with a populated GraphManager and call
resume_from_env() get the full
resume.
Disabling the integrity check#
For one-off loads of pre-v0.2 checkpoints that don’t have a manifest:
from maddening.cloud.resume import download_and_load_state
download_and_load_state(
gm, url, skip_integrity_check=True,
)
The entrypoint.resume_from_url helper passes the flag through.
Do not use skip_integrity_check=True in production — the whole
point of the manifest is to catch the silent-corruption case.
Static-data: what gets restored, what doesn’t#
Following the v0.2 #3 contract, static_data is not in the
.npz. After a resume:
initial_state()outputs (state, meta) → restored from the snapshot.static_data(meshes, lookup tables) → rebuilt fromself.paramsduring your code’s graph reconstruction. See the “Static-data channel” section of DESIGN.md.
If you rebuild the graph in code identical to the pre-preemption
process and call load_state, both pieces match. If you change
the graph topology, load_state raises a ValueError listing the
nodes/fields that don’t match — better than silently broadcasting
garbage.
python -m maddening.examples.advanced.checkpoint_resume_demo shows
the local half of this on one machine: a coupled run saved with
save_state, resumed with load_state in a freshly built graph and
continued bitwise identically – the coupling group’s _meta warm
starts and a calibrated parameter included – next to a restart from
the node states alone, which is not the same run.
Test coverage and what’s deferred#
The file:// path is fully unit-covered in
tests/cloud/test_resume.py (URL transport, including the fsspec
memory:// round trip) and tests/cloud/test_preempt_checkpoint.py
(snapshot hook + entry-point helper) — every codepath above runs
against a _FakeCloudSession + local tempfile. What’s
not yet covered:
End-to-end RunPod spot preemption (requires real credentials).
s3:///gs:///az://against real buckets (the fsspec path is exercised with the in-memorymemory://filesystem only; the backends themselves are third-party).Multi-snapshot lifecycle (last-N retention, garbage collection).
The MADDENING contract is “write local file → orchestrator handles
transport → entrypoint reads URL”. Whether the transport is a
presigned https:// URL or a bucket URL read through fsspec is the
orchestrator’s choice; retention and garbage collection stay outside
the package.