We had a pipeline that could run for hours. GPU time is expensive. A crash at 95%
was the same as a crash at 0% — except we were angrier.
I got tired of it. Not in the "we need a framework" way. In the "this is stupid" way.
What we had before
A long script:
research → script → render (forever) → assemble → done If the process died in render, we started over. Every time.
People say "use a job queue". We had workers. We still lost time, because
progress lived only in RAM.
The fix (boring on purpose)
After every stage, write down that you finished it.
research [done] render [done] ← crash here? next run skips research+render assemble [pending] Then:
-
run()executes pending stages -
resume()skipsdone, retriesfailed - state is written atomically (tmp file → rename), so a kill mid-write doesn't corrupt everything
- workers send a heartbeat; watchdog notices when they go silent
That's it. No magic scheduler. Just refusing to forget finished work.
Why not "just checkpoint tensors"?
We don't need mid-layer optimizer state for every job. Most of our cost is
stage-level work: research, render, package. Stage checkpoints cover 95% of the pain
with 5% of the complexity.
Trade-off on purpose: a stage still re-runs from its start. That's acceptable.
Losing the whole pipeline is not.
What broke along the way
- Non-atomic state writes. Kill during save → corrupted JSON → resume lied to us. Fixed with write-to-temp +
os.replace. - Retries that duplicated side effects. Stage contract now says: re-entry must be safe (idempotent), or the stage owns its own local temp dir.
- "Busy" hung workers. Looked alive in the process list. Heartbeat file + staleness check.
Numbers
- Long jobs: tens of minutes to hours
- Before: crash = full re-run
- After: crash = re-run from last finished stage
- Tests: 12 behavioral contracts (resume, fail-fast, atomic state, stale heartbeat)
Practical checklist (copy/paste)
- [ ] Checkpoint after each stage, not "sometime later"
- [ ] Atomic write (tmp + rename). Kill mid-write must not corrupt state
- [ ]
resume()skipsdone, retriesfailed - [ ] Stages should be idempotent on re-entry
- [ ] Heartbeat file + staleness check for hung workers
- [ ] Fail-fast on poison stages
- [ ] Test: crash after stage 1 → resume does stage 2 only
Rule of thumb
If re-running the job costs more than 5 minutes — checkpoint it.
If it costs GPU-hours — checkpoint it today.
Takeaways
- Checkpoint I/O is cheap. Re-running 3 hours of GPU is not.
- Atomic writes matter more than fancy progress bars.
- Resume is a feature, not an ops afterthought.
- Heartbeat ≠ "it works". It's only "it's alive".
- Fail-fast on poison stages beats "keep going and hope".
Open-sourced here: checkpointed-task-pipeline
Related reliability pieces: