Checkpointing in NeMo AutoModel

View as Markdown

Introduction

During machine learning experiments, the model training routine regularly saves checkpoints. A checkpoint is a complete snapshot of a run that includes model weights, optimizer states, and other metadata required to resume training exactly where it left off. Writing these snapshots at regular intervals lets you recover quickly from crashes or pauses without losing progress.

NeMo AutoModel checkpoints capture the complete state of a distributed training run across multiple GPUs or nodes. This approach reduces memory overhead, improves GPU utilization, and allows you to resume training with a different parallelism strategy.

NeMo AutoModel writes checkpoints in two formats: Hugging Face Safetensors and PyTorch Distributed Checkpointing (DCP). It also supports two layouts:

  • Consolidated Checkpoints: The complete model state is saved as a Hugging Face-compatible bundle, typically in a single file or a compact set of files with an index. Because tensors are not split across GPUs (unsharded), tools such as Hugging Face, vLLM, and SGLang can load these checkpoints directly.

  • Sharded Checkpoints: During distributed training with parameter sharing, each GPU holds a subset (or “shard”) of the full state, such as model weights and optimizer states. When checkpointing, each GPU writes its own shard independently without reconstructing the full model state.

The following table provides an overview of the available checkpoint formats.

TaskModel DomainDCP (Sharded)Safetensors (Sharded)Safetensors (Consolidated)
SFTLLM
SFTVLM
PEFTLLM / VLM🚧🚧

Change output formats through the recipe’s YAML configuration file:

1checkpoint:
2 ...
3 model_save_format: safetensors # Format for saving (torch_save or safetensors)
4 save_consolidated: final # Recommended: export consolidated HF weights only for the final checkpoint.
5 # Other modes: false (sharded only) or every/true (export every checkpoint).
6 consolidation_timeout_minutes: 30 # Timeout for inline consolidated-export synchronization.
7 ...

save_consolidated accepts the following values:

  • final (recommended): Keep intermediate checkpoints sharded and export consolidated Hugging Face weights only for the final checkpoint.
  • false: Save sharded checkpoints only. Run the generated model/consolidate.sh helper later if you need Hugging Face weights.
  • every (or legacy true): Export consolidated Hugging Face weights during every checkpoint save. Use this only when every checkpoint must be immediately loadable by Hugging Face tools.

AutoModel writes a model/consolidate.sh helper next to Safetensors model shards. Use this helper to create a Hugging Face-compatible model/consolidated/ directory after training for save_consolidated: false checkpoints, or for earlier checkpoints when using save_consolidated: final. Creating consolidated Hugging Face weights requires model_save_format: safetensors.

Inline consolidated export synchronizes ranks through a dedicated Gloo process group. consolidation_timeout_minutes controls that synchronization independently of dist_env.timeout_minutes, which continues to control training collectives. The default is 30 minutes.

The optimizer states are always saved in DCP format with the .distcp extension.

NeMo AutoModel automatically creates symbolic links in the checkpoint directory to provide convenient access to important checkpoints:

  • LATEST: Points to the most recently saved checkpoint. This is useful for resuming training from the last saved state.
  • LOWEST_VAL: Points to the checkpoint with the lowest validation score or loss. This provides easy access to the best-performing checkpoint based on validation metrics, making it ideal for model evaluation or deployment.

These symbolic links eliminate the need to manually track checkpoint names or search through directories to find the best model. When validation is enabled in your training run, both links are automatically maintained and updated as training progresses.

Interrupted Saves

A save can be cut short by a wall-clock limit, a preemption, or an out-of-memory error, which leaves a partially written epoch_<E>_step_<S> directory behind. NeMo AutoModel marks each checkpoint directory as in progress while it is being written and clears the mark only after every component is saved and the checkpoint is published. Directories left by an interrupted save stay marked, which determines how they are treated:

  • Resume skips them. A checkpoint becomes resumable only after its marker is cleared. Both the LATEST pointer lookup and the step-order fallback ignore marked directories. Training resumes from the most recent complete checkpoint. Setting checkpoint.restore_from directly to a marked directory, or to a different pointer such as LOWEST_VAL whose target is marked, raises an error instead of loading an incomplete checkpoint.
  • The next save of that step replaces them. Checkpoint directory names are derived from the step, so the resumed run targets the same directory when it reaches that step again. Because the marked directory holds a checkpoint that was never published, NeMo AutoModel removes it, logs a warning, and writes the new checkpoint in its place.
  • Retention does not preserve them. When max_recent_checkpoints is set, a marked directory never occupies a slot in the recent window, so it is pruned as an orphan.

Only marked directories are replaced. A published checkpoint is never overwritten: if you resume from an older checkpoint with restore_from and training runs past a step that already has a published checkpoint, the save fails with FileExistsError rather than discarding it. Remove that directory, or point checkpoint.checkpoint_dir at a fresh directory, before rerunning the step.

Checkpoint saves are synchronous by default and are published before training continues. With opt-in asynchronous checkpointing (checkpoint.is_async: true), the next save call or a clean training shutdown waits for the background write and publishes the checkpoint. If the job terminates before either point, the pending checkpoint stays marked as in progress (even if its file writes finished), and resume uses the previous published checkpoint.

msc:// paths use NVIDIA MultiStorageClient (MSC) object storage. NeMo AutoModel supports them for sharded Distributed Checkpoint (DCP) model and optimizer data when checkpoint.save_consolidated is false. However, a full recipe checkpoint is not self-contained in object storage. RNG, dataloader, and other recipe state written with torch.save, along with the LATEST and LOWEST_VAL pointers, remain local to the training environment. The interrupted-save marker and automatic stale-directory replacement described above also require a local filesystem and are unavailable for msc:// roots.

Use a local or shared filesystem checkpoint directory when you need a portable, exact training resume. If you use an msc:// checkpoint directory for DCP data, configure it as follows:

1checkpoint:
2 checkpoint_dir: msc://bucket/path
3 save_consolidated: false
4 max_recent_checkpoints: null

Checkpoint Retention

By default, NeMo AutoModel keeps all intermediate checkpoints. To limit disk usage when checkpoints are saved frequently, configure a bounded recent-checkpoint window:

1checkpoint:
2 max_recent_checkpoints: 2

max_recent_checkpoints sets the number of checkpoint directories in the recent window, ordered by step. It is not a hard limit on the total number of checkpoint directories.

Checkpoint-root pointers protect their targets in addition to the recent window. These pointers include LATEST, LOWEST_VAL, other top-level symbolic links, and .txt fallback pointer files. This behavior supports reliable resumption from the latest checkpoint and preserves explicitly referenced checkpoints, such as the best validation checkpoint.

For example, set max_recent_checkpoints: 2 to retain the two most recent checkpoints. If LOWEST_VAL points to an older checkpoint, NeMo AutoModel also retains the LOWEST_VAL target.

Bounded retention removes only standard epoch_<E>_step_<S> checkpoint directories. It does not remove recipe-specific exports, such as retrieval distillation step_<S> directories.

Leave the value unset, or set it to null, to keep all intermediate checkpoints:

1checkpoint:
2 max_recent_checkpoints: null

Bounded retention supports only local filesystem checkpoint directories. If checkpoint.checkpoint_dir uses msc:// storage, leave max_recent_checkpoints unset.

Safetensors

To ensure seamless integration with the Hugging Face ecosystem, NeMo AutoModel saves checkpoints in the Safetensors format. Safetensors is a memory-safe, zero-copy alternative to Python’s pickle (PyTorch .bin). Hugging Face Transformers supports Safetensors natively, and Safetensors offers safety and performance advantages over Python pickle-based approaches.

Key Benefits

  • Native Hugging Face Compatibility: Checkpoints can be loaded directly into Hugging Face-compatible tools, including vLLM, SGLang, and others.
  • Memory Safety and Speed: The Safetensors format prohibits saving serialized Python code, ensuring memory safety, and supports zero-copy loading for improved performance.
  • Optional Consolidation: Sharded checkpoints can be merged into a standard Hugging Face model format for easier downstream use.

This format also supports optional consolidation of multiple shards into a complete Hugging Face format model.

Example

The following command runs the LLM fine-tuning recipe on two GPUs and saves the resulting checkpoint in the Safetensors format:

$automodel --nproc-per-node=2 examples/llm_finetune/llama3_2/llama3_2_1b_squad.yaml \
> --step_scheduler.ckpt_every_steps 20 \
> --checkpoint.model_save_format safetensors \
> --checkpoint.save_consolidated final

The preceding command uses the llama3_2_1b_squad.yaml configuration file as a running example. Adjust it for your use case. More configuration examples are available in the examples/ directory.

If you are running on a single GPU, run the following command:

$automodel examples/llm_finetune/llama3_2/llama3_2_1b_squad.yaml \
> --step_scheduler.ckpt_every_steps 20 \
> --checkpoint.model_save_format safetensors \
> --checkpoint.save_consolidated final

After running for a few seconds, the standard output should be:

...
> Saving checkpoint to checkpoints/epoch_0_step_20
...

The checkpoints/ directory should have the following contents:

checkpoints/
├── LATEST -> epoch_0_step_20
├── LOWEST_VAL -> epoch_0_step_20
└── epoch_0_step_20
├── model
│ ├── consolidate.sh
│ ├── shard-00001-model-00001-of-00001.safetensors
│ └── shard-00002-model-00001-of-00001.safetensors
└── optim
├── __0_0.distcp
└── __1_0.distcp
...

The epoch_0_step_20/ directory stores the full training state from step 20 of the first epoch, including both the model and optimizer states.

Because this example uses save_consolidated: final, intermediate checkpoints such as epoch_0_step_20/ do not include model/consolidated/ before the run reaches the final checkpoint. To export this intermediate checkpoint for Hugging Face-compatible tools, run the generated helper:

$bash checkpoints/epoch_0_step_20/model/consolidate.sh

Run the helper from the AutoModel repo root so it can find tools/offline_hf_consolidation.py, or set CONSOLIDATION_TOOL=/path/to/tools/offline_hf_consolidation.py.

The helper defaults to one CPU worker process with five writer threads so it is safe on small machines. For large checkpoints, run it on a CPU compute node and increase parallelism:

$NPROC_PER_NODE=16 NUM_THREADS=5 bash checkpoints/epoch_0_step_20/model/consolidate.sh

NPROC_PER_NODE controls worker processes, and NUM_THREADS controls writer threads per process. Keep NPROC_PER_NODE * NUM_THREADS within your CPU allocation. You can also submit the helper to a CPU Slurm partition, as shown in the following example:

$sbatch --cpus-per-task=80 --wrap='NPROC_PER_NODE=16 NUM_THREADS=5 bash /path/to/checkpoints/epoch_0_step_20/model/consolidate.sh'

By default, consolidated export uses the original Hugging Face Safetensors headers when they are available. Ordinary floating-point tensors are restored to their original per-tensor Hugging Face dtype, such as BF16, FP16, or FP32, even if the saved sharded checkpoint uses a different floating dtype. If the run started from configuration-only weights or the original Hugging Face metadata is unavailable, export keeps the saved checkpoint dtype. If an original quantized or packed tensor was saved as a floating-point tensor, export leaves it as float and emits a warning.

You can request an explicit floating-point dtype cast during offline export:

$CAST_DTYPE=bf16 bash checkpoints/epoch_0_step_20/model/consolidate.sh

Use CAST_DTYPE when the consolidated Hugging Face bundle should override the default per-tensor dtype behavior, such as CAST_DTYPE=bf16 to export ordinary floating-point tensors as BF16 for serving. Supported values include bf16, fp16, fp32, and fp64. Only ordinary floating-point tensors with a different source dtype are cast. Tensors already in the cast dtype, FP8 tensors, and non-floating tensors are left unchanged.

The helper writes checkpoints/epoch_0_step_20/model/consolidated/. Load and run that consolidated checkpoint directly with the Hugging Face Transformers API:

1import torch
2from transformers import pipeline
3
4model_id = "checkpoints/epoch_0_step_20/model/consolidated/"
5pipe = pipeline(
6 "text-generation",
7 model=model_id,
8 torch_dtype=torch.bfloat16,
9 device_map="auto",
10)
11
12print(pipe("The key to life is"))
13
14>>> [{'generated_text': 'The key to life is to be happy. The key to happiness is to be kind. The key to kindness is to be'}]

Although this example uses the Hugging Face Transformers API, the consolidated/ checkpoint is compatible with any Hugging Face-compatible tool, such as vLLM, SGLang, and others.

PEFT

When training with Parameter-Efficient Fine-Tuning (PEFT) techniques, only a small subset of model weights is updated, and the rest of the model remains frozen. This dramatically reduces the size of the checkpoint, often to just a few megabytes.

PEFT checkpoints save adapter files directly under model/ and do not generate or need model/consolidate.sh.

Benefits of Consolidated Adapter Checkpoints

Because the PEFT state is so lightweight, sharded checkpointing adds unnecessary overhead. Instead, NeMo AutoModel automatically saves a compact Hugging Face-compatible adapter checkpoint when using PEFT. This makes it:

  • Easier to manage and share (just the adapters).
  • Compatible with Hugging Face Transformers out of the box.
  • Ideal for deployment and downstream evaluation.

Run PEFT Fine-Tuning on Two GPUs

To fine-tune a model using PEFT and save a Hugging Face-compatible checkpoint:

$automodel --nproc-per-node=2 examples/llm_finetune/llama3_2/llama3_2_1b_hellaswag_peft.yaml --step_scheduler.ckpt_every_steps 20 --checkpoint.model_save_format safetensors

After training, the run produces a compact Safetensors adapter checkpoint that can be loaded directly with Hugging Face tools:

checkpoints/
├── LATEST -> epoch_0_step_20
├── LOWEST_VAL -> epoch_0_step_20
├── epoch_0_step_20
│ ├── config.yaml
│ ├── dataloader
│ │ ├── dataloader_dp_rank_0.pt
│ │ └── dataloader_dp_rank_1.pt
│ ├── losses.json
│ ├── model
│ │ ├── adapter_config.json
│ │ ├── adapter_model.safetensors
│ │ ├── automodel_peft_config.json
│ │ ├── special_tokens_map.json
│ │ ├── tokenizer.json
│ │ └── tokenizer_config.json
│ ├── optim
│ │ ├── __0_0.distcp
│ │ └── __1_0.distcp
│ ├── rng
│ │ ├── rng_dp_rank_0.pt
│ │ └── rng_dp_rank_1.pt
│ └── step_scheduler.pt
├── training.jsonl
└── validation.jsonl

The following example shows the direct compatibility of NeMo AutoModel with Hugging Face and PEFT:

1from peft import AutoPeftModelForCausalLM
2from transformers import AutoTokenizer
3
4checkpoint_path = "checkpoints/epoch_0_step_20/model/"
5model = AutoPeftModelForCausalLM.from_pretrained(checkpoint_path)
6tokenizer = AutoTokenizer.from_pretrained(checkpoint_path)
7
8model = model.to("cuda")
9model.eval()
10inputs = tokenizer("Preheat the oven to 350 degrees and place the cookie dough", return_tensors="pt")
11
12outputs = model.generate(input_ids=inputs["input_ids"].to("cuda"), max_new_tokens=50)
13print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True)[0])
14
15>>> Preheat the oven to 350 degrees and place the cookie dough in a large bowl. Roll the dough into 1-inch balls and place them on a cookie sheet. Bake the cookies for 10 minutes. While the cookies are baking, melt the chocolate chips in the microwave for 30 seconds.

PyTorch DCP

NeMo AutoModel also offers native PyTorch DCP checkpointing support (.distcp extension). Similar to Safetensors, it provides load-time resharding and parallel saving.

As a simple example, run the following command to launch the training recipe on two GPUs.

$automodel --nproc-per-node=2 examples/llm_finetune/llama3_2/llama3_2_1b_squad.yaml \
> --step_scheduler.ckpt_every_steps 20 \
> --checkpoint.model_save_format torch_save
$
$...
$> Saving checkpoint to checkpoints/epoch_0_step_20
$...

After 20 steps, AutoModel saves the following checkpoint:

checkpoints/
├── LATEST -> epoch_0_step_20
├── LOWEST_VAL -> epoch_0_step_20
└── epoch_0_step_20
├── config.yaml
├── dataloader
│ ├── dataloader_dp_rank_0.pt
│ └── dataloader_dp_rank_1.pt
├── losses.json
├── model
│ ├── __0_0.distcp
│ └── __1_0.distcp
└── optim
├── __0_0.distcp
└── __1_0.distcp
...

If you rerun the script, NeMo AutoModel automatically detects and restores the most recent checkpoint.

$automodel --nproc-per-node=2 examples/llm_finetune/llama3_2/llama3_2_1b_squad.yaml \
> --step_scheduler.ckpt_every_steps 20 \
> --checkpoint.model_save_format torch_save
$
$...
$> Loading checkpoint from checkpoints/epoch_0_step_20
$...

Save Checkpoints When Using Docker

When training inside a Docker container (refer to the Installation Guide), any files written to the container’s filesystem are lost when the container exits, especially with --rm. To keep your checkpoints, bind-mount a host directory to the checkpoint path before starting the container:

$docker run --gpus all -it --rm \
> --shm-size=8g \
> -v "$(pwd)"/checkpoints:/opt/Automodel/checkpoints \
> nvcr.io/nvidia/nemo-automodel:26.06.00

You can also set a custom checkpoint directory through the YAML config or CLI override:

1checkpoint:
2 checkpoint_dir: /mnt/shared/my_checkpoints
$# Or with a CLI override:
$automodel examples/llm_finetune/llama3_2/llama3_2_1b_squad.yaml \
> --checkpoint.checkpoint_dir /mnt/shared/my_checkpoints

When using a custom path, make sure the corresponding host directory is mounted into the container with -v.

Mount additional host directories for datasets and the Hugging Face model cache to avoid downloading large models again across container restarts. Refer to the Installation Guide for a complete docker run example with all recommended mounts.

Enable Asynchronous Checkpointing

Checkpointing is synchronous by default. NeMo AutoModel can instead write checkpoints asynchronously to reduce training stalls caused by I/O. When enabled, checkpoint writes are scheduled in the background using PyTorch Distributed Checkpointing’s async API while training continues.

  • Enable (YAML):
    1checkpoint:
    2 is_async: true
  • Enable (CLI): Add --checkpoint.is_async True to your run command.
  • Requirements: PyTorch ≥ 2.9.0. If an older version is detected, async mode is automatically disabled.
  • Behavior: At most one checkpoint is written at a time. The next save waits for the previous write to finish.
  • Publication: The next save call waits for and publishes the previous asynchronous checkpoint. On clean shutdown, each recipe waits for and publishes the final checkpoint before closing the checkpointer. A forced termination before either point can leave a finished background write unpublished, as described in Interrupted Saves.
  • PEFT: Adapter model files are written synchronously on rank 0. Optimizer states can still be written asynchronously.

Preemption Checkpointing

On shared clusters, jobs can be preempted or reach their wall-time limits before training finishes. NeMo AutoModel can catch an operating system signal sent ahead of the shutdown, save a checkpoint at the next step boundary, and exit gracefully. This lets training resume from the moment of preemption instead of the last periodic checkpoint.

1step_scheduler:
2 ...
3 preemption_signal: SIGUSR1 # default: SIGTERM

preemption_signal accepts the following values:

  • A signal name as a string, case-insensitive, with or without the SIG prefix ("SIGUSR1", "usr1", and "SIGTERM" are all valid).
  • A signal number, such as 15.
  • A list of preceding options to watch multiple signals at once, such as ["SIGUSR1", "SIGTERM"].
  • null to disable preemption handling entirely.

By default, SIGTERM is watched, so runs already checkpoint on a graceful termination request without any configuration.

How It Works

When any rank receives the configured signal, the signal handler records it locally. The flags are then gathered across ranks at the next step boundary, so every rank observes the preemption at the same step. All ranks write a checkpoint together and shut down cleanly. This means it is sufficient for the signal to reach a single rank. For example, the signal can reach only the workers on one node.

Since the checkpoint is written at a step boundary, the delay between signal delivery and the checkpoint is at most one training step. Set the signal lead time to cover one step and the time to write a checkpoint for your model size and storage backend.

If the signal arrives while the final steps of a run are completing, the run finishes normally and writes its regular final checkpoint, so no separate preemption checkpoint is made.

Slurm Usage

The reference slurm.sub submission script demonstrates the full setup. Slurm delivers the warning signal to the batch script through the --signal directive:

$#SBATCH --signal=B:USR1@300 # send USR1 to the batch script 300s before the time limit

The B: prefix is required: it delivers the signal to the batch script only. The batch script then forwards SIGUSR1 to the local training worker processes. The workers must be signaled directly rather than srun or torchrun, because torchrun terminates on an unhandled USR1, taking the job down before a checkpoint can be written. Forwarding to the workers on the batch node alone is sufficient because the all-gather propagates the flag to every rank on every node.

To test preemption behavior without waiting for a time limit, deliver the signal manually to a running job’s batch script:

$scancel --batch --signal=USR1 <jobid>

--signal=B:USR1@<sec> guarantees the warning before a wall-time termination. Whether the same warning is delivered on a scheduler preemption, such as a higher-priority job taking the nodes, depends on the cluster’s Slurm configuration (GraceTime on the partition and PreemptParameters=send_user_signal). Ask your cluster administrator if preemption-triggered signals are required.

Resume Training After Preemption

A preemption checkpoint is a regular checkpoint: the LATEST symbolic link is updated to point to it. Resubmitting the same job resumes from the preempted step with model, optimizer, and scheduler state intact.

Signals that arrive before training starts (during model loading or dataloader construction) cannot trigger a checkpoint, since the signal handler is installed when the step scheduler is created and there is no training state to save at that point.

Save Additional States

You can also save additional states in NeMo AutoModel. By default, AutoModel automatically checkpoints the dataloader, rng, and step_scheduler states that are necessary to resume training accurately. A complete Safetensors consolidated checkpoint looks like this:

checkpoints/
├── LATEST -> epoch_0_step_20
├── LOWEST_VAL -> epoch_0_step_20
├── epoch_0_step_20
│ ├── config.yaml
│ ├── dataloader
│ │ ├── dataloader_dp_rank_0.pt
│ │ └── dataloader_dp_rank_1.pt
│ ├── losses.json
│ ├── model
│ │ ├── consolidated
│ │ │ ├── config.json
│ │ │ ├── generation_config.json
│ │ │ ├── model-00001-of-00001.safetensors
│ │ │ ├── model.safetensors.index.json
│ │ │ ├── special_tokens_map.json
│ │ │ ├── tokenizer.json
│ │ │ └── tokenizer_config.json
│ │ ├── shard-00001-model-00001-of-00001.safetensors
│ │ └── shard-00002-model-00001-of-00001.safetensors
│ ├── optim
│ │ ├── __0_0.distcp
│ │ └── __1_0.distcp
│ ├── rng
│ │ ├── rng_dp_rank_0.pt
│ │ └── rng_dp_rank_1.pt
│ └── step_scheduler.pt
├── training.jsonl
└── validation.jsonl

To define a new state to be checkpointed in the recipe, create a new attribute in the recipe class (defined using self. inside the recipe). Ensure that the new attribute uses both the load_state_dict and state_dict methods.

The following example shows how to define the state:

1class NewState:
2
3 def __init__(self, ...):
4 self.state_value = ...
5 self.another_value = ...
6 ...
7
8 def state_dict(self) -> dict[str, Any]:
9 return {
10 "<some state you're tracking>": self.state_value,
11 "<another state you're tracking>": self.another_value,
12 }
13
14 def load_state_dict(self, state_dict: dict[str, Any]) -> None:
15 self.state_value = state_dict["<some state you're tracking>"]
16 self.another_value = state_dict["<another state you're tracking>"]

Inside your recipe class, define the new state as an instance attribute using self.new_state = NewState(...).