Skip to content

M-CHECKPOINT CHK.4: Frequency displacement checkpoint/resume - #119

Merged
jonathanschultzNU merged 2 commits into
mainfrom
claude/quantui-lab2-improvements-no3hwc
Sep 7, 2026
Merged

M-CHECKPOINT CHK.4: Frequency displacement checkpoint/resume#119
jonathanschultzNU merged 2 commits into
mainfrom
claude/quantui-lab2-improvements-no3hwc

Conversation

@jonathanschultzNU

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #118 (merged) on the same branch — the previous PR's history
is preserved via rebase onto the post-merge main; this PR only contains
the two new commits below.

Adds concurrency-safe, per-displacement checkpoint/resume for Frequency's
finite-difference IR and Raman loops (M-CHECKPOINT CHK.4, the one item
left open from the Lab 2 findings in #118). Full design writeup lives in
the (private) planning repo's TODO/roadmaps/34-m-checkpoint-calc-restart-roadmap.md.

Geometry-Opt and PES-Scan (#118) both have a natural sequence point to
checkpoint at — an optimizer step, a scan point. The Hessian's 6N
finite-difference displacement loop doesn't, and freq_ir_workers.py/
freq_raman_workers.py can run displacements concurrently via the
opt-in QUANTUI_FREQ_PARALLEL worker pool, so resume has to diff a set
of completed displacement ids, not trim a prefix.

  • quantui/checkpoint.py — set-oriented completion API
    (mark_item_done/completed_item_ids/completed_items) alongside the
    existing index-based completed_points(), plus path-only siblings
    (mark_item_done_at/completed_items_at) for a ProcessPoolExecutor
    worker that only has a directory path, not a picklable Checkpoint
    object.
  • quantui/freq_displacement_ids.py (new) — deterministic
    (atom_index, axis, sign) → id scheme, the single source of truth
    shared by every path below.
  • quantui/freq_calc.pyrun_freq_calc() takes checkpoint/
    resume (same convention as Geometry-Opt). Serial and parallel paths
    are unified around one shared dict keyed by displacement id, so resume,
    the parallel pool, and the serial fallback can never disagree about
    what's already done — and the Hessian assembly step only runs once
    every required id is present, regardless of which path produced it.
  • quantui/freq_ir_workers.py / quantui/freq_raman_workers.py
    each worker durably records its own completion (temp-file +
    os.replace) before returning to the parent, so a completed
    displacement survives even if the parent process dies before collecting
    that Future.
  • quantui/raman_calc.py — identical treatment under a separate named
    item set (raman_displacements), so its records can't collide with
    IR's (freq_displacements) despite sharing the exact same displacement
    grid.
  • quantui/backends/worker.py_run_frequency now calls
    _begin_worker_checkpoint() and threads checkpoint/resume through, the
    same pattern _run_geometry_opt/_run_pes_scan already used. This is
    the actual SLURM batch path the motivating production case hit.

One pre-existing bug fixed in passing: a parallel-pool failure partway
through used to discard every already-completed displacement and
recompute all of them serially. The shared-dict unification fixes this
for free — the serial fallback now skips anything already done.

Testing

pytest -m "not network": full suite green, 3237 passed / 30 skipped. 37
new tests (test_freq_calc.py, test_raman_calc.py,
test_backends_worker.py) — the load-bearing ones assert run_scf_with_rescue
call counts, not just a plausible-looking answer: a fully-banked resume
does zero displacement recompute, a partial resume recomputes exactly the
missing ids, a corrupted banked item is recomputed rather than trusted,
and resume=False correctly ignores a populated checkpoint. ruff +
black clean.

Authorship

  • Claude (Sonnet 5): code edits, review, and conceptual discussion
  • Jonathan Schultz: overall vision, planning, review, and orchestration

Generated by Claude Code

Foundational pieces of CHK.4 (frequency displacement restart) per the
scoped design in QuantUI-development-tracking roadmap 34: concurrency-
safe, per-displacement, set-based checkpointing rather than a wave-
boundary scheme.

CHK.4.1 -- Checkpoint API extension. Adds a set-oriented completion
API (mark_item_done()/completed_item_ids()/completed_items()) to
Checkpoint, generalizing CHK.3's index-based completed_points() to
arbitrary named item sets. Each item gets its own file
(items/<name>/<item_id>.json), written via temp-file + os.replace --
never a shared file, so two writers can never race on the same
filename and a crash mid-write never produces a file that looks done.
completed_item_ids() is derived from validated, parsed content, not
mere filename presence, so a corrupted item is never mistaken for a
completed one. has_progress() now recognizes item-set progress too.

Also adds mark_item_done_at()/completed_items_at() -- path-only
siblings of the same API, for a caller that has only a directory path
rather than a full Checkpoint object. This is exactly the situation a
ProcessPoolExecutor worker is in (CHK.4.4): it receives the checkpoint
directory as a plain string via initargs, and a Checkpoint with a live
log stream attached is not something safe to pickle across a process
boundary.

CHK.4.2 -- quantui/freq_displacement_ids.py: deterministic
(atom_index, axis, sign) -> id scheme (e.g. "d000_x_+"), shared by
freq_calc.py's serial/parallel dispatch and freq_ir_workers.py /
freq_raman_workers.py's worker processes, so none of them can disagree
about what the required displacement set is.

104 new/updated tests (test_checkpoint.py, test_freq_displacement_ids.py).
Not yet wired into freq_calc.py/freq_ir_workers.py/freq_raman_workers.py
-- that's CHK.4.3-.5, still open.
Completes M-CHECKPOINT CHK.4 (roadmap 34) -- concurrency-safe,
per-displacement resume for Frequency's IR and Raman finite-difference
loops, on top of CHK.4.1/.2's checkpoint API + id scheme.

freq_calc.py: run_freq_calc()/_run_freq_calc_body() take checkpoint/
resume params (same convention as optimizer.py's CHK.2). The IR
displacement loop (serial and parallel) is unified around one shared
_dipoles dict keyed by (atom, axis, sign) -- resumed items, parallel-
pool results, and serial-fallback results all land in the same dict,
so the final dpdx assembly is a proper gate: it only runs once every
required id is present, regardless of which path produced it. Each
displacement is durably banked via checkpoint.mark_item_done() the
moment it finishes. mark_complete() is called on success so a finished
run doesn't linger forever in the "unfinished calculations" listing.

Bonus fix found while unifying the two paths: a parallel-pool failure
partway through used to discard every already-completed displacement
and recompute all of them serially. The shared _dipoles dict fixes
this for free -- the serial fallback now skips anything already done.

freq_ir_workers.py / freq_raman_workers.py: run_displaced_scf() /
run_displaced_polarizability() take an item_id and, when
init_worker()/init_raman_worker() were given a checkpoint directory,
write their result durably via checkpoint.mark_item_done_at() before
returning -- so a completed displacement survives even if the parent
process dies before collecting that Future.

raman_calc.py: _cpu_raman_activities_fd()/compute_raman_activities()
get the identical checkpoint/resume treatment under a separate named
item set ("raman_displacements") so its records can't collide with
IR's ("freq_displacements") despite sharing the exact same
(atom, axis, sign) displacement grid.

backends/worker.py: _run_frequency() now calls
_begin_worker_checkpoint() and threads checkpoint/resume into
run_freq_calc() -- this is the actual SLURM batch path where the real
production pain happened (GOTCHAS.md: a 19-atom NCShare frequency job
needed two resource-bump rounds, discarding the whole Hessian each
time). Previously only geometry_opt/pes_scan were wired (CL2.8).

37 new tests across test_freq_calc.py, test_raman_calc.py,
test_backends_worker.py. The CHK.4.6 tests prove actual resume
behavior via run_scf_with_rescue call counts, not just "produces a
plausible-looking answer": a fully-banked resume does zero displacement
recompute, a partial resume recomputes exactly the missing ids, a
corrupted banked item is recomputed rather than trusted, resume=False
correctly ignores existing progress. Full suite: 3237 passed, 30 skipped.
@jonathanschultzNU
jonathanschultzNU merged commit 297b06d into main Sep 7, 2026
5 checks passed
@jonathanschultzNU
jonathanschultzNU deleted the claude/quantui-lab2-improvements-no3hwc branch September 7, 2026 05:08
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.

2 participants