Skip to content

Checkpoint/resume design (doc only, for review) - #777

Open
DLWoodruff wants to merge 12 commits into
Pyomo:mainfrom
DLWoodruff:checkpoint-poc
Open

Checkpoint/resume design (doc only, for review)#777
DLWoodruff wants to merge 12 commits into
Pyomo:mainfrom
DLWoodruff:checkpoint-poc

Conversation

@DLWoodruff

@DLWoodruff DLWoodruff commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Checkpoint / resume — design for review (no library code yet)

This PR adds a design document (doc/designs/checkpointing_design.md) for
checkpointing 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

  • Do not dill the opt/hub object graph — it holds live MPI communicators, RMA
    windows, and persistent solver handles. Those are always reconstructed via
    normal startup (a fresh MPI job each morning).
  • Do dill the mid-run scenario models — this is the recommended backend for
    the use case above. It captures W/rho/nonants/fixedness, the second-stage
    MIP 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.
  • A low-cost leaf-rebuild backend remains available (--checkpoint-backend leaf): rebuild via scenario_creator and overlay tiny numeric arrays — small,
    fast, version-robust — for small/cheap-creator runs or frequent kill-safety
    writes.
  • Keep the best xhat, not just the best bound: in a cylinders run the best
    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

  • Framework + leaf-rebuild backend (farmer LP, gurobi_persistent): serial and
    multi-rank (-np 3, uneven -np 2) resume reproduces a full run
    bit-identically for W/nonants/rho; cylinders (PH hub + lagrangian +
    xhatshuffle) hub primal resumes bit-identical inside WheelSpinner; geometry
    mismatch is refused with a clear error.
  • dill-reload backend on a MIP (sizes SIZES3, gurobi_persistent,
    single-thread for a deterministic solve): a mid-run scenario model — including
    the linearized-prox case with 845 cuts + 65 ProxApproxManagers — dills and
    reloads 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.
  • Still to prove in later phases: the dill-reload backend under multi-rank and
    cylinders, incumbent carry across a dill-reload stop, a measured warm-start
    speedup, and the disk footprint at true model scale.

Determinism contract

  • MIPs (the target): resume continues correctly and warm-started, with the
    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.
  • Leaf-rebuild on a deterministic LP/QP solve: primal trajectory can be
    bit-identical (a bonus, not the target).
  • Bounds and incumbent: valid, best-so-far, not bit-reproducible (async).

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 Checkpointer serves hub + spoke, an
extension checkpoint_state/restore_state contract, async per-spoke incumbent
checkpoints 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 saving
does not apply and resume must release the wrapper-held fresh models; the
variable_probability identity-keying invariant; the wrapper-mutated model dill
round-trip flagged for early validation; 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 on resume, and the
special 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), and
sizes. §12 recast as resolved decisions + deferrals.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.49%. Comparing base (64a8fff) to head (3c95ae2).
⚠️ Report is 43 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DLWoodruff
DLWoodruff force-pushed the checkpoint-poc branch 2 times, most recently from 31f8ad5 to 02ee029 Compare June 27, 2026 19:25
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>
@DLWoodruff
DLWoodruff marked this pull request as ready for review June 27, 2026 22:13
The method definition is at spcommunicator.py:1010, not 1019.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread doc/designs/checkpointing_design.md Outdated
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused on this point -- doesn't serializing the best solution as already stored by the xhat / incumbent spokes already do this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread doc/designs/checkpointing_design.md Outdated
Comment on lines +348 to +352
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread doc/designs/checkpointing_design.md Outdated
Comment thread doc/designs/checkpointing_design.md Outdated
DLWoodruff and others added 5 commits July 1, 2026 16:06
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-model recommended; leaf alternative).
  • 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.

Comment thread doc/designs/checkpointing_design.md Outdated
Comment on lines +458 to +460
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Comment on lines +157 to +162
- **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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +3 to +6
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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@bknueven
bknueven requested a review from tvalenciaz July 17, 2026 17:44
DLWoodruff and others added 4 commits July 20, 2026 18:15
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants