Draft

This documentation is in draft and under active review. Figures for the Fall 2026 GPU reservation system are not yet published, and some pages describe behavior that has not been verified against a primary source. Confirm any figure with datahub@ucsd.edu before relying on it.

Checkpointing & Logging Long Runs

A container can stop for reasons unrelated to faulty code: the runtime limit, idle culling, preemption, and maintenance. This page covers checkpoints a job can resume from, the termination warning that precedes some of these stops, and logs for unattended jobs.

What Can End a Run

Cause Warning Where it is documented
The runtime limit: 6 hours by default, 12 if set at launch None; the pod reports DeadlineExceeded The Runtime Limit
Idle culling, once the GPU stops being used Yes; a warning comes first, and the status is recorded on the pod What Counts as Idle
Preemption, once the session is past its runtime guarantee and its GPU is needed Usually; about 15 minutes of notice, readable by the job itself Preemption
Eviction, when the reservation is cancelled or given to a teammate None Eviction
Research Cluster maintenance Notice in advance, not on the pod; all running jobs are terminated Scheduled Maintenance

None of these stops indicates a fault in the code. Instructional maintenance generally leaves running jobs alone.

Warning

The runtime limit, an eviction, and Research Cluster maintenance are not announced on the pod. The runtime limit stops the container at its deadline with no prior warning and no signal to catch. Research Cluster maintenance terminates every running job. Work that is not on disk before any of them arrives is lost.

Preemption and idle culling are announced on the pod, where a running program can read them. The reservation controller marks a session it may need to stop minutes before stopping it, as described in The Termination Warning. The idle culler records a status and a projected deadline in the same way, as described in Idle Culler Annotations.

Checkpointing

A checkpoint is a saved copy of enough state to resume a run, written on a chosen schedule. For a training run, that usually means the model weights, the optimizer state, and the epoch or step counter. The full set of components is listed in What a Resumable Checkpoint Contains. A job that cannot resume from a checkpoint restarts from the beginning after an interruption.

Checkpoint Location

Write checkpoints to a home directory. Home persists between containers, and the container and the login node share a filesystem, so a checkpoint written by a job is readable from the login node after the pod is gone. A path inside the container that is not a mounted directory is lost with the pod. Directory locations are described in Where Files Live.

Do not write checkpoints into a shared or course-wide directory unintentionally. Its quota serves the whole class, not one run.

Wall-Clock Schedule

Checkpointing on wall-clock time, rather than only at the end of each epoch, bounds the work an interruption can lose. The runtime limit ends a run regardless of epoch boundaries, and with per-epoch checkpoints an epoch that takes 90 minutes loses 90 minutes of work at each interruption. A checkpoint written every 15 or 30 minutes limits the loss to that interval, at little cost.

Automatic Resume

A script that finds the newest checkpoint at startup and continues from it resumes after an interruption without anyone present. A --resume flag has to be supplied by hand at each restart.

Number of Checkpoints Kept

Checkpoints are large, and home directories have quotas, as described in Workspace and Personal Quotas. Bound the number of checkpoints kept. The last two and the best one are usually enough. Delete an old checkpoint only after the new one is complete. A job that deletes the old checkpoint first can lose all saved progress to one interruption during the save.

What a Resumable Checkpoint Contains

A checkpoint that holds only the model weights does not resume the same run. Each component in this table other than the weights is state the optimizer or the data pipeline carries. Omitting one shows as a jump in the loss curve at the point of resume.

Component Call Notes
Model weights model.state_dict() Under torch.compile the keys gain an _orig_mod. prefix. Saving model._orig_mod.state_dict(), or stripping the prefix on load, keeps the checkpoint loadable by an uncompiled model
Optimizer optimizer.state_dict() The largest component. Adam and AdamW carry two fp32 moments, so optimizer state is commonly 2-3× the model in bytes
Learning-rate scheduler scheduler.state_dict() Cheap to save. Without it, a warmup or cosine schedule restarts from the beginning
Mixed-precision scaler scaler.state_dict() torch.amp.GradScaler holds an adaptive loss scale. A resume without it re-converges through a few skipped steps
Step and epoch counters Kept by the training script The position a resume is expressed in
Data position StatefulDataLoader.state_dict() torchdata's drop-in DataLoader replacement, which supports mid-epoch resume without replaying batches. It requires the same num_workers on load as on save
Random number generator state torch.get_rng_state(), torch.cuda.get_rng_state_all(), random.getstate(), numpy.random.get_state() Needed for a bit-comparable resume of dropout, augmentation, and sampling. It can be omitted where reproducibility does not matter
Averaged weights and metric state Kept by the training script Exponential moving averages, best-so-far metrics, early-stopping counters

Store the run's configuration alongside the weights. A checkpoint saved without its configuration cannot be identified later.

Atomic Checkpoint Writes

An interruption can end a process while it is writing a checkpoint. Single-file and sharded checkpoints are protected against this in different ways.

Single-File Checkpoints

Write a single-file checkpoint to a temporary path, flush it, then rename it. os.replace is atomic within one filesystem, so the visible path always holds either the complete old checkpoint or the complete new one, never a truncated file:

import os, torch

def save_atomic(state: dict, path: str) -> None:
    tmp = f"{path}.tmp"
    with open(tmp, "wb") as fh:
        torch.save(state, fh)
        fh.flush()
        os.fsync(fh.fileno())      # the data is durable before the rename
    os.replace(tmp, path)          # atomic, within one filesystem only
    dfd = os.open(os.path.dirname(path) or ".", os.O_RDONLY)
    try:
        os.fsync(dfd)              # and the rename itself is durable
    finally:
        os.close(dfd)

A checkpoint written directly to its final path and interrupted during the write fails to load with RuntimeError: unexpected EOF.

Sharded Checkpoints

A distributed checkpoint is a directory of per-rank shards, and no atomic rename covers all of them at once. Complete the save, then publish it, in one of two ways:

  • Write into a scratch directory, and rename the directory once every rank has finished.
  • Write an explicit DONE marker last, and have the resume path ignore any directory without one.

With torch.distributed.checkpoint, the .metadata file is written after the shards, so its absence indicates that a directory is incomplete. Depend on an explicit marker rather than on .metadata.

Checkpoint Frequency

The Young/Daly Interval

The Young/Daly interval is sqrt(2 · C · MTBF), where C is the wall-clock cost of one checkpoint and MTBF is the mean time between interruptions. It balances saving so often that the saves dominate the run against saving so rarely that each interruption costs hours.

Interval from Checkpoint Cost

Under a reservation system that announces its interruptions, the interval can be set from C alone. A common target is checkpoint overhead below about 5% of step time, which for a synchronous save means an interval of roughly 20 × C. That interval covers interruptions that are not announced, such as a node failure, an out-of-memory kill, or a collective that times out. Announced interruptions are covered by the warning described in The Termination Warning. A checkpoint written on the warning limits the loss to one step rather than one interval.

Measure C once on the storage the checkpoint is actually written to, and log the value. Assumed values of C are consistently optimistic.

Asynchronous Checkpoints

Where C is large, use asynchronous checkpointing before lengthening the interval. torch.distributed.checkpoint offers dcp.async_save, which stages tensors into CPU buffers and writes them from a background thread while training continues. The blocking part of the save drops to roughly the staging copy.

Asynchronous saves have two costs:

  • Host memory on the order of one checkpoint per rank.
  • Only one save may be outstanding at a time. Wait on the previous future before issuing the next, or the memory use multiplies.

An asynchronous save that was never flushed is not a checkpoint. Wait on the outstanding future before exiting on a preemption.

The Termination Warning

The reservation controller marks a session it may need to stop before stopping it. The mark is a set of annotations written onto the pod. It appears only while the session is at risk: a booking is coming due for capacity the session is holding, or the class has less than its headroom free, and the session is one of the candidates that could supply it. Every candidate is marked, not only the sessions that will be stopped. See Preemption.

Warning Timings

Timing Value
How far ahead the controller looks Bookings starting in the next 30 minutes
Earliest stop for a booking 15 minutes before the booking starts, or the end of the session's guarantee if that is later
Usual notice About 15 minutes. There is no minimum: a booking made at the last minute gives less
Earliest stop for headroom 15 minutes after the warning
Delay before the container can read a warning About 2 minutes

The Runtime Guarantee

A session inside its runtime guarantee is never stopped, however short of capacity the cluster is. The mark can still appear during the guarantee, when the booking that needs the capacity starts after the guarantee ends. The warning then states that the reclaim is queued for the moment the guarantee ends. A warning of this kind arrives while the guarantee is still running, which leaves the most time to checkpoint. The end of a guaranteed window is described in End of a Reservation Window.

A job is not stopped at guaranteed-until. It keeps running until another reservation needs the capacity, so exiting at the guarantee gives up run time that remains available. From guaranteed-until onward, the job can be stopped on notice it does not control. Shorten the checkpoint interval at that point.

Warning Annotations

Four annotations are relevant to a long-running job:

Annotation Value What it says
galends/guaranteed-until UTC instant, YYYY-MM-DDTHH:MM:SSZ The end of the protected period. It can move later while the session runs, when an abutting follow-on window is booked
galends/termination-warning-at UTC instant, same format The earliest moment the session could be stopped. Never earlier than guaranteed-until. Present only while the session is at risk
galends/termination-warning-risk Decimal between 0 and 1, two places, e.g. 0.33 The share of the candidates that has to be stopped. 1.00 means all of them
galends/termination-warning-message A sentence The same thing in prose, in Pacific time. Written to be displayed as it stands, and not to be parsed. Its wording refers to "a reservation starting then" even for a stop 15 minutes before a booking starts, and for a headroom warning

termination-warning-at is the earliest possible stop, not a scheduled one. The shortfall it was computed from may be gone before that moment arrives, in which case nothing happens and the annotations are removed. Treat the moment as the start of a period in which the session may be stopped at any time, not as a countdown to a certain stop.

Best-Effort Annotations

Every annotation is optional and best-effort. Any of them can be absent at any moment, and the three warning annotations are usually removed when the risk clears. A warning can also stay on the pod after the risk has passed: a termination-warning-at in the past, on a session that is still running, is stale. The controller does not read any of the annotations back to decide anything. Each decision is recomputed from live reservation state. Code that reads the annotations handles each one being missing, ignores a value that does not parse rather than failing, and re-reads each value instead of caching it at startup.

Canceling a Pending Termination

Extend in the reservation app, if it is granted, cancels a pending termination at any point until the pod is deleted: the session moves onto a new booking and is guaranteed again. Extend can be refused when the GPUs are needed for a booking that follows. The controller re-checks every session's live guarantee before it selects any session to stop, so an Extend recorded before the selection always takes precedence. See Extend.

A new booking that starts exactly when the current guarantee ends, for the same class, number of GPUs, and workspace, also cancels it. A booking with a gap protects the session only once it opens, which can be after the stop. See Back-to-Back Bookings.

Reading the Warning From Inside a Container

Annotations are visible to the container only when the pod spec projects them. Pods started by launch.sh already mount a downward-API volume at /etc/podinfo; ls /etc/podinfo lists the files it carries. A pod created from a hand-written manifest needs a volume of its own. A downward-API volume projects the annotations and is refreshed as the values change:

spec:
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          - path: guaranteed-until
            fieldRef:
              fieldPath: metadata.annotations['galends/guaranteed-until']
          - path: termination-warning-at
            fieldRef:
              fieldPath: metadata.annotations['galends/termination-warning-at']
          - path: termination-warning-risk
            fieldRef:
              fieldPath: metadata.annotations['galends/termination-warning-risk']
          - path: termination-warning-message
            fieldRef:
              fieldPath: metadata.annotations['galends/termination-warning-message']
  containers:
    - name: notebook
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
          readOnly: true

Each file then holds one raw value, with no quoting, no escaping, and no trailing newline, so $(cat …) is the value:

$ cat /etc/podinfo/termination-warning-at; echo
2026-08-21T17:30:16Z
$ cat /etc/podinfo/termination-warning-risk; echo
0.33

Empty Annotation Files

An absent annotation is an empty file, not a missing one. All four files are created when the pod starts, and the three warning files stay empty while the session is not at risk. Test for a warning with -s, "exists and is not empty".

Warning

-f and -e are true from the pod's first second. A loop guarded on either one exits immediately on every run, and does so silently.

Downward-API Environment Variables

Downward-API environment variables are not suitable for these annotations. An environment variable is resolved once when the container starts, and the annotations are written after that, some of them changing during the run. Only the volume is refreshed.

Refresh Delay

The kubelet updates the files on its own sync loop, which adds to the controller's own cadence. The warning therefore reaches the container some time into the notice period, not at its start: usually about 2 minutes after the controller writes it.

Polling every 15 to 30 seconds is sufficient. To watch the files with inotify, watch the directory rather than the file, because the whole set is swapped atomically behind a symlink.

Projecting the Whole Annotation Map

fieldPath: metadata.annotations with no subscript projects the whole annotation map into a single file. That file carries every annotation the session has, including the reservation behind it, its GPU class, and when it was admitted. Each annotation is on its own line as key="value", with quotes and escapes that have to be undone. Per-key files need no parser and are sufficient for checkpointing.

Idle Culler Annotations

The idle culler publishes its own status in the same way. dsmlp/idle-gpu-status and dsmlp/idle-gpu-cull-deadline are annotations on the same pod, and an additional fieldRef for each projects them alongside the four reservation annotations. A job whose GPU is unused between phases can be culled on the culler's own timetable, independently of any reservation. Idle culling is described in What Counts as Idle.

Acting on the Warning

Batch Jobs Processed in Units

A batch job that processes work in units, such as shards, epochs, or sweeps, usually needs only to stop starting new units. Check for the warning between units, never inside one:

PODINFO=${PODINFO:-/etc/podinfo}

# -s is the operator that matters here: the file exists and is empty until the
# controller warns this pod, so -f and -e would bail on the first shard.
warned() { [[ -s "$PODINFO/termination-warning-at" ]]; }

for shard in "${SHARDS[@]}"; do
    if warned; then
        cat "$PODINFO/termination-warning-message" >&2
        printf 'stopped before %s; rerun to pick up the rest\n' "$shard" >&2
        exit 75                 # EX_TEMPFAIL, for whatever submitted the job
    fi
    process_shard "$shard"      # each shard commits its own output
done

Re-read the file on every pass through the loop, and do not store the result. A warning can be withdrawn when the window is extended, the incoming booking never claims its capacity, or the session is re-linked to another reservation. The file is then empty again. A job that stored the first warning it saw would stop for a warning that had already been withdrawn. A job that is to continue rather than hand back its remaining units can make the same test and log the result instead of acting on it.

Training Loops

A training loop checkpoints at a step boundary, not from a signal handler. A save during the backward pass captures inconsistent state, and under distributed training a save is a collective operation that every rank has to enter in the same iteration:

from datetime import datetime, timezone
from pathlib import Path

PODINFO = Path("/etc/podinfo")

def _read(name):
    try:
        raw = (PODINFO / name).read_text().strip()
    except OSError:
        return None
    return raw or None

def _instant(name):
    raw = _read(name)
    try:
        return datetime.fromisoformat(raw) if raw else None
    except ValueError:
        return None            # anything unparseable is ignored, not raised on

def stop_expected(lead_seconds):
    """True when a stop is close enough that a checkpoint should be written."""
    now = datetime.now(timezone.utc)
    for key in ("termination-warning-at", "guaranteed-until"):
        instant = _instant(key)
        if instant is not None and (instant - now).total_seconds() <= lead_seconds:
            return True
    return False
warned = False

for step, batch in enumerate(loader, start=resume_step):
    train_step(batch)

    urgent = stop_expected(lead_seconds=2 * CHECKPOINT_SECONDS + 60)
    if dist.is_initialized():
        urgent = any_rank(urgent)          # OR-reduced across ranks

    if step % CHECKPOINT_EVERY == 0 or (urgent and not warned):
        save_atomic(state(step), CHECKPOINT_PATH)
    warned = urgent                        # re-arms if the warning is withdrawn

The loop depends on four elements:

  • lead_seconds is derived from the measured cost of a checkpoint, not guessed. A save that starts too late to finish produces no usable checkpoint.
  • any_rank makes the ranks agree. Each pod carries its own annotations, so in a multi-pod job only some members may be marked. Losing any one member ends the job, and a collective save that only some ranks enter deadlocks. The flag is OR-reduced across ranks and acted on as an aggregate.
  • warned debounces the urgent save. Tracking the current value, rather than latching the first warning, makes the urgent save fire once per warning episode, and again if the warning clears and returns.
  • guaranteed-until is a cue in its own right. Past guaranteed-until a session is preemptible, and a session that enters that state with an hour-old checkpoint can lose an hour of work.

Stopping Voluntarily

For a batch job, stopping voluntarily is a legitimate response and usually the better one. Checkpointing, exiting cleanly, and letting a submission system resubmit the job hands back the capacity the incoming booking needs, chooses the stopping point, and avoids depending on the grace period. For an interactive session, checkpointing and continuing is usually the better response. The risk figure is not a certainty, the warning is frequently withdrawn, and a notebook that checkpoints and continues is usually still running afterwards.

The Risk Figure

termination-warning-risk is a band, not a probability. It describes the share of the candidate sessions that has to be stopped, on the assumption that the choice among them is arbitrary. Where selection follows a policy instead, the number is only an indication. Use it in coarse thresholds, such as "possible" and "likely". Where checkpoints are cheap, save on any warning and disregard the risk figure.

The Grace Period

A preempted or evicted pod is deleted in the ordinary way: SIGTERM, then a grace period, then a forced stop. Pods started by launch.sh have a grace period of 600 seconds, or 10 minutes. A SIGTERM handler that saves a checkpoint is a useful fallback where the save finishes well within that time, and it is the only chance to save before an eviction, which has no warning. The grace period begins only after the decision to stop the pod has been made, so for a preemption the warning comes earlier. Checkpoint on the warning first.

Resuming

Selecting the Checkpoint to Load

Load the newest complete checkpoint, which is not always the newest path. The marker written by the save path distinguishes the two, as described in Atomic Checkpoint Writes. On any load error, fall back to the previous checkpoint. That fallback is the reason for keeping more than one checkpoint.

Restoring State

Restore everything that was saved, in particular the scheduler and the data position. A resume that restores only the weights is visible in the loss curve. The components are listed in What a Resumable Checkpoint Contains.

Loading with torch.load

torch.load defaults to weights_only=True from PyTorch 2.6. A checkpoint holding anything beyond plain tensors and containers needs those types allowlisted through torch.serialization.safe_globals. Use torch.serialization.safe_globals rather than weights_only=False, which permits arbitrary code execution on load. Where only weights are needed, safetensors avoids the issue. safetensors stores tensors only, so optimizer state still goes through torch.save.

Testing the Resume Path

Test the resume path deliberately:

  1. Kill a run at a random step.
  2. Restart it.
  3. Check that the loss curve is continuous across the restart.

Checkpointing by Job Shape

Job Checkpoint size Handling
A LoRA or other adapter fine-tune Tens of MB (the adapter only) A save takes seconds, so it can be written often and on any warning. Resuming still needs the optimizer, scheduler, and step count alongside the adapter. The base model is referenced by name rather than copied into the checkpoint
A full fine-tune on one node Model, plus 2-3× that in optimizer state Saved from one rank where the replicas are identical. The termination warning is of most value in this case
From-scratch or large-model training across nodes Hundreds of GB, sharded torch.distributed.checkpoint handles sharded model and optimizer state together. Asynchronous saves suit the periodic schedule. On a warning, one synchronous save at a step boundary is more reliable than an asynchronous save that may not be flushed
A run spanning several reservations Any Design the run for restart rather than continuity. A run that resumes cleanly from disk can be scheduled across several windows and interrupted between them at almost no cost
Inference or a service pod Nothing to checkpoint Use the warning to drain the pod: stop accepting work, finish work in flight, and exit

Logging

An unattended job can be understood only from its logs, and a job with no log of its own cannot be debugged after it ends. Output from print statements in a background run goes nowhere that can be read.

Redirecting Output

The simplest form of logging is a shell redirect:

python run.py all > log.txt 2>&1 &

2>&1 is needed because > alone captures standard output, and tracebacks arrive on standard error. Add python -u as well. Python buffers output to a file, and without -u an interrupted job can lose its last several minutes of log.

The logging Module

The logging module timestamps every line and records which file and function produced it, which makes the log searchable:

# randdiv.py
import logging
import numpy as np

logging.basicConfig(filename='log.txt',
                    filemode='a',
                    level=logging.INFO,
                    datefmt='%H:%M:%S',
                    format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s')

def myfunc():
    a = np.random.randint(-5, 5)
    b = np.random.randint(-5, 5)
    if b == 0:
        logging.info('zerodivisionerror preempted')
    else:
        return a / b

print([myfunc() for _ in range(100)])

A line in log.txt then reads 2022-10-30 23:37:11,815,815 root INFO zerodivisionerror preempted, which records when it happened and where it came from. filemode='a' appends, so a resumed run adds to the existing log instead of erasing it.

Logging Interruptions

Log four values for correlation with an interruption: wall-clock time, the epoch or step, the path of the last checkpoint written, and whether a termination warning was present. When a run ends unexpectedly, these values locate the restart point and show whether the ending was announced.

Logs That Outlive the Pod

kubectl logs <pod-name> reads from the pod, so it stops working once the pod is deleted. That includes a pod that deletes itself at the end of a batch job, and a preempted pod. Use it to inspect a pod that is running, not to keep a record.

A file in a home directory persists after the pod is deleted. For unattended work, redirect output into one:

launch-scipy-ml.sh -g 1 -B -- bash -c 'python -u ./train.py > run.log 2>&1'

run.log is then readable from the login node while the job is still running, and after it is gone. Batch mode is described in Job Modes, and monitoring a job in progress in Watching a Running Job.