Skip to content

Checkpoint write throughput regresses ~41% on POSIX/local_fs (mlpstorage 3.0.26 → 3.0.33) - Materially change of results #682

Description

@wolfgang-desalvador

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
forkforkserver 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.
.

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions