Article

Distributed LLM Fine-Tuning Checkpoint Resume in Production: Sharded Checkpoints, Atomic Commits, and World Size Changes

A practical guide to production-grade checkpoint and resume for distributed LLM fine-tuning. Covers sharded checkpoints, atomic commits, optimizer/RNG state recovery, world size changes, and fault injection drills to prevent silent training drift.

Background: A Checkpoint Exists ≠ Training Can Resume

In single-node fine-tuning, torch.save(model.state_dict()) is often sufficient for model export. But with FSDP, ZeRO, Tensor Parallel, or Pipeline Parallel, training state is scattered across multiple ranks, and a checkpoint transforms from “one file” into a set of interdependent shards.

Production incidents rarely involve a complete absence of checkpoints. Instead, you encounter:

  • Some ranks finish writing, others only write halfway before a node failure;
  • latest points to a new directory, but the new checkpoint hasn’t been fully committed;
  • Model weights restore successfully, but optimizer momentum, learning rate, or GradScaler are not restored;
  • The dataset is re-read from the start of an epoch, causing sample duplication;
  • The original training used 64 GPUs, but only 48 are available for resume, and old shards cannot be loaded into the new topology;
  • Async saving backlogs, consuming all host memory, eventually dragging down the training process.

Therefore, the goal of production-grade checkpoint resume isn’t “finding a batch of files.” It’s ensuring a checkpoint simultaneously meets four conditions: state completeness, commit determinability, topology compatibility, and resume verifiability.

PyTorch Distributed Checkpoint (DCP) supports parallel save and load across multiple ranks and can re-shard across different world sizes in supported scenarios based on the loading side’s shard information. It produces multiple files, not a single traditional torch.save file, and loading requires the target model to first allocate state storage and provide shard information.

Core Principle: Treat a Resume Point as a Distributed Transaction

1. Define the Training State Boundary

A resumable checkpoint should cover at least:

  1. Model State: Base model parameters, LoRA or other adapter parameters, trainable heads.
  2. Optimizer State: Adam first and second moments, parameter group settings, ZeRO/FSDP shards.
  3. Scheduler State: Current learning rate, warmup progress, scheduler internal counter.
  4. Numerical State: GradScaler, mixed precision configuration, gradient accumulation micro-step.
  5. Random State: Python, NumPy, PyTorch CPU, each CUDA device, and data sampler RNG.
  6. Progress State: Global step, epoch, consumed samples, data cursor.
  7. Configuration Fingerprint: Model architecture, tokenizer, chat template, dataset version, training parameters, and code version.
  8. Parallel Topology: World size, DP/TP/PP dimensions, ZeRO stage, FSDP sharding strategy.

Hugging Face Accelerate’s checkpoint interface saves the model, optimizer, RNG, and GradScaler, and also allows registering custom stateful objects like learning rate schedulers. If resuming mid-epoch, you also need to restore or skip the already consumed DataLoader batches. This detail is often missed: identical weights do not guarantee the model sees the same next sample.

2. Save in Sharded Format, Avoid Forced Aggregation on the Training Path

In large model training, aggregating all shards into a single full-precision FP32 state dict can cause significant communication, CPU memory, and save-pause overhead. A more sensible primary checkpoint is a Sharded Checkpoint: each rank writes its own shard in parallel, while saving enough metadata for the loading side to reorganize.

Full weight export and fault recovery are two different goals:

GoalTraining Resume CheckpointRelease Artifact
Primary ConcernFast write, complete state, re-shardableUniversal format, stable file, easy for inference loading
Write MethodEach rank writes shards in parallelAggregate into full weights
LifecycleFrequently generated during training, cleaned after resumeLong-term retention, consumed downstream

Don’t force full weight aggregation for every training checkpoint just to make publishing easier. DeepSpeed provides tools to extract FP32 weights from ZeRO checkpoints, but the official documentation warns that this step can be CPU memory-bound, and loading aggregated weights back into the model usually requires re-initializing the DeepSpeed Engine.

3. Use Commit Markers to Distinguish “File Exists” from “Resume Point Valid”

It’s recommended to split a save operation into two phases:

Phase A: Write to a Temporary Prefix

checkpoints/
  step-00012000.inprogress/
    rank-00000.distcp
    rank-00001.distcp
    ...
    metadata.part.json

After each rank finishes writing, record the file size, checksum, and state summary. The coordinating rank only enters the commit phase after confirming all expected shards exist and pass verification.

Phase B: Publish Commit Evidence

Object storage typically shouldn’t rely on directory renames to simulate atomic operations. A more robust engineering approach is to use immutable prefixes, then write a very small COMMITTED file or Manifest, and finally update the latest.json pointer. The recovery side only acknowledges versions with valid commit evidence.

{
  "checkpoint_id": "step-00012000",
  "status": "COMMITTED",
  "global_step": 12000,
  "saved_world_size": 64,
  "parallelism": { "dp": 8, "tp": 8, "pp": 1 },
  "training_state": {
    "epoch": 2,
    "micro_step": 3,
    "consumed_samples": 1572864
  },
  "fingerprints": {
    "model_config": "sha256:...",
    "tokenizer": "sha256:...",
    "dataset_manifest": "sha256:...",
    "training_config": "sha256:..."
  },
  "shards": [
    { "rank": 0, "path": "rank-00000.distcp", "size": 4831838208, "sha256": "..." }
  ]
}

latest.json is just an index, not a source of truth. Even if the pointer is corrupted, the system should be able to scan recent committed Manifests for fallback, rather than selecting the version with the largest directory name.

Engineering Implementation

Save Flow: Freeze Consistent State First, Then Async Persist

PyTorch DCP’s async_save first copies the training state to an internal CPU buffer, then writes to storage asynchronously, reducing blocking time on the GPU training main path. But this isn’t a zero-cost operation: the official documentation notes that CPU memory pressure is roughly proportional to the per-rank checkpoint size and the number of ranks; using Pinned Memory can accelerate staging copies but causes peak pinned memory to remain resident long-term.

A simplified save controller:

from concurrent.futures import Future
import torch.distributed as dist
import torch.distributed.checkpoint as dcp

checkpoint_future: Future | None = None

def save_checkpoint(state: dict, checkpoint_id: str) -> Future:
    global checkpoint_future
    # Avoid piling up multiple async save tasks
    if checkpoint_future is not None:
        checkpoint_future.result()
    checkpoint_future = dcp.async_save(
        state_dict=state,
        checkpoint_id=f"{checkpoint_id}.inprogress",
    )
    return checkpoint_future

def commit_checkpoint(checkpoint_id: str) -> None:
    assert checkpoint_future is not None
    checkpoint_future.result()
    dist.barrier()
    if dist.get_rank() == 0:
        verify_all_shards(checkpoint_id)
        write_manifest(checkpoint_id)
        publish_latest_pointer(checkpoint_id)
    dist.barrier()

Here, verify_all_shards, write_manifest, and publish_latest_pointer are business-side commit protocols, not a complete transaction system automatically provided by DCP. The key constraint is: an incomplete Future cannot be treated as a successful checkpoint, and a directory without a published Manifest cannot be used for resume.

Resume Flow: Compatibility Check Must Precede Loading

After a resume task starts, it’s recommended to follow this order:

  1. Find the most recent committed Manifest.
  2. Verify the Manifest, shard count, sizes, and checksums.
  3. Compare model architecture, tokenizer, dataset, and training configuration fingerprints.
  4. Create a new Device Mesh or parallel topology based on the target GPU count.
  5. Construct and pre-allocate the target model and optimizer state dicts.
  6. Load and re-shard the model and optimizer states.
  7. Restore the Scheduler, GradScaler, RNG, global step, and data cursor.
  8. Execute post-resume validation before allowing the task to enter formal training.
state = build_stateful_training_state(
    model=model,
    optimizer=optimizer,
    scheduler=scheduler,
    scaler=scaler,
)
dcp.load(
    state_dict=state,
    checkpoint_id=selected_checkpoint,
)
restore_rng_state(manifest["rng_state"])
restore_data_cursor(manifest["training_state"])
validate_resume_state(model, optimizer, manifest)

DCP loading is in-place, relying on the target model’s already allocated state dict and shard information. Therefore, the order “read the checkpoint first, then decide the new topology” is wrong; you should first decide the resume resources and shard layout, then perform the load.

World Size Changes: Handle in Three Categories

Category 1: Only Data Parallelism Changes. When the model architecture and supported sharding strategy remain unchanged, DCP can re-shard based on the target side’s shard information. This type of change is the most suitable first-stage goal for elastic recovery.

Category 2: Changes in TP, PP, DP, or ZeRO Combinations. This typically requires a universal checkpoint format or framework-specific conversion. DeepSpeed documentation marks Universal Checkpoints as a still-in-development capability for supporting GPU count changes under 3D Parallelism. The core idea of the Universal Checkpointing paper is to decouple the distributed representation at save time from the unified parameter representation and mapping metadata at load time.

In production, you cannot assume any combination is recoverable just because a feature is named “Universal.” A clear compatibility matrix must be established:

resume_compatibility:
  - from: {dp: 8, tp: 8, pp: 1, zero: 1}
    to: {dp: 6, tp: 8, pp: 1, zero: 1}
    status: verified
  - from: {dp: 8, tp: 8, pp: 1, zero: 1}
    to: {dp: 8, tp: 4, pp: 2, zero: 1}
    status: blocked_until_drill_passes

Category 3: Changes in Model Architecture or State Semantics. For example, adding a trainable layer, changing the optimizer, adjusting the vocabulary, or switching ZeRO stages. These cannot be simply treated as world size changes. They should enter an explicit migration flow; if no migrator exists, you must Fail Closed rather than skipping missing keys and continuing training.

How to Prove “It Really Resumed”

Successful loading only proves deserialization didn’t error. Production acceptance also requires short-run comparisons:

  • Global step, learning rate, and GradScaler match the save point.
  • Optimizer parameter groups and momentum tensor counts match.
  • The data cursor hasn’t returned to the start of an epoch.
  • The first batch ID after resume is as expected.
  • The next step’s loss, gradient norm, and parameter update magnitude show no anomalous jumps.
  • After running several consecutive steps, the metric difference from an uninterrupted control task is within a predefined tolerance.

Distributed GPU training may not achieve bit-exact consistency across all kernels, topologies, and parallel configurations. Therefore, resume acceptance shouldn’t just compare final weight hashes. A more practical goal is to verify state continuity, no missing or duplicate data, and no unexplained trajectory mutations.

Fault Injection Drills: Don’t Wait for the First Incident to Validate Recovery

It’s recommended to periodically execute the following drills in a pre-production environment:

  1. Kill a process while some ranks are writing, verify .inprogress is not selected.
  2. Delete one shard, verify the recovery side rejects loading and falls back to the previous commit point.
  3. Tamper with file content, verify the checksum guardrail activates.
  4. Point latest.json to a non-existent version, verify Manifest scan fallback.
  5. Resume with a different world size, verify the re-sharding compatibility matrix.
  6. Save and resume mid-gradient-accumulation, verify the micro-step doesn’t drift.
  7. Simulate object storage timeout, verify the async save Future doesn’t backlog indefinitely.
  8. After resume, run a short-run comparison on fixed samples, verify loss and data cursor.

A checkpoint system without fault injection is just an unvalidated backup script.

Monitoring Metrics and Alerts

At a minimum, record the following metrics:

MetricMeaning
checkpoint_save_duration_secondsDuration of a single save operation
checkpoint_async_staging_secondsDuration of the async staging phase
checkpoint_bytes_totalTotal checkpoint size
checkpoint_commit_lag_secondsLatency from save completion to commit completion
checkpoint_inprogress_countNumber of currently incomplete checkpoints
checkpoint_last_committed_stepMost recently committed global step
checkpoint_restore_duration_secondsDuration of a restore operation
checkpoint_restore_failures_total{reason}Number of restore failures, categorized by reason
checkpoint_checksum_failures_totalNumber of checksum failures
checkpoint_reprocessed_samples_totalNumber of samples reprocessed after resume
resume_loss_deltaLoss difference before and after resume
resume_world_size_change_totalNumber of world size change resumes

Alerts should not just look at “whether a directory was recently created.” They should look at how far the current training progress is from the most recently committed and drill-validated resume point. This is the true Recovery Point Objective (RPO) for the training task.

Applicable Scenarios

This approach is particularly suitable for:

  • Cross-node, long-running SFT, DPO, and continual pre-training tasks.
  • Training clusters using preemptible or elastic GPU resources.
  • Models using FSDP, ZeRO, or hybrid parallelism that cannot be fully saved as a single file.
  • Tasks where the GPU count may be adjusted during training, but the optimizer trajectory must be preserved.
  • Enterprise environments with strict requirements for no missing/duplicate samples, training auditability, and cost control.

For small LoRA tasks that complete in a few hours, you don’t need to copy the entire complex system. However, it’s still recommended to save the optimizer, scheduler, RNG, data cursor, and configuration fingerprints, rather than just the adapter weights.

Common Misconceptions

Misconception 1: If the model loads, training can resume. Restoring only model parameters loses optimizer momentum, learning rate progress, and numerical scaling state. The result is closer to “restarting training from old weights” than a true resume.

Misconception 2: The directory with the largest name is the latest checkpoint. A half-written directory can also have the largest step. Validity must be judged by the Manifest and commit marker.

Misconception 3: All ranks writing to the same file is simpler. This easily leads to overwrites, lock contention, and corruption. Distributed training should use the framework’s supported sharded save protocol.

Misconception 4: Async saving can be infinitely concurrent. Each task requires CPU staging and I/O. PyTorch’s official recommendation is that most users should keep only one async request at a time to avoid extra memory pressure.

Misconception 5: Variable world size means all parallelism dimensions are variable. Changes in DP count, TP/PP, and model architecture are different levels of migration and must be validated separately.

Misconception 6: It’s okay to re-shuffle data on resume. Duplicate or skipped samples change the training distribution, especially affecting short datasets, preference data, and data recipes with staged mixing.

Go-Live Checklist

  • State boundaries defined for model, optimizer, scheduler, scaler, RNG, and data cursor.
  • Each checkpoint has an immutable ID, Manifest, checksum, and commit marker.
  • Uncommitted directories are not selected by latest or automatic recovery logic.
  • Object storage publishing does not rely on directory renames.
  • Async save runs at most one task concurrently, with a CPU/Pinned Memory budget.
  • Compatibility matrix established between original and target world sizes.
  • Model, tokenizer, dataset, code, and training config fingerprints verified before resume.
  • LR, scaler, global step, data cursor, loss, and gradient norm validated after resume.
  • Drills performed for missing shards, corrupted shards, rank interruption, pointer corruption, and storage timeout.
  • Monitoring focuses on the most recently committed resume point, not the most recently created directory.
  • At least one previous healthy checkpoint is retained to allow fallback if the latest version is corrupted.
  • Release artifact export and training resume checkpoints use different lifecycles.

References

  1. PyTorch — Getting Started with Distributed Checkpoint
  2. PyTorch — Asynchronous Saving with Distributed Checkpoint
  3. Hugging Face Accelerate — Checkpointing
  4. DeepSpeed — Model Checkpointing
  5. Universal Checkpointing: A Flexible and Efficient Distributed Checkpointing System for Large-Scale DNN Training with Reconfigurable Parallelism

FAQ

Can I resume training from just the model weights?
Usually not. Reliable resume requires restoring the optimizer, learning rate scheduler, GradScaler, random number generator states, training step, gradient accumulation position, and data loading progress. Otherwise, the learning rate, sample order, or optimization trajectory will change upon resume.
Can I load an old checkpoint directly after changing the number of GPUs?
It depends on the checkpoint format, parallelism strategy, and framework capabilities. PyTorch DCP supports re-sharding within its coverage based on target shard information. Changes involving combinations of TP, PP, ZeRO, etc., require framework-specific universal checkpoints or conversion flows, and compatibility drills should be completed first.
Is asynchronous saving always better than synchronous saving?
Not necessarily. Asynchronous saving reduces the training main-path pause but consumes CPU buffers, pinned memory, and storage bandwidth, and requires managing outstanding tasks. Production environments should typically limit to one concurrent async checkpoint and set guardrails on memory and commit latency.
If I only save model weights, why does the loss still decrease after resume?
A decreasing loss does not prove correct resume. The model may still train after changes to optimizer momentum, learning rate phase, random state, and sample order, but the trajectory has shifted. Production acceptance should check state continuity and short-run comparisons, not just whether the task continues to run.