Skip to content

Add pickle-based checkpoint support for exact suspend/resume - #103

Open
aoymt wants to merge 3 commits into
feature/rng-refactorfrom
feature/checkpoint
Open

aoymt wants to merge 3 commits into
feature/rng-refactorfrom
feature/checkpoint

Conversation

@aoymt

@aoymt aoymt commented Aug 15, 2026

Copy link
Copy Markdown

Summary

This PR makes the search policies safely picklable and adds a checkpoint
API so that an interrupted search can be resumed bit-exactly: the
continued run selects exactly the same candidates as if it had never
stopped.

Built on top of #NN (RNG refactoring); please merge that first — this
branch is based on it.

The primary motivation is applications that embed PHYSBO as a library
(e.g. ODAT-SE): they already have their own checkpoint cycle but had to
approximate PHYSBO's state via save()/load(), giving up bit-wise
reproducibility because the policy could not be pickled (the MPI
communicator blocked it) and internal state (RNG, pending predictor
updates, candidate partition) was not captured.

What's added

All functionality lives in a new CheckpointMixin
(physbo/search/_checkpoint.py), inherited by all six policies via
discrete.Policy and range.Policy.

Safe pickling (the embedding use case)

  • __getstate__/__setstate__ exclude the MPI communicator
    (mpi4py communicators other than predefined ones are not picklable),
    so a policy can be embedded directly in a host application's own
    checkpoint dict:

    state = {"step": step, "policy": self.policy, ...}   # just works
    pickle.dump(state, f)
    # ... on restart ...
    self.policy = state["policy"]
    self.policy.set_comm(self.mpicomm)
  • set_comm(comm) re-attaches a communicator after unpickling, and
    validates that its size and rank match the stored state.

  • In the Generator RNG mode (rng= argument from #NN) the RNG state is
    part of the policy, so a plain pickle captures everything needed for
    exact resumption. Hosts using the legacy mode keep saving
    numpy.random.get_state() themselves, as ODAT-SE already does.

Single-file checkpoint API (standalone use)

  • policy.save_checkpoint(filename) — collective under MPI: rank-local
    state (each rank's remaining candidates and RNG state) is gathered and
    rank 0 writes one file atomically (temp file + os.replace);
    the shared model state (predictor/training/history, identical across
    ranks) is stored once, not per rank.
  • Policy.load_checkpoint(filename, comm=None) — collective: rank 0
    reads and broadcasts; each rank takes back its local state and the
    communicator is re-attached.
  • The file records the checkpoint format version, PHYSBO version,
    policy class, and mpisize. On load: wrong format/class/mpisize raise
    a RuntimeError with a clear message; a PHYSBO version mismatch
    emits a RuntimeWarning (bit-exact resumption is guaranteed only
    within the same version).
  • Legacy RNG mode is fully supported: the global numpy.random state
    of each rank is captured in the checkpoint and restored on load
    (documented as a deliberate side effect — the stream lives there).

Relation to save()/load()

The existing save()/load() are unchanged and keep their role:
portable, results-only persistence (loadable with a different number of
MPI processes, robust across versions). Docstrings of both APIs now
cross-reference each other, and a new manual page (en/ja,
checkpoint.rst) explains the distinction, MPI usage, and the
embedding pattern.

Tests

  • 272 tests pass; 11 new unit tests and 3 new MPI checks (run with
    np=2 and np=4).
  • Resumed runs equal uninterrupted runs bit-exactly: serial and MPI,
    legacy and Generator modes, including multi-probe search.
  • Regression test for the original blocker: a policy created with a
    Dup()ed communicator pickles and checkpoints correctly.
  • Error paths: wrong mpisize / format version / policy class rejected;
    version-mismatch warning verified.
  • No numerical change to existing behavior: fixed-seed reference
    outputs (10 scenarios, serial and np=2) are bit-identical to the
    base branch.

Limitations / future work

  • Resuming with a different number of MPI processes is rejected.
    With the single-sequence RNG design from #NN this is no longer
    blocked by RNG state in principle (for the BLM path), but the
    re-partitioning of the candidate set is left for a follow-up; the
    format version field is there to allow it.

🤖 Generated with Claude Code

aoymt and others added 3 commits August 13, 2026 15:28
Third stage of the RNG refactoring plan (dev/checkpoint_plan.md):
policies can now be pickled safely and a run can be suspended and
resumed bit-exactly.

- CheckpointMixin (physbo/search/_checkpoint.py), inherited by all six
  policies via discrete.Policy and range.Policy:
  - __getstate__/__setstate__ exclude the MPI communicator (mpi4py
    comms other than the predefined ones are not picklable) so a policy
    can be embedded directly in a host application's own checkpoint,
    e.g. ODAT-SE's per-rank state dict.
  - set_comm(comm) re-attaches a communicator after unpickling, with
    size/rank validation against the stored state.
  - save_checkpoint/load_checkpoint: a single-file checkpoint
    consolidated on rank 0 (collective operations; atomic write via
    os.replace). Rank-local state (candidate partition and RNG) is
    gathered per rank; the shared model state is stored once. The file
    records format version, PHYSBO version, policy class, and mpisize,
    all validated on load.
  - RNG state: the Generator mode stores the policy-owned generators;
    the legacy mode captures and restores the global numpy.random state
    per rank (documented side effect).
- save()/load() docstrings now point to the checkpoint API for exact
  resumption; they remain the portable, results-only persistence.

Verification: resumed runs equal uninterrupted runs bit-exactly in
both RNG modes, serial and MPI (np=2, np=4), including multi-probe
search; policies created with a Dup()ed communicator pickle and
checkpoint correctly; wrong mpisize/format/class are rejected on load.
No numerical change to existing behavior (fixed-seed references are
bit-identical to the previous commit). 272 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a "Checkpoint and restart" page (en/ja) covering:
- the difference between save()/load() (portable results) and
  save_checkpoint()/load_checkpoint() (bit-exact resumption),
- the legacy vs Generator RNG modes and their interaction with
  checkpointing,
- usage under MPI (collective operations, same mpisize required,
  BLM recommended for TS),
- the embedding pattern for host applications (pickling the policy
  into the host's own checkpoint and re-attaching the communicator
  with set_comm()).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant