Commit e2cad6c
Add on-demand full-state checkpointing for OpenShift AI / KubeFlow preemption (#686)
* Add on-demand full-state checkpointing for OpenShift AI / KubeFlow preemption
Implements signal-driven checkpoint-and-exit for distributed training jobs
running in OpenShift AI as KubeFlow training jobs or multi-node bare metal.
When `on_demand_checkpointing=True` is set in TrainingArgs:
- Parent process (run_training) installs handlers for SIGTERM, SIGINT,
SIGUSR1, SIGUSR2, SIGXCPU, and SIGHUP — covering all signals
Kubernetes/OpenShift sends before the hard SIGKILL.
- On signal receipt, a trigger file is atomically written to /dev/shm
(tmpfs, shared within the pod, zero disk I/O).
- Worker processes check for the trigger file after each optimizer step
via an all_reduce(MAX) collective, ensuring global consensus across
all ranks on all nodes.
- When any rank detects the trigger, all ranks collectively save a
full-state distributed checkpoint (model + optimizer + LR scheduler)
then exit gracefully.
- Parent waits up to 300s for workers to complete the checkpoint before
proceeding with normal shutdown.
https://claude.ai/code/session_01HSxsk7SnMULJxy7uafe7t3
* Address review feedback for on-demand checkpointing
- Fix mypy error: properly type _original_handlers dict with _SignalHandler
type alias instead of bare object
- Fix ruff/isort: remove duplicate comment, fix import ordering
- Namespace trigger file with rdzv_id as job_id so concurrent jobs sharing
/dev/shm don't interfere with each other
- Recompute subprocess failure status after forced termination to avoid
stale exit code
- Gate consensus log message to rank 0 to reduce log noise on large jobs
* Check for on-demand checkpoint after each minibatch backward
Move the checkpoint request check from after the full optimizer step to
after each minibatch's backward pass inside BatchLossManager.process_batch.
This ensures the system responds within one fwd+bwd cycle (~1-2s) even
when gradient accumulation spans many minibatches, giving more time to
save before Kubernetes sends SIGKILL after the grace period.
The check is passed as an optional interrupt_check callback to keep
checkpoint-specific logic out of BatchLossManager. When triggered, the
batch loop breaks early and the training loop saves the checkpoint
immediately, skipping the optimizer step to preserve the pre-step model
state for exact resumption.
* Add diagnostic note when on-demand checkpoint fails to save in time
When the training subprocess fails after an on-demand checkpoint signal
was received, the error message now includes guidance to increase
terminationGracePeriodSeconds or reduce fwd/bwd pass time so the
checkpoint check fires before SIGKILL arrives.
* Fix help text: checkpoint check happens after each minibatch backward
Update --on_demand_checkpointing help text and TrainingArgs description
to accurately state that workers check for the trigger file after each
minibatch backward pass, not after each training step.
* Add checkpoint checks at 5 synchronization points per training step
Expand on-demand checkpointing to check for a trigger at five points:
1. Before each minibatch forward pass
2. Before each minibatch backward pass
3. After each minibatch backward pass (existing)
4. Before the optimizer step
5. After the optimizer step
This minimizes the latency between a termination signal arriving and
the checkpoint being saved, which is critical when the SIGKILL grace
period is short (e.g. 30s on OpenShift/Kubernetes).
Also cleans up the save-and-exit logic in train() by extracting a
_save_and_exit() helper to eliminate three nearly identical blocks,
and fixes _compute_average_loss to handle the case where the
minibatch loop is interrupted before any forward pass completes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add unit tests for on-demand checkpointing
24 tests covering:
- Trigger file helpers (path generation, write, exists, remove)
- ParentSignalHandler (install, handle, idempotency, uninstall, real signal)
- check_checkpoint_requested (trigger detection, cleanup, all_reduce consensus)
- BatchLossManager interrupt handling (all 3 check points, early exit, float loss)
* fix: correct help text for on_demand_checkpointing sync point count
* Add stale trigger cleanup and exact mid-epoch resume
Two fixes to the on-demand checkpointing feature:
1. Stale trigger file cleanup: ParentSignalHandler.install() now checks
for and removes any existing trigger file before installing signal
handlers. If the file exists before handlers are installed, it's from
a previous run that was killed before workers could clean it up.
Prevents a new training job from immediately checkpointing and exiting.
2. Exact mid-epoch resume: save_on_demand_checkpoint() now persists
global_step in the checkpoint metadata alongside current_epoch and
samples_seen. On resume, load_latest_full_state() detects the
global_step field and sets last_step accordingly, so the training
loop fast-forwards to the exact step within the epoch. Without this,
mid-epoch checkpoints would skip to the next epoch on resume, losing
remaining steps.
Tested with Qwen2-1.5B-Instruct on 2 GPUs: interrupted at step 19/25,
checkpoint saved with global_step=19, resumed and completed steps 20-25.
* Fix ruff formatting in utils.py
* Add documentation for on-demand checkpointing feature
* Document manual trigger file creation for on-demand checkpointing
* Improve manual trigger docs: clarify job ID requirement
* Simplify trigger file: remove job_id namespacing
Drop the job_id suffix from the trigger file path. The file is now
always /dev/shm/instructlab_checkpoint_requested with no suffix.
The namespacing was defensive against concurrent jobs sharing /dev/shm,
but in practice Kubernetes pods each get their own /dev/shm. This makes
manual triggering trivial: touch /dev/shm/instructlab_checkpoint_requested
* Fix pylint: remove unnecessary lambda wrapper
* fix: update unit tests for simplified trigger file API (no job_id)
* fix: add compat shim for Nemotron's is_flash_attn_greater_or_equal_2_10
The function was renamed to is_flash_attn_greater_or_equal in
transformers 5.x, but Nemotron's HF Hub remote code still imports
the old name. Inject a compatibility wrapper before model loading.
* fix: make unit tests work without CUDA
- Patch torch.tensor in check_checkpoint_requested tests to avoid
CUDA device creation (CI runs without GPUs)
- Override BatchLossManager.torch_device to CPU in fixture
so _prepare_model_inputs doesn't trigger CUDA init
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 091c21e commit e2cad6c
8 files changed
Lines changed: 1080 additions & 34 deletions
File tree
- docs
- src/instructlab/training
- tests/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| 37 | + | |
36 | 38 | | |
37 | 39 | | |
38 | 40 | | |
| |||
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
65 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
66 | 72 | | |
67 | 73 | | |
68 | 74 | | |
69 | 75 | | |
70 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
71 | 83 | | |
72 | 84 | | |
73 | 85 | | |
| |||
82 | 94 | | |
83 | 95 | | |
84 | 96 | | |
| 97 | + | |
85 | 98 | | |
86 | 99 | | |
87 | 100 | | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
88 | 106 | | |
89 | 107 | | |
90 | 108 | | |
| |||
96 | 114 | | |
97 | 115 | | |
98 | 116 | | |
99 | | - | |
| 117 | + | |
100 | 118 | | |
101 | 119 | | |
102 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
103 | 127 | | |
104 | 128 | | |
105 | 129 | | |
| |||
108 | 132 | | |
109 | 133 | | |
110 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
111 | 140 | | |
112 | 141 | | |
113 | 142 | | |
| |||
127 | 156 | | |
128 | 157 | | |
129 | 158 | | |
| 159 | + | |
130 | 160 | | |
131 | 161 | | |
132 | 162 | | |
| |||
165 | 195 | | |
166 | 196 | | |
167 | 197 | | |
168 | | - | |
169 | | - | |
| 198 | + | |
| 199 | + | |
170 | 200 | | |
171 | 201 | | |
172 | 202 | | |
| |||
177 | 207 | | |
178 | 208 | | |
179 | 209 | | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
180 | 217 | | |
181 | 218 | | |
182 | | - | |
183 | | - | |
184 | | - | |
| 219 | + | |
185 | 220 | | |
186 | 221 | | |
187 | 222 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
361 | 361 | | |
362 | 362 | | |
363 | 363 | | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
364 | 377 | | |
365 | 378 | | |
366 | 379 | | |
| |||
0 commit comments