Checkpoint: Save and Restore System State

Last updated: 07/03/2026

Overview

TransferQueue provides tq.save_checkpoint and tq.load_checkpoint to persist and restore the full system state — controller metadata and storage data — enabling fault tolerance and training resumption after a restart.

A checkpoint captures two components:

  • Controller state — all partition metadata, the global index manager, and sampler state.
  • Storage data — the tensor and non-tensor field data held by each SimpleStorageUnit.

Quick Start

import transfer_queue as tq

tq.init(config)

# ... put/get data during training ...

# Save at a step boundary
tq.save_checkpoint("/shared/fs/checkpoints/step_1000", metadata={"step": 1000})

# --- After restart ---
tq.init(config)
tq.load_checkpoint("/shared/fs/checkpoints/step_1000")
# System state restored; training can resume

API Reference

tq.save_checkpoint

def save_checkpoint(
    checkpoint_dir: str | Path,
    *,
    include_storage: bool = True,
    metadata: dict[str, Any] | None = None,
) -> None
Parameter Description
checkpoint_dir Directory to write the checkpoint. Created if it does not exist. If a checkpoint already exists at this path it is replaced (best-effort; see Known Limitations §3).
include_storage Whether to save storage unit data. For SimpleStorage (in-memory), this is forced to True regardless of the value passed — skipping storage would cause complete data loss on restart. For persistent external backends, False is valid.
metadata Optional user-defined key-value pairs written into metadata.json. Useful for recording step number, timestamp, etc.

Raises: RuntimeError if tq.init() has not been called, or if any write step fails.


tq.load_checkpoint

def load_checkpoint(
    checkpoint_dir: str | Path,
) -> None
Parameter Description
checkpoint_dir Path to a directory previously written by save_checkpoint.

Raises: FileNotFoundError if the directory or required files are missing; ValueError if the number of storage units in the checkpoint does not match the running system; RuntimeError if tq.init() has not been called or restore fails.

Prerequisite: tq.init() must have been called and the system must be in a clean state (no prior data operations). load_checkpoint restores state into a running system; it does not launch components.

Checkpoint Directory Layout

checkpoint_dir/
├── metadata.json                        # Flags and user metadata
├── controller_state.pkl                 # Controller state (pickle)
└── simple_storage/
    ├── storage_unit_info.json           # Position-to-ID manifest
    ├── su_0_<id>.pkl                    # StorageUnit at position 0
    ├── su_1_<id>.pkl                    # StorageUnit at position 1
    └── ...

metadata.json

{
  "storage_saved": true,
  "user_metadata": {"step": 1000}
}

storage_unit_info.json

[
  {"position": 0, "storage_unit_id": "<id_0>"},
  {"position": 1, "storage_unit_id": "<id_1>"}
]

Architecture

tq.save_checkpoint / tq.load_checkpoint
          │
          │  ZMQ RPC
          ├──────────────► TransferQueueController (Ray Actor)
          │                  - partition metadata
          │                  - index_manager state
          │                  - sampler state
          │
          │  ZMQ RPC (concurrent)
          └──────────────► StorageManager
                              ├── SimpleStorageUnit 0  ──► su_0_<id>.pkl
                              ├── SimpleStorageUnit 1  ──► su_1_<id>.pkl
                              └── ...

Both the controller and each storage unit write their data directly to disk from within their own processes. The ZMQ RPC carries only the target file path and an ACK, not the payload — this avoids routing large tensors through the Ray object store.

Save Order and Consistency

The save sequence is: controller first, storage units second.

This ordering is intentional. In the normal data flow, a put writes to the storage unit before the controller's production_status is updated. At any point in time, storage units hold a superset (or equal set) of what the controller considers produced. Snapshotting the controller first therefore guarantees that every index the controller records as produced is present in the subsequently snapshotted storage units.

If storage were snapshotted first, a race could produce a checkpoint where the controller records an index as produced but the corresponding storage data was not yet captured — consumers would read a missing entry on restore.

Concurrent clear during save: If clear_partition or clear_samples runs concurrently with save_checkpoint, the controller snapshot may reference indexes that are subsequently deleted from storage before the storage snapshot is taken. The resulting checkpoint reflects a mixed view. To avoid this, callers should ensure no concurrent clear operations are issued during save_checkpoint. In practice, checkpoints are typically taken at step boundaries where no clearing is in progress.

Multi-Node Requirements

checkpoint_dir must reside on a shared filesystem accessible from all nodes (NFS, GPFS, Lustre, etc.), because each StorageUnit writes its file directly to that path. Single-node deployments have no such requirement.

Storage Unit Count Matching

On load, the number of storage units in the checkpoint must exactly match the running system. The matching is by position (index in the ordered list), not by storage unit ID — since IDs are regenerated on each tq.init(), position-based matching supports restart with freshly created actors. A count mismatch raises ValueError and aborts the restore.

Known Limitations

1. Controller request thread can hang on checkpoint I/O failure

The controller's request loop currently has no error handling around the checkpoint branches. If save_checkpoint or load_checkpoint raises (e.g., the path is not writable, or the pickle file is corrupt), the exception propagates up and kills the request thread. The client's recv_multipart call will block indefinitely.

Workaround: Ensure the checkpoint path is writable and the target file is not corrupt before calling. Verify disk space and file system permissions ahead of time.


2. Save consistency is not guaranteed under concurrent clears

See Save Order and Consistency above. Concurrent clear_partition or clear_samples during save_checkpoint can produce a checkpoint whose controller view references storage entries that no longer exist.

Workaround: Do not issue clear_partition or clear_samples while save_checkpoint is running. This is naturally satisfied when checkpointing at training step boundaries.


3. Replacing an existing checkpoint is not fully atomic

The current save sequence is:

if checkpoint_dir.exists():
    shutil.rmtree(checkpoint_dir)   # (1) old directory deleted
tmp_dir.rename(checkpoint_dir)      # (2) new directory moved into place

If step (2) fails after step (1) (e.g., cross-device rename, disk full), the old checkpoint has already been deleted and the new one is also cleaned up by the exception handler — both copies are lost.

Workaround: Maintain an additional copy of the previous checkpoint (e.g., save to step_N while keeping step_N-1) so a failure at step N leaves step_N-1 intact.


4. Load is not transactional — partial restore has no rollback

The load sequence is:

if meta.get("storage_saved"):
    client.load_storage_checkpoint(...)   # (1) storage restored first
client.load_controller_checkpoint(...)    # (2) controller restored second

If step (1) partially succeeds and step (2) fails, the system is left in a mixed state: some storage units hold checkpoint data while the controller still reflects its pre-restore state. There is no rollback path.

Workaround: If load_checkpoint raises, call tq.init() again to reset the system to a clean state before retrying.