Severity: High — regression landed inside the submission window and materially changes reported results.
Summary
llama3-8b checkpoint save throughput dropped ~41% between mlpstorage
3.0.26 and 3.0.33 on an identical cluster, workload, and DLIO checkpoint
code path (POSIX filesystem, storage_type: local_fs).
| Version |
Commit |
Checkpoint save I/O (GiB/s) |
| 3.0.26 |
6576710 |
~12.4 |
| 3.0.33 |
75414e4 |
~7.4 |
Δ = ~−41% on the same hardware and workload.
Environment
- Model / mode:
llama3-8b, checkpointing, num_checkpoints_write=10
- Ranks:
num_processes=8 (2 × 4), dual-socket 192 vCPU clients
- Target: POSIX filesystem —
storage_type: local_fs, o_direct: false,
fsync: true
Root cause
AI Analysis below, which has been proven to be correct on real tests.
The regression is caused by PR #650 / fix(#642), which changed the
streaming-checkpoint writer subprocess context from fork to forkserver
in mlpstorage_py/checkpointing/streaming_checkpoint.py:
# 3.0.26 (~12.4 GiB/s)
ctx = mp.get_context('fork')
# 3.0.33 (#642, ~7.4 GiB/s)
ctx = _writer_mp_context() # prefers forkserver, falls back to spawn
#642 was a correct fix for a fork-after-live-Tokio deadlock on the
object-store path (the parent starts a Tokio runtime via
ObjStoreLibStorage._preflight() before forking). However it was applied
unconditionally, including the backend='file' / local_fs path, which
never starts a parent-side Tokio runtime and therefore never had the
deadlock. forkserver writers spawn from a clean daemon started before
MPI/NUMA placement, losing the parent's copy-on-write warm state and CPU/NUMA
locality with the shared-memory buffers, consistent with the ~41% drop.
Profiling evidence
Per-checkpoint profiling on the post-#650 (forkserver) code shows the write
pipeline is starved: total time is dominated by the generation/writer split
rather than the storage device (close/fsync is negligible). Illustrative
profile output:
================================================================================
RESULTS
================================================================================
Generation: 4.1571s @ 0.45 GB/s
I/O: 1.3122s @ 1.42 GB/s
- write: 1.3049s
- close: 0.0073s (fsync/finalize)
Total: 4.3688s
Throughput ratio: 0.3x (gen/io)
Pipeline overhead: 4.8%
Bottleneck: Generation
Chunks: 60
Submission-window impact (why this invalidates results)
The performance-affecting change was merged to main inside the submission
window:
| Change |
PR |
Commit |
Arrived on main |
fork → forkserver writer context (root cause) |
#650 (fix #642) |
db4e4f4 |
2026-07-02 |
Because a performance-affecting code change (db4e4f4, 2026-07-02) landed on
main during the submission window, results captured against the pre-#650 code
(3.0.26, ~12.4 GiB/s) and results captured against post-#650 code (3.0.33,
~7.4 GiB/s) are not comparable, and any submission spanning this boundary is
invalid: the same run configuration yields a ~41% different headline metric
depending solely on which side of db4e4f4 the benchmark code sits.
.
Summary
llama3-8bcheckpoint save throughput dropped ~41% between mlpstorage3.0.26 and 3.0.33 on an identical cluster, workload, and DLIO checkpoint
code path (POSIX filesystem,
storage_type: local_fs).657671075414e4Δ = ~−41% on the same hardware and workload.
Environment
llama3-8b, checkpointing,num_checkpoints_write=10num_processes=8(2 × 4), dual-socket 192 vCPU clientsstorage_type: local_fs,o_direct: false,fsync: trueRoot cause
AI Analysis below, which has been proven to be correct on real tests.
The regression is caused by PR #650 / fix(#642), which changed the
streaming-checkpoint writer subprocess context from
forktoforkserverin
mlpstorage_py/checkpointing/streaming_checkpoint.py:#642 was a correct fix for a
fork-after-live-Tokio deadlock on theobject-store path (the parent starts a Tokio runtime via
ObjStoreLibStorage._preflight()before forking). However it was appliedunconditionally, including the
backend='file'/local_fspath, whichnever starts a parent-side Tokio runtime and therefore never had the
deadlock.
forkserverwriters spawn from a clean daemon started beforeMPI/NUMA placement, losing the parent's copy-on-write warm state and CPU/NUMA
locality with the shared-memory buffers, consistent with the ~41% drop.
Profiling evidence
Per-checkpoint profiling on the post-#650 (
forkserver) code shows the writepipeline is starved: total time is dominated by the generation/writer split
rather than the storage device (
close/fsync is negligible). Illustrativeprofile output:
Submission-window impact (why this invalidates results)
The performance-affecting change was merged to
maininside the submissionwindow:
mainfork→forkserverwriter context (root cause)db4e4f4Because a performance-affecting code change (
db4e4f4, 2026-07-02) landed onmainduring the submission window, results captured against the pre-#650 code(3.0.26, ~12.4 GiB/s) and results captured against post-#650 code (3.0.33,
~7.4 GiB/s) are not comparable, and any submission spanning this boundary is
invalid: the same run configuration yields a ~41% different headline metric
depending solely on which side of
db4e4f4the benchmark code sits..