fix(trainer): improve checkpoint consistency on resume - #253
Open
Kaiwei LIU (KAIWEILIUCC) wants to merge 2 commits into
Open
fix(trainer): improve checkpoint consistency on resume#253Kaiwei LIU (KAIWEILIUCC) wants to merge 2 commits into
Kaiwei LIU (KAIWEILIUCC) wants to merge 2 commits into
Conversation
Author
|
@microsoft-github-policy-service agree |
Contributor
There was a problem hiding this comment.
Pull request overview
Improves trainer checkpoint recovery and resume consistency.
Changes:
- Validates committed checkpoint prefixes and removes stale artifacts.
- Restores scheduler and step-buffer state.
- Adds atomic persistence and recovery tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Review summary |
|---|---|
tests/test_trainer_resume.py |
Adds focused recovery, phase, buffer, and atomic-write tests. |
skillopt/engine/trainer.py |
Implements checkpoint recovery and resume restoration. Two unresolved moderate findings concern unguarded marker conversion and trusting best_skill.md after recovery. |
Suppressed comments (7)
skillopt/engine/trainer.py:1394
- An accepted slow update can change
current_score, but this amendment only persists the best fields. The amended history/step record therefore reports the pre-slow current score, making epoch summaries and history-only recovery inconsistent with the runtime state. Persistcurrent_scorehere as well.
"best_origin": best_origin,
"best_score": best_score,
"best_step": best_step,
"best_skill_hash": skill_hash(best_skill),
skillopt/engine/trainer.py:625
- These checks never compare
runtime_state.scheduler_statewith the committed step record. If the marker has the same step/hash metadata but a stale or malformed scheduler state, the resume path below loads it and either uses the wrong learning-rate phase or fails on the nextscheduler.step(). Validate it againstlast_record(or fall back to the record state) before accepting the marker.
runtime.get("current_skill_hash") in (None, previous_skill_hash)
and runtime.get("history_hash") in (None, _json_digest(committed))
and runtime.get("step_record_hash")
in (None, _json_digest(last_record) if last_record is not None else "")
skillopt/engine/trainer.py:1341
- This clears the hash whenever a phase checkpoint calls
_persist_runtime_statewithoutstep_record(the meta-skill andcompletecalls do exactly that). Recovery treatsNoneas an opt-out, so after an epoch/final checkpoint it no longer validates the last step record against the runtime marker. Preserve the hash forhistory[-1]when it is the record forlast_completed_step, or pass that record at these call sites.
"step_record_hash": (
_json_digest(step_record) if step_record is not None else None
),
skillopt/engine/trainer.py:1304
- With skill-aware reflection enabled, this conditional persists raw
skill_initbefore theinject_empty_appendix_fieldcall at line 1314. The first step records its input hash after injection, while recovery computes the previous hash from rawskill_v0000and rejects step 1 on the next startup, so a baseline resume becomes unrecoverable after that step. Persist the normalized initial skill before starting/resuming.
if not (runtime_state or history) or not os.path.exists(skill_zero_path):
_save_skill(out_root, 0, skill_init)
skillopt/engine/trainer.py:638
- The normalization above replaces any non-list history with
[], sonot isinstance(history, list)is always false here. For a non-list or otherwise invalidhistory.json,changedis also false whencommittedis empty, meaning the invalid file is never rewritten and subsequent startups continue without a usable history document. Preserve a validity flag before normalization and rewrite invalid history.
if changed or (os.path.exists(history_path) and not isinstance(history, list)):
skillopt/engine/trainer.py:1230
- For legacy runtime markers,
best_skill_hashis absent, so this validation is skipped andbest_skill.mdis trusted. The old writer updated that convenience file before publishing its runtime marker; a crash can therefore leave it at an uncommitted step, and the resumed gate will use the wrong best skill. When the hash is absent, rebuild/validatebest_skillfrom the versionedbest_stepsnapshot instead of accepting this file.
expected_best_hash = runtime_state.get("best_skill_hash")
if expected_best_hash and skill_hash(best_skill) != expected_best_hash:
skillopt/engine/trainer.py:636
- Pre-v2 runtime markers are valid dictionaries but have no
phase. Defaulting them tostepmakes an epoch-boundary legacy checkpoint delete that epoch'sslow_updateandmeta_skilldirectories, including completed done artifacts. The next resume then reruns potentially nondeterministic LLM updates and can produce a different skill; missing legacy phase should preserve structurally complete artifacts or otherwise distinguish unknown phase from an in-progress step.
phase=str(runtime.get("phase", "step")) if runtime else "step",
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Improve checkpoint consistency when resuming trainer runs.
No new CLI flags or configuration options are required.
Problem
The previous resume logic primarily relied on
runtime_state.last_completed_step, or the last history entry, without cross-validating all related step records and skill snapshots.If a run is interrupted while checkpoint files are being written, there is a possibility that runtime state, history, skill snapshots, step directories, and learning-rate history may reflect slightly different progress.
For a mid-epoch resume, the accumulated step buffer may also be unavailable, and scheduler progress may be reconstructed from the global step number instead of its previously persisted internal state.
Changes
On startup, the trainer now finds the longest contiguous sequence of valid committed steps.
If the last valid step is
N, it:NNN + 1New checkpoints include skill hashes, commit IDs, scheduler state, and the current commit phase.
runtime_state.jsonis written last and acts as the commit marker. History, runtime state, skill snapshots, and step records are written atomically.Compatibility
Tests
Focused resume-related tests: