Durable training
The longer a training job runs, the more likely something under it fails: a spot node is reclaimed, a node fails, a batch runs out of memory, or the cluster has no free H100s. Flyte turns each of these into something your code handles, instead of a run you start over.
| Failure | What to use | What happens |
|---|---|---|
| The task’s pod dies or is preempted | retries with flyte.Checkpoint |
The next attempt loads the last checkpoint and continues from there |
| A node in a multi-node job fails, or the job is preempted after it starts | ClusterFailurePolicy |
The whole group restarts in place and resumes from the checkpoint |
| The container runs out of memory | flyte.errors.OOMError with override |
The driver retries the task with more memory |
| The GPU type you asked for isn’t available | max_queued_time with override |
The driver moves on to the next GPU type |
| A later stage fails | Recovery or caching | A new run reuses the stages that already succeeded |
Resume from a checkpoint after a retry
flyte.ctx().checkpoint is a per-task store in your object store. It survives across a task’s retries. Save to it as training progresses, and load from it at the start of the task:
@env.task(retries=RETRIES)
async def train_linear(epochs: int = 10) -> float:
checkpoint = flyte.ctx().checkpoint
model = nn.Linear(4, 1)
opt = torch.optim.SGD(model.parameters(), lr=0.01)
# Resume model, optimizer, and epoch from the previous attempt, if any.
prev = await checkpoint.load()
if prev:
blob = torch.load(prev, map_location="cpu", weights_only=False)
model.load_state_dict(blob["model"])
opt.load_state_dict(blob["opt"])
start = int(blob["epoch"]) + 1
else:
start = 0
wpath = pathlib.Path("pytorch_linear") / "training.pt"
wpath.parent.mkdir(parents=True, exist_ok=True)
failure_interval = epochs // RETRIES
for epoch in range(start, epochs):
x = torch.randn(8, 4)
y = torch.randn(8, 1)
loss = torch.nn.functional.mse_loss(model(x), y)
opt.zero_grad()
loss.backward()
opt.step()
if epoch > start and epoch % failure_interval == 0:
# Simulate a failure so the next attempt resumes from the checkpoint
raise RuntimeError(f"Simulated failure at epoch {epoch}")
# Save model, optimizer, and epoch state to object storage.
torch.save(
{"model": model.state_dict(), "opt": opt.state_dict(), "epoch": epoch},
wpath,
)
await checkpoint.save(wpath)
with torch.no_grad():
return float(model(torch.ones(1, 4)).squeeze().item())
Save the optimizer state and the step or epoch along with the weights, so the resumed attempt continues the same schedule. To connect a framework’s own checkpointing, such as the Hugging Face Trainer or Lightning’s ModelCheckpoint, use a callback. See Intra-task checkpoints, and the Brain tumor MRI classification tutorial for a Lightning training task that resumes this way.
Use spot capacity
Spot and preemptible GPUs cost a fraction of on-demand ones. With checkpoints in place, a preemption costs only the work since the last save. Set interruptible=True and give the task retries:
import flyte
train_env = flyte.TaskEnvironment(
name="train",
resources=flyte.Resources(gpu="A100:8"),
interruptible=True,
)
@train_env.task(retries=5)
async def train(epochs: int) -> flyte.io.Dir: ...Keep the driver and evaluation tasks on on-demand capacity with interruptible=False. See Interruptible tasks.
Restart a multi-node job
When one pod of a clustered job fails, the other ranks can’t continue. ClusterFailurePolicy restarts the whole group in place:
import flyte
from flyte.clustered import ClusterFailurePolicy, ClusteredTaskEnvironment, TorchRun
env = ClusteredTaskEnvironment(
name="pretrain",
resources=flyte.Resources(gpu="H100:8", shm="16Gi"),
replicas=4,
nproc_per_node=8,
runtime=TorchRun(),
failure_policy=ClusterFailurePolicy(max_restarts=2, restart_on_host_maintenance=True),
)max_restartsis the number of in-place restarts before the attempt fails. They don’t count against the task’sretries.restart_on_host_maintenance=Truemakes a restart caused by a node eviction free, so planned maintenance doesn’t use upmax_restarts.resume_after_preemption=Truekeeps the attempt when Kueue preempts the job after it started. The job waits to be readmitted and then resumes, without using a retry. It needs Flyte backend v2.0.54 or later. Without it, a preemption ends the attempt, so setretrieson the task.
Pod-local disk is wiped on a restart, so write checkpoints from rank 0 to flyte.ctx().checkpoint. flyte.ctx().restart_attempt counts the in-place restarts, which is useful for logging. See Clustered task environment.
Recover from out-of-memory errors
When a container runs out of host memory, the parent task receives flyte.errors.OOMError and can call the task again with more memory:
import flyte.errors
@driver_env.task
async def main(epochs: int) -> flyte.io.Dir:
for memory in ["256Gi", "512Gi", "1Ti"]:
try:
resources = flyte.Resources(gpu="A100:8", memory=memory)
return await train.override(resources=resources, retries=0)(epochs=epochs)
except flyte.errors.OOMError:
continue
raise RuntimeError("Out of memory at every size")override keeps the task’s own retries unless you set them. retries=0 sends an out-of-memory error straight back to the driver, instead of trying the same size again first.
Running out of GPU memory is different: the framework raises its own exception, such as torch.cuda.OutOfMemoryError. See Error handling to catch it and retry with a smaller batch.
Handle GPU hardware faults
When the platform attributes a failure to a GPU fault, such as a GPU that fell off the bus or an uncorrectable ECC error, the driver receives flyte.errors.GPUFaultSystemError. The platform has already retried the task on other hardware by then, so move it to a different GPU type or queue instead of retrying in place. flyte.errors.GPUFaultUserError means your code caused the fault, so fix the code rather than retrying. See Handling GPU hardware faults.
Fall back to another GPU type
A job that asks for scarce GPUs can wait in the queue for a long time. Set timeout=flyte.Timeout(max_queued_time=...) on the training task to bound the wait. When it expires, the driver catches flyte.errors.MaxQueuedTimeExceededError and calls the task again with override(resources=...) for the next GPU type on its list. See Falling back when GPU capacity isn’t available for the full example.
Reuse finished stages
A pipeline that fails at evaluation shouldn’t retrain the model. Two features keep finished stages from running again:
- Recovery starts a new run from a failed one and reuses every action that succeeded. See Recover a failed run.
- Caching reuses a task’s output in any later run with the same inputs and code. Set
cache="auto"on data prep and training. See Caching.
Related
- Retries and timeouts
- Distributed LLM pretraining: checkpointing and fault tolerance for a multi-node pretraining run.
- Fine-tuning a VLM: multi-node fine-tuning with recovery checkpoints.
- Pre-training
- Post-training