Preemption recovery and resume
Preemption recovery and resume
This note documents the current preemption-handling and resumption design for synthetic-sequences training, with emphasis on the entangled trainer. The goal is to explain the code architecture, the operational contract between trainer and Slurm wrapper, and the design choices that future maintainers should preserve unless they are making a deliberate migration.
Scope
The primary implementation described here is the entangled recovery path in path:experiments/synthetic_sequences/entangled/resumption.py and path:experiments/synthetic_sequences/entangled/train_entangled_model.py.
The disentangled trainer already had most of the software-side recovery primitives. The related disentangled change in this work was mainly to tighten the Slurm wrapper so it launches via srun and uses the same signal-forwarding contract as the entangled wrappers.
Architecture map
- Shared preemption primitives live in
path:core/slurm/preemption.py. This is where the trainer-side signal handler and the resumable exit code contract live. - Shared auto-resume metadata helpers live in
path:core/slurm/auto_resume.py. This module ownsconfig_identity.json,run_status.json, and the config-hash helpers used to decide whether an existing run directory belongs to the current setting. - Entangled-specific recovery logic lives in
path:experiments/synthetic_sequences/entangled/resumption.py. That module owns run discovery, legacy-run bootstrap, resume-position arithmetic, shutdown checkpoint writing, and W&B run-id persistence. path:experiments/synthetic_sequences/entangled/train_entangled_model.pyintegrates the recovery flow into startup, checkpoint restore, loop re-entry, and graceful shutdown polling.path:experiments/synthetic_sequences/data/baseline_loader.pyaccepts an optionaltorch.Generatorso shuffled training order can be rebuilt deterministically after restart.path:experiments/synthetic_sequences/run_naming.pyexcludes recovery knobs such asenable_graceful_shutdown,auto_resume, andmax_resubmit_attemptsfrom run identity so resubmits map back to the original deterministic run directory.- The scheduler-facing contract lives in the Slurm templates:
path:experiments/synthetic_sequences/slurm/templates/run_entangled_pretrained.job.j2,path:experiments/synthetic_sequences/slurm/templates/run_entangled_task_size.job.j2, andpath:experiments/synthetic_sequences/slurm/templates/run_disentangled_beta.job.j2.
End-to-end recovery flow
- A fresh launch computes the deterministic run directory from the sanitized run config, collection slug, and seed.
- If
--auto-resumeis enabled and the user did not provide--resume-from, the entangled trainer checks whether that deterministic run directory already exists and whether it containscheckpoints/last.pt. - If the run directory has current metadata, the trainer compares the saved config hash to the current sanitized config hash. If the hashes differ, auto-resume refuses to attach to that run.
- If the run directory is a legacy run that has
last.ptbut noconfig_identity.json, the trainer recomputes the hash from the checkpoint's savedconfig_snapshot. If that matches, it bootstraps the missing metadata files and resumes the legacy run in place. - Model, optimizer, scheduler, and RNG restoration still go through the existing
resume_if_requested(...)path. Recovery did not introduce a second state-restore implementation. - Resume position is computed separately from checkpoint metadata as
(start_epoch, resume_batch_idx). The meaning ofresume_batch_idxis "how many effective batches instart_epochwere already fully committed before shutdown." - The training loop rebuilds the correct epoch-local dataloader and skips
batch_idx <= resume_batch_idxon the first resumed epoch. This is the core invariant that prevents replaying completed work. - When a shutdown signal is observed, the trainer saves an immediate step checkpoint, updates
last.pt, appends agraceful_shutdownevent, updatesrun_status.jsontopreempted, incrementsresubmit_count, finishes the tracker, and exits with code42. - The Slurm wrapper owns the scheduler-side behavior:
sruncreates the job step that receives signals, the batch shell forwardsSIGUSR1timeout warnings to the child, the batch shell ignoresTERM, and#SBATCH --requeuemakes preempted jobs eligible for scheduler-native requeue.
Why the design looks like this
Recovery knobs are not part of run identity
Recovery settings such as enable_graceful_shutdown, auto_resume, and max_resubmit_attempts are operational knobs, not experimental setting knobs. They are therefore excluded from run identity in path:experiments/synthetic_sequences/run_naming.py. If they were part of the identity hash, turning on recovery for an existing experiment would create a different run directory and defeat auto-resume.
Entangled resume is defined at effective-batch granularity
The important unit is the effective optimizer batch, not the micro-batch. The checkpoint already contains the optimizer and scheduler state after all committed effective batches. Resuming by replaying any completed effective batch would advance those states twice. This is why the trainer computes resume_batch_idx and explicitly skips already-finished effective batches on the first resumed epoch.
Explicit epoch-local shuffle seeds are easier to reason about than sampler internals
New entangled runs seed the shuffled training dataloader with seed + epoch. That makes the epoch-local record order reconstructible from small, explicit metadata rather than from opaque sampler state. This choice is simpler to inspect and easier to recover correctly after process death.
Legacy entangled runs are supported, but only as far as their old invariants allow
Older entangled checkpoints were created before explicit epoch-local shuffle seeds and before the current metadata files existed. The current code supports them by:
- validating identity from the checkpoint config hash when
config_identity.jsonis missing; - bootstrapping
config_identity.jsonandrun_status.jsonon first resumed launch; - treating missing train-order metadata as
legacy_random_sampler; - reconstructing the historical epoch-1 record ordering when resuming a legacy single-long-epoch run.
This is intentionally a compatibility bridge, not a timeless second design. The exactness guarantee is strongest for the current single-long-epoch experiments that motivated the change.
The batch shell must not own the preemption TERM
On this cluster, preemption signals are delivered to job steps started with srun, while timeout warnings configured with #SBATCH --signal=B:SIGUSR1@... are delivered to the batch shell. The templates therefore use two different mechanisms:
trap '' TERMin the batch shell so thesrunstep and Python signal handler own the preemption path;- an explicit
USR1trap in the batch shell so timeout warnings can be forwarded to the child process.
Without srun, or if the batch shell handled TERM itself, the trainer would not reliably receive the signal that is supposed to trigger checkpointing.
run_status.json is mutable control state; checkpoint config is immutable identity evidence
run_status.json answers operational questions such as whether the run is currently running, preempted, or completed, what the last signal was, and how many resumable exits have occurred. The checkpoint's saved config snapshot, by contrast, is used as immutable evidence about what experiment setting produced the run. Keeping these roles separate makes recovery decisions easier to debug.
W&B continuity is opportunistic, not required for filesystem continuity
The recovery path saves wandb_run_id.txt once a tracker exists and reuses it on later launches. If that file is absent, the run can still resume correctly from the filesystem and checkpoint state. W&B continuity is best-effort metadata, not a prerequisite for training continuity.
Submission-layer note: snapshot mode on resubmit
The resubmit path can start from already-generated collection rows. If a previous generation enabled job-code snapshots, those rows may still carry old snapshot metadata fields even after a later resubmit turns snapshot mode off.
The current code handles this in two places:
path:core/slurmkit/job_code_snapshots.pynow clears stale snapshot metadata whenjob_code_snapshot_enabledis false;- the templates parse
job_code_snapshot_enabledexplicitly instead of relying on the truthiness of strings like"false".
This matters operationally because a resubmitted job should not keep cd-ing into a stale detached worktree after snapshot mode has been disabled.
What future developers should preserve
- Auto-resume must stay tied to deterministic run identity, not to ad hoc filesystem scans.
resume_if_requested(...)should remain the single source of truth for model/optimizer/scheduler/RNG restoration.- Resume-position arithmetic and dataloader reconstruction should stay explicit and inspectable.
- The trainer and Slurm template together define the recovery contract. Changing one side without the other is risky.
- Exit code
42is part of the operational contract, not an incidental magic number. - Legacy support should be removed only as a deliberate migration after the old collections no longer matter.
Debugging checklist
When a run does not resume as expected, inspect these first:
run_status.json: current status,resubmit_count, andlast_signalconfig_identity.json: whether the run directory belongs to the current settingcheckpoints/last.pt: whether a resumable checkpoint actually existsevents.jsonl: whether agraceful_shutdownevent was writtenwandb_run_id.txt: whether W&B continuity should be expected- the trainer log line beginning with
resumed from checkpoint:: this printsglobal_step,start_epoch,resume_batch_idx, andtrain_order_mode
Tests that protect this contract
The most relevant tests are:
path:experiments/synthetic_sequences/entangled/tests/test_resumption.pypath:experiments/synthetic_sequences/entangled/tests/test_train_entangled_pretrained_smoke.pypath:experiments/synthetic_sequences/slurm/tests/test_template_recovery_contracts.pypath:experiments/synthetic_sequences/slurm/tests/test_sigterm_trap.pypath:experiments/synthetic_sequences/slurm/tests/test_job_code_snapshots.pypath:tests/experiments/synthetic_sequences/test_run_naming.py