This is a staging copy of the PARASOL documentation, published for review before it goes live. Pages here may be unfinished, unreviewed or wrong, and they can change or disappear without notice.
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 parasol@ucsd.edu before relying on it.
A container can stop for reasons unrelated to faulty code: the runtime limit,
idle culling, preemption, and maintenance. Checkpoint and log a long or
unattended run so that it can resume after a stop and so that its logs show
what happened.
None of these stops indicates a fault in the code. Weekly maintenance
generally leaves running jobs alone.
Warning
The runtime limit, an eviction, and quarterly 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. Quarterly 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. See
The Termination Warning. The idle culler records a
status and a projected deadline in the same way. See
Idle Culler Annotations.
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.
What a Resumable Checkpoint Contains
lists the full set of components.
A job that cannot resume from a checkpoint restarts from the beginning after an
interruption.
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.
Where Files Live
describes directory locations.
Do not write checkpoints into a shared or course-wide directory unintentionally.
Its quota serves the whole class, not one run.
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.
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.
Checkpoints are large, and home directories have quotas. See
Workspace and Personal Quotas.
Bound the number of checkpoints kept. The last two and the best one are usually
enough.
Delete the old checkpoint last
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.
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
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:
An atomic save in Python
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.
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.
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.
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 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.
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.
Warning
An asynchronous save that was never flushed is not a checkpoint. Wait on the
outstanding future before exiting on a preemption.
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.
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.
End of a Reservation Window
describes the end of a guaranteed 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.
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, naming the cause: a reservation starting at a given time, or GPUs kept free for on-demand jobs. Written to be displayed as it stands, and not to be parsed
The message takes one of two forms:
A reservation starting at a given time
At risk of preemption: this pod is at or nearing the end of its GPU runtime guarantee and may be terminated as early as 2026-09-23 11:45:00 PDT to free GPUs for a reservation starting at 2026-09-23 12:00:00 PDT (risk 0.50). Extend or re-book the reservation to retain capacity.
GPUs kept free for on-demand jobs
At risk of preemption: this pod is at or nearing the end of its GPU runtime guarantee and may be terminated as early as 2026-09-23 11:45:00 PDT to keep GPUs free for on-demand jobs (risk 1.00). Extend or re-book the reservation to retain capacity.
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.
Every annotation is optional and best-effort. Any of them can be absent at any
moment. The controller removes the three warning annotations once the risk has
passed, usually within a few minutes. A termination-warning-at in the past, on
a session that is still running, means the session can be stopped at any
moment. 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.
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.
Every pod on PARASOL (formerly DSMLP) carries its annotations in the file
/etc/podinfo/annotations. The cluster's admission controller adds the
downward-API volume that holds the file to each pod it admits, whether the pod
was started by launch.sh, by Datahub, from a hand-written manifest, or by a
Job or Deployment. The kubelet refreshes the file as the values change.
Do not add a volume at /etc/podinfo to a hand-written manifest. A second mount
at the same path can cause the pod to be refused.
The 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". The value is quoted, and any quote or backslash
inside it is escaped:
An annotation that is not set has no line in the file. The file itself exists
from the pod's first second, and the three warning annotations are absent while
the session is not at risk. Test for a warning by looking for the annotation's
line, not for the file.
Warning
A test on the file itself, such as -f or -e, is true from the pod's first
second. A loop guarded on the file exits immediately on every run, and does
so silently.
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.
The kubelet updates the file 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 file with inotify,
watch the directory rather than the file, because the file is swapped
atomically behind a symlink.
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 appear in the same annotations file. A job whose GPU is
unused between phases can be culled on the culler's own timetable,
independently of any reservation.
What Counts as Idle
describes idle culling.
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:
ANNOTATIONS=${ANNOTATIONS:-/etc/podinfo/annotations}
# Prints the value of galends/<name>, or nothing when the annotation is absent.
# The file exists from the start, so test for the line, never for the file.
annotation() { sed -n "s|^galends/$1=\"\(.*\)\"\$|\1|p" "$ANNOTATIONS"; }
warned() { [[ -n "$(annotation termination-warning-at)" ]]; }
for shard in "${SHARDS[@]}"; do
if warned; then
annotation termination-warning-message >&2 # escapes left as written
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
annotation's line then disappears from the file. 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.
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:
Read the warning and guarantee annotations
import ast
from datetime import datetime, timezone
from pathlib import Path
ANNOTATIONS = Path("/etc/podinfo/annotations")
def _read(name):
"""The value of galends/<name>, or None when the annotation is absent."""
try:
lines = ANNOTATIONS.read_text().splitlines()
except OSError:
return None
prefix = f"galends/{name}="
for line in lines:
if line.startswith(prefix):
try:
return ast.literal_eval(line[len(prefix):]) or None
except (ValueError, SyntaxError):
return None
return 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
Checkpoint at a step boundary
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.
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.
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.
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.
Load the newest complete checkpoint, which is not always the newest path. The
marker written by the save path distinguishes the two. See
Atomic Checkpoint Writes. On any load error, fall
back to the previous checkpoint. That fallback is the reason for keeping more
than one checkpoint.
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.
What a Resumable Checkpoint Contains
lists the components.
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.
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
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.
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 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.
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.
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:
run.log is then readable from the login node while the job is still running,
and after it is gone. Job Modes describes
batch mode. Watching a Running Job describes monitoring
a job in progress.