Checkpoint/resume design (doc only, for review) - #777
Conversation
cdc8b0f to
e5f8c4e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #777 +/- ##
==========================================
+ Coverage 76.16% 76.49% +0.32%
==========================================
Files 169 175 +6
Lines 22224 23236 +1012
==========================================
+ Hits 16928 17774 +846
- Misses 5296 5462 +166 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
31f8ad5 to
02ee029
Compare
Design for checkpointing mpi-sppy runs (serial, multi-rank, and cylinders) so a killed or interrupted job can resume where it left off. Approach: reconstruct the scaffolding (MPI comms, RMA windows, persistent solvers, Pyomo models) via the normal startup path, then restore the algorithmic state (iteration counter, W, nonant values, rho, the incumbent / best xhat, and stateful-extension internals). Pickling the whole object graph with dill is not viable -- the core objects hold live MPI communicators, RMA windows, and persistent solver handles. Doc-only; no library code yet. Covers: why dill-everything fails, the state inventory (with a reconstructed-vs-restored and bit-reproducible-vs-carried- forward breakdown), the determinism contract (primal trajectory bit-identical; bounds/incumbent valid but carried forward, not reproducible), the required core changes, a proposed file layout, and a 6-phase rollout. Validated by a serial + multi-rank + cylinders proof of concept on farmer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
02ee029 to
be8b367
Compare
The method definition is at spcommunicator.py:1010, not 1019. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| primal state (W/nonants/rho) is unaffected (it is gathered from nonants/params), | ||
| but any *full-objective* read taken right after an eval is wrong. Snapshot and | ||
| restore all vars around an eval, or evaluate on a copy. | ||
| 5. **Geometry / cfg fingerprint** (§5.6) with a clear refusal on mismatch. |
There was a problem hiding this comment.
I'm confused on this point -- doesn't serializing the best solution as already stored by the xhat / incumbent spokes already do this?
There was a problem hiding this comment.
Yes — and that's the direction I took the revision. §5.3 already identified the xhat spoke's best_solution_cache as the thing to serialize; I've now made it explicit (revised §5.3 and §9 item 6) that the spoke checkpoints that cache itself, asynchronously on each improvement (reusing _maybe_write_incumbent_on_improvement), with no separate hub-driven coordination. So there's no extra machinery beyond serializing what the spoke already holds.
| 7. **Atomic, per-rank, barriered writes** (already in the PoC): each rank writes | ||
| only its local state to a rank-tagged file via temp-then-rename, inside a | ||
| barrier, so a hard kill never yields a half-written or partial-across-ranks | ||
| checkpoint. Retain the last *k* checkpoints (configurable) so a kill *during* | ||
| a checkpoint still leaves a usable earlier one. |
There was a problem hiding this comment.
Isn't it sufficient to just keep a single snapshot? After we complete the operations to write the new snapshot, can't we just immediately delete the prior one?
There was a problem hiding this comment.
Agreed — revised §9 item 7 to a single published generation. The new generation is written to fresh files, then manifest.json is atomically renamed to point at it (the single commit point), then the prior generation is deleted. A kill before the flip keeps the previous complete checkpoint; a kill after keeps the new one. So one committed generation is enough for kill-safety; keeping older generations is now just optional history, not a requirement.
There was a problem hiding this comment.
Follow-up: the doc now goes one step further and pins this down — retention of more than one checkpoint is explicitly not supported (d11832a). Exactly one committed generation exists at any time; the prior one is deleted right after the manifest flip, and the only time two exist is transiently during a publish (the documented peak disk footprint). The earlier "older generations are optional history" wording is gone.
Adopt bknueven's review suggestions on Pyomo#777: - Drop the hub-triggered snapshot barrier. Each spoke checkpoints its own incumbent asynchronously on improvement (reusing _maybe_write_incumbent_on_improvement); the determinism contract already makes bounds/incumbent carried-forward best-so-far, so a globally-consistent cross-cylinder snapshot at iteration k is unnecessary — and this removes the stall/deadlock risk against got_kill_signal. (§5.3, §9 item 6, §11 Phase 4) - Single published generation instead of last-k retention: the manifest flip is the atomic commit point, so one committed generation is enough for kill-safety; older generations are optional history. (§8, §9 item 7, §10) - File layout: hub/ keeps iteration-tagged generations; spokes/ keeps latest-wins per-spoke files overwritten async on improvement. (§10) - Clarify the variable_probability note: restore must reproduce the zero-prob W-masking invariant (surrogate fixed-at-0), not just reload raw W. (§12) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ned stop/resume
Re-center the checkpoint/resume design on the concrete use case: a multi-day
run that intentionally stops at end of day and resumes the next morning, with
large MIP scenarios, in the same environment, checkpointing about twice as
often as it resumes.
At that (infrequent) checkpoint cadence the per-write overhead of dilling the
whole scenario model is negligible, and same-day resume makes dill's version
fragility irrelevant -- so dilling the mid-run scenario models becomes the
recommended backend. It captures W/rho/nonants/fixedness, the second-stage
MIP warm start (via the existing warmstart_subproblems / solution_available
path), the linearized-prox cuts + ProxApproxManager, and model-attached
extension state (fixer's counter) in one consistent shot, and avoids re-running
an expensive scenario_creator on resume.
Keep the leaf-rebuild path as the low-cost backend (tiny, fast, version-robust
checkpoints) for small/cheap-creator runs or frequent kill-safety writes;
select via --checkpoint-backend {dill-model, leaf}. Reconstructing the
scaffolding (comms/windows/solvers) and restoring the non-model state
(iteration counter, hub bounds/incumbent, spoke best-xhat by name, extension-
object state, cursor/RNG) is common to both backends.
Soften the determinism contract for MIPs (correct warm-started continuation +
preserved incumbent, not bit-reproducible), shift the primary trigger to
end-of-run/on-signal, and flag the one load-bearing unvalidated assumption --
that a mid-run MIP model round-trips through dill -- as Phase 1's first job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…un-dill risk Fold the MIP dill-reload PoC (sizes SIZES3, gurobi_persistent single-thread) into the design: - §6: the dill-reload backend is now validated on a MIP. A mid-run scenario model -- including the linearized-prox case (845 xsqvar_cuts + 65 ProxApproxManagers) -- round-trips through dill in-process and cross-process, re-solving to identical objective + decision vars; a serial stop->reload-> continue is bit-identical to an uninterrupted run under the deterministic single-thread solve. Note what remains for later phases (multi-rank, cylinders, incumbent carry, warm-start speedup, scale footprint). - §9 item 2: the reload branch must skip attach_Ws_and_prox as well as attach_PH_to_objective, and refresh saved_objectives[sname] (Eobjective reads those handles; they dangle to the discarded fresh model after a swap). The swap targets local_scenarios (there is no local_subproblems). - §12: move the mid-run-dill round-trip from "Still open" to "Resolved". Also reword the leaf-rebuild backend in §2.2 from "is retained as" to "remains available as" the low-cost backend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a design document describing a checkpoint/resume architecture for mpi-sppy runs (including cylinders), focusing on planned stop/resume of long-running, large-scenario MIP studies by reconstructing MPI/solver scaffolding and restoring algorithm/model state (primarily via dilling mid-run scenario models).
Changes:
- Introduces a doc-only design specifying what state is reconstructed vs restored, and proposes two backends (
dill-modelrecommended;leafalternative). - Defines checkpoint semantics (triggers, atomic publication via a manifest, geometry/cfg fingerprinting) and details state inventory including incumbents and extension state.
- Proposes a phased implementation plan with testing expectations and rollout milestones.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| dangle to the discarded fresh model — and note the reload targets | ||
| `local_scenarios` only (there is no `local_subproblems`; the solve path | ||
| iterates `local_scenarios`). This is a distinct branch from the leaf-rebuild |
There was a problem hiding this comment.
Good catch — this was inaccurate, and it flagged a real implementation gotcha. Corrected in 3bcc029: a plain PH has no local_subproblems, but the generic file-based path the dill-reload backend builds on sets sp.local_subproblems = sp.local_scenarios (scenario_io.py) and CGBase.solve_loop iterates it, so the reload branch must refresh that alias where it exists or it dangles to the discarded model. (Re-opened — this had been resolved by mistake.)
| - **Warm-start plumbing:** `spopt.py` already supports warm-starting subproblem | ||
| solves — the `warmstart_subproblems` option plus `WarmstartStatus.PRIOR_SOLUTION` | ||
| use a warm start when `s._mpisppy_data.solution_available` is set | ||
| (`spopt.py:301-305`). Restoring the model's variable values and setting | ||
| `solution_available=True` feeds the restored MIP solution straight into this | ||
| path — no new solver code. |
There was a problem hiding this comment.
Agreed — replaced the file.py:line references with file + symbol (e.g. warmstart_subproblems in spopt.py) in 3bcc029 so they stay stable as the code moves.
| Status: **draft** (framework and dill-reload backend both PoC-validated — the | ||
| latter on a serial MIP; multi-rank and cylinders pending). Scope: checkpoint a | ||
| running mpi-sppy job so it can be stopped and resumed later. Must work on multiple | ||
| MPI ranks and for cylinder (hub-and-spoke) runs. |
There was a problem hiding this comment.
Fixed in 3bcc029 — the CLI flag is now --checkpoint-backend dill-reload (was dill-model), so the flag and the narrative use one name, with "reload" as the restore step.
- Correct the local_subproblems claim (§9 item 2): a plain PH has none, but the generic file-based path the dill-reload backend builds on sets sp.local_subproblems = sp.local_scenarios (scenario_io.py) and CGBase.solve_loop iterates it, so the reload branch must refresh that alias where it exists or it dangles to the discarded model. - Reconcile backend naming: the CLI flag is now --checkpoint-backend dill-reload (was dill-model) so the flag and the narrative use one name; "reload" is the restore step. - Replace volatile file:line references with file + symbol (e.g. "warmstart_subproblems in spopt.py" instead of "spopt.py:301-305") so they do not drift as the code moves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gers Extend §8 from the single `--checkpoint-every k` cadence to three composable triggers, enabled by `--checkpoint-dir`: - `--checkpoint-at-termination` (default on) — terminal checkpoint fired from the hub's existing `post_everything` hook when the run terminates for any internal reason (convergence, `--max-iterations`, cylinder convergence, `--time-limit`); the planned-stop use case pairs it with `--time-limit`. Not driven by external OS signals (recorded as a non-goal in §1). - `--checkpoint-every-seconds S` — wall-clock cadence checked at `enditer` via `allreduce_or(now - last_checkpoint >= S)`, mirroring the existing `time_limit` collective check so ranks decide together (no barrier deadlock). - `--checkpoint-every-iterations K` — the former `--checkpoint-every k`, renamed for symmetry. Also update §9 item 8 (two write points: periodic at `enditer`, terminal at `post_everything`, plus the scenario_denouement caveat) and §11 Phase 1 flag list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tly planned
The primary use case is fully served by the dill-reload backend (Phases 1-4), so
the leaf-rebuild backend and the broader spoke coverage bundled with it are
deferred. Reframe §11 Phase 6 as a possible-but-unscheduled future phase (design
keeps its hooks and the shared framework/manifest so it can be added later), and
reconcile the three surfaces that implied leaf ships alongside dill-reload:
§1 ("still supported" -> future option, not currently planned), §2.2 ("remains
available" -> designed but not currently planned), and the §8 --checkpoint-backend
flag (dill-reload is the only implemented/valid backend until Phase 6 lands).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rness - New §8.2: stochastic ADMM contract (naming/file discovery must not use module name creators or extract_num; creator-cost/memory caveats incl. releasing wrapper-held models; variable_probability identity-keying invariant; wrapper-mutated model dill round-trip to validate; AdmmBundler; spoke-set differences). - Restore is now an in-core resume branch in Iter0 replacing the iter-0 solve (precedent: iter0_from_pickle) — no throwaway W=0 solve; dropped the special end-of-iteration-0 checkpoint. - New §11.1: A/B full-run vs stop+resume CI harness with explicit similarity criteria; instances farmer, farmer+cvar, stoch-distr (--stoch-admm), sizes; wired into the phase test lists. - §10: define <S> in file names; fold in the disk-footprint peak. - §12 recast as resolved decisions + deferrals (former open items folded into the sections where they belong). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkpoint / resume — design for review (no library code yet)
This PR adds a design document (
doc/designs/checkpointing_design.md) forcheckpointing an mpi-sppy run so it can be stopped and resumed later. It is
doc-only and intended for design review before any implementation.
Use case driving the design
A long (multi-day) run that is intentionally stopped and resumed on a schedule
— e.g. a three-day study that ends each day and picks up the next morning on the
same cluster — with large MIP scenarios. Checkpoints are infrequent (a
handful over the whole run, roughly twice as many writes as resumes). This regime
is what makes the primary design choice below the right one; a different regime
(frequent checkpoints purely for hard-kill safety) is served by the low-cost
backend, also in the doc.
TL;DR
windows, and persistent solver handles. Those are always reconstructed via
normal startup (a fresh MPI job each morning).
the use case above. It captures
W/rho/nonants/fixedness, the second-stageMIP warm start, the linearized-prox cuts, and model-attached extension state
in one consistent shot, and avoids re-running an expensive
scenario_creator.At a few checkpoints the write cost is negligible, and same-environment resume
makes dill's version fragility a non-issue.
--checkpoint-backend leaf): rebuild viascenario_creatorand overlay tiny numeric arrays — small,fast, version-robust — for small/cheap-creator runs or frequent kill-safety
writes.
xhat solution values live on the xhat spoke, while the hub holds only the
incumbent objective. The design checkpoints both.
Validated by proofs of concept
gurobi_persistent): serial andmulti-rank (
-np 3, uneven-np 2) resume reproduces a full runbit-identically for
W/nonants/rho; cylinders (PH hub + lagrangian +xhatshuffle) hub primal resumes bit-identical inside
WheelSpinner; geometrymismatch is refused with a clear error.
sizesSIZES3,gurobi_persistent,single-thread for a deterministic solve): a mid-run scenario model — including
the linearized-prox case with 845 cuts + 65
ProxApproxManagers — dills andreloads in-process and cross-process, re-solving to identical objective and
decision variables; a serial stop → reload → continue is bit-identical to
an uninterrupted run for both quadratic and linearized prox.
cylinders, incumbent carry across a dill-reload stop, a measured warm-start
speedup, and the disk footprint at true model scale.
Determinism contract
best xhat preserved — but is not bit-reproducible (multi-threaded MIP solves
are nondeterministic and admit multiple optima). The PoC's bit-identity results
used a single-thread deterministic solve as a validation crutch.
bit-identical (a bonus, not the target).
What the doc specifies
State inventory (reconstructed vs restored; what rides in the dilled model vs
non-model leaf data), the required core changes (global iteration counter, a
reload-model resume branch that skips re-attach and refreshes objective handles, a
one-line xhatter write hook so a single
Checkpointerserves hub + spoke, anextension
checkpoint_state/restore_statecontract, async per-spoke incumbentcheckpoints with no hub↔spoke coordination, atomic single-generation writes via a
manifest), a proposed file layout, and a 6-phase rollout (each phase a
review-sized PR).
Ready for review. Implementation would land as the phased PRs described in the doc.
Update log
Per review (@bknueven): leaned into mpi-sppy's asynchrony — dropped the
hub-triggered snapshot barrier (each spoke checkpoints its own incumbent
asynchronously on improvement) and simplified retention to a single
manifest-published generation.
Design pivot (this revision): re-centered on the concrete use case above
(multi-day planned stop/resume, huge MIP scenarios, same environment). The
recommended backend is now dill the mid-run scenario models rather than
leaf-data-only — at this checkpoint cadence the overhead is negligible and it
captures the MIP warm start + prox cuts for free — with leaf-rebuild kept as the
low-cost alternative. Softened the determinism contract for MIPs, shifted the
primary trigger to end-of-run/on-signal, and validated the load-bearing
assumption (a mid-run MIP model round-trips through dill) with the MIP PoC above.
Design review pass (this revision): added §8.2 stochastic ADMM (wrapped-name
file discovery — no
scenario_names_creator/extract_num; the creator-cost savingdoes not apply and resume must release the wrapper-held fresh models; the
variable_probabilityidentity-keying invariant; the wrapper-mutated model dillround-trip flagged for early validation; AdmmBundler; spoke-set differences).
Restore is now an in-core resume branch in
Iter0replacing the iter-0 solve(precedent:
iter0_from_pickle) — no throwawayW = 0solve on resume, and thespecial end-of-iteration-0 checkpoint is dropped. Added §11.1, an A/B
(uninterrupted vs stop+resume) CI harness with explicit similarity criteria over
small instances: farmer, farmer+
--cvar, stoch-distr (--stoch-admm), andsizes. §12 recast as resolved decisions + deferrals.🤖 Generated with Claude Code