Skip to content
Posts en inglés. Usá el traductor del navegador para leerlos en tu idioma.

How to Build Fault-Tolerant Training Workflows on Reclaimable GPU Instances

Yammbo
· 11 min read
gpu training checkpointing deep learning resume reclaimable instances cloud gpu
How to Build Fault-Tolerant Training Workflows on Reclaimable GPU Instances

Training large machine learning models often requires significant computational resources, and cloud-based GPU instances offer a flexible solution. However, to make these resources more accessible and cost-effective, cloud providers sometimes offer 'spot' or 'reclaimable' GPU instances. These instances come at a lower price point but with the understanding that the provider can reclaim them with short notice. For long-running training jobs, this presents a significant challenge: how do you ensure your progress isn't lost when the instance unexpectedly shuts down? This tutorial guides you through building a fault-tolerant training workflow using checkpointing and resuming, making your machine learning jobs resilient to such interruptions.

Step 1: Understanding Reclaimable GPU Instances and Their Implications

Reclaimable GPU instances, such as DigitalOcean Spot GPU Droplets, are a common offering in cloud environments. They allow users to access powerful hardware like NVIDIA HGX B300 or AMD Instinct MI350X/MI355X GPUs at a reduced cost by utilizing the provider's idle capacity. The trade-off is that these instances can be terminated by the cloud provider when the capacity is needed elsewhere, often with a short notice period (e.g., two hours via email). When an instance is reclaimed, its local storage (boot disk, scratch disk) is typically lost.

For machine learning training, this means that any progress saved only on the instance's local disk, or any training state held only in memory, will be permanently lost upon termination. This can lead to wasted computation time, increased costs, and significant delays in your project. The core implication is that your training job must be designed to withstand arbitrary interruptions and resume seamlessly from a known good state.

To achieve this, the primary strategy involves:

  • Periodic Checkpointing: Regularly saving the entire state of your training job.
  • External Storage: Storing these checkpoints on a durable, highly available object storage service (e.g., DigitalOcean Spaces, Amazon S3, Google Cloud Storage).
  • Resume Logic: Implementing code that can detect and load the latest checkpoint upon startup, allowing training to continue from where it left off.
  • Graceful Shutdown Handling: Ensuring that your application can perform a final checkpoint and upload it when it receives a termination signal.

Step 2: Designing Your Checkpointing Strategy

A robust checkpointing strategy is the foundation of fault-tolerant training. It involves deciding what data to save, when to save it, and where to store it. The goal is to capture enough information to fully restore your training process to its exact state at the time of the checkpoint.

What to Checkpoint

To resume training accurately, you typically need to save:

  • Model Weights: The learned parameters of your neural network.
  • Optimizer State: The internal state of your optimizer (e.g., AdamW's momentum and variance estimates), which is crucial for correct gradient updates.
  • Learning Rate Scheduler State: If you're using a learning rate scheduler, its state ensures the learning rate continues to evolve correctly.
  • Current Epoch/Step Number: To know exactly where to restart the training loop.
  • Random States: The states of Python's, NumPy's, and your deep learning framework's (PyTorch, TensorFlow) random number generators. This ensures reproducibility if you need to debug or verify runs.

When to Checkpoint

The frequency of checkpointing involves a trade-off between recovery time and overhead. More frequent checkpoints mean less re-computation upon a reclaim but incur higher I/O and storage costs. A good starting point is:

  • Every N steps or epochs: For example, every 500-1000 training steps, or after every epoch.
  • Before significant operations: Such as before validation runs or saving intermediate results.
  • Upon receiving a termination signal: To ensure the very latest progress is saved.

Choosing External Storage

Since the instance's local disk is ephemeral, checkpoints must be stored externally. Object storage services are ideal for this due to their durability, scalability, and accessibility from any new instance. Services like DigitalOcean Spaces provide an S3-compatible API, making integration straightforward.

Here's a conceptual Python example using a deep learning framework (e.g., PyTorch) for saving a checkpoint:

import torch
import os

def save_checkpoint(model, optimizer, scheduler, epoch, step, path): checkpoint_data = { ‘epoch’: epoch, ‘step’: step, ‘model_state_dict’: model.state_dict(), ‘optimizer_state_dict’: optimizer.state_dict(), ‘scheduler_state_dict’: scheduler.state_dict(), ‘rng_state’: torch.get_rng_state(), ‘cuda_rng_state’: torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None, # Add other states like numpy, python random states if needed } torch.save(checkpoint_data, path) print(f”Checkpoint saved to {path} at epoch {epoch}, step {step}“)

Example usage within a training loop:

if step % CHECKPOINT_INTERVAL == 0:

checkpoint_filename = f”checkpoint_epoch_{epoch}step{step}.pt”

local_path = os.path.join(“/tmp/checkpoints”, checkpoint_filename)

save_checkpoint(model, optimizer, scheduler, epoch, step, local_path)

# Then upload local_path to object storage

# For example, using boto3 for S3-compatible storage:

# s3_client.upload_file(local_path, BUCKET_NAME, f”checkpoints/{checkpoint_filename}“)

Step 3: Implementing the Resume Logic

Once you have a checkpointing strategy, the next critical step is to implement the logic to resume training from the latest available checkpoint. This involves checking for existing checkpoints, loading their contents, and configuring your training loop to pick up where it left off.

Detecting and Loading the Latest Checkpoint

Upon startup, your training script should first attempt to find and download the most recent checkpoint from your external object storage. This typically involves listing objects in your checkpoint bucket, sorting them by timestamp or step number, and downloading the latest one.

After downloading, you load the saved state dictionaries into your model, optimizer, and scheduler. You also set the starting epoch and step number for your training loop and restore the random number generator states to ensure continuity.

Here’s a conceptual Python example for loading a checkpoint:

import torch

import os

def load_checkpoint(model, optimizer, scheduler, path): if not os.path.exists(path): print(f”No checkpoint found at {path}. Starting training from scratch.”) return 0, 0 # Return initial epoch and step

checkpoint_data = torch.load(path)
model.load_state_dict(checkpoint_data['model_state_dict'])
optimizer.load_state_dict(checkpoint_data['optimizer_state_dict'])
scheduler.load_state_dict(checkpoint_data['scheduler_state_dict'])
torch.set_rng_state(checkpoint_data['rng_state'])
if checkpoint_data['cuda_rng_state'] is not None and torch.cuda.is_available():
torch.cuda.set_rng_state_all(checkpoint_data['cuda_rng_state'])
epoch = checkpoint_data['epoch']
step = checkpoint_data['step']
print(f"Resumed training from checkpoint at epoch {epoch}, step {step}")
return epoch, step

Example usage at the beginning of your training script:

latest_checkpoint_path = “/tmp/checkpoints/latest_checkpoint.pt” # Or downloaded from S3

start_epoch, start_step = load_checkpoint(model, optimizer, scheduler, latest_checkpoint_path)

Then, your training loop would start from start_epoch and start_step

for epoch in range(start_epoch, total_epochs):

for step in range(start_step if epoch == start_epoch else 0, steps_per_epoch):

# … training logic …

Integrating into the Training Loop

Your training loop needs to be flexible enough to start from any given epoch and step. This usually means adjusting the loop counters and ensuring that data loaders can be re-initialized correctly to provide the appropriate batch for the starting step.

Step 4: Automating Graceful Shutdowns and Checkpoint Uploads

While periodic checkpoints protect against sudden failures, gracefully handling a termination signal allows your job to save its absolute latest progress. Cloud providers typically send a termination signal (e.g., SIGTERM) to an instance before reclaiming it, giving your application a brief window to shut down cleanly.

Implementing a Signal Handler

Your training script can catch these signals and execute a final checkpoint and upload. This minimizes lost work to just the few seconds between the last periodic checkpoint and the termination signal.

import signal

import sys import time

Assume save_checkpoint function is defined as in Step 2

Assume s3_client and BUCKET_NAME are configured for object storage

def signal_handler(sig, frame): print(“Received termination signal (SIGTERM). Performing final checkpoint…”) # These variables (model, optimizer, etc.) would be accessible from the main scope # For demonstration, assume current_epoch, current_step are globally accessible global model, optimizer, scheduler, current_epoch, current_step

final_checkpoint_filename = f"checkpoint_final_epoch_{current_epoch}_step_{current_step}.pt"
local_path = os.path.join("/tmp/checkpoints", final_checkpoint_filename)
save_checkpoint(model, optimizer, scheduler, current_epoch, current_step, local_path)
# Upload to object storage
try:
# s3_client.upload_file(local_path, BUCKET_NAME, f"checkpoints/{final_checkpoint_filename}")
print(f"Final checkpoint uploaded to object storage: {final_checkpoint_filename}")
except Exception as e:
print(f"Error uploading final checkpoint: {e}")
sys.exit(0) # Exit cleanly

Register the signal handler

signal.signal(signal.SIGTERM, signal_handler)

Your main training loop would then run

For example:

current_epoch = 0

current_step = 0

while True:

# Your training logic

current_step += 1

if current_step % 100 == 0:

print(f”Training at step {current_step}“)

time.sleep(0.1) # Simulate work

if current_step > 1000 and current_epoch == 0: # Just for demo

current_epoch = 1

current_step = 0

if current_epoch > 2:

break

Using a Service Manager (e.g., systemd)

To ensure your training script runs reliably and receives termination signals, it’s best to run it as a service using a system manager like systemd on Linux. A systemd service unit can be configured to start your script, manage its lifecycle, and correctly forward signals.

An example systemd service file (e.g., /etc/systemd/system/my-training-job.service):

[Unit]

Description=My Fault-Tolerant ML Training Job After=network.target

[Service] ExecStart=/usr/bin/python3 /path/to/your/training_script.py WorkingDirectory=/path/to/your/project Restart=on-failure RestartSec=5s StandardOutput=journal StandardError=journal SyslogIdentifier=my-training-job User=youruser Group=yourgroup KillSignal=SIGTERM TimeoutStopSec=120 # Give your script 120 seconds to checkpoint and exit

[Install] WantedBy=multi-user.target

After creating this file, you would enable and start your service:

sudo systemctl daemon-reload
sudo systemctl enable my-training-job.service
sudo systemctl start my-training-job.service

The KillSignal=SIGTERM and TimeoutStopSec directives are crucial. systemd will send SIGTERM to your script and wait for TimeoutStopSec before forcibly terminating it with SIGKILL if it doesn’t exit. This gives your signal handler time to save the final checkpoint.

Step 5: Orchestrating the Workflow for Resilience

The true test of fault tolerance comes when an instance is reclaimed and a new one needs to take over. This step involves automating the setup of a new instance to seamlessly resume training from the latest checkpoint.

Using Cloud-Init for Automated Setup

Cloud providers offer mechanisms like cloud-init (or similar user data scripts) to execute commands automatically when a new instance boots up. This is the ideal place to orchestrate your resume workflow:

  1. Install Dependencies: Install necessary software packages (e.g., Python, pip, deep learning frameworks, object storage SDKs).
  2. Fetch Code: Clone your training repository from a version control system (e.g., Git).
  3. Configure Object Storage: Set up credentials for accessing your external object storage.
  4. Download Latest Checkpoint: Implement a script that identifies and downloads the latest checkpoint from your object storage to the instance’s local scratch disk.
  5. Start Training Service: Enable and start your systemd service (as defined in Step 4) which will then load the downloaded checkpoint and resume training.

An example cloud-init user data script snippet:

#cloud-config
runcmd:

The download_latest_checkpoint.py script would use the boto3 library (or equivalent for your object storage) to list objects in your checkpoint bucket, find the one with the highest timestamp or step number, and download it to a known location (e.g., /tmp/checkpoints/latest_checkpoint.pt).

The Resumption Flow

When a new reclaimable GPU instance is provisioned with this cloud-init script, the following sequence occurs:

  1. The instance boots.
  2. cloud-init executes, installing dependencies, cloning your repository, and downloading the latest checkpoint.
  3. Your systemd service is enabled and started.
  4. Your training script starts, detects the downloaded checkpoint, loads the model and optimizer states, and continues training from the exact step where the previous instance left off.

Step 6: Testing Your Fault-Tolerant Setup

The final and most crucial step is to thoroughly test your fault-tolerant workflow. This involves simulating a reclaim event and verifying that your new instance successfully picks up the training without losing progress.

Simulating a Reclaim

To simulate a reclaim, you can simply destroy your running GPU instance through your cloud provider’s control panel or API. This will trigger the termination process, including sending the SIGTERM signal to your running training job.

Verification Steps

  1. Monitor Checkpoint Uploads: Ensure that your training script is regularly uploading checkpoints to your object storage. After manually destroying an instance, verify that a final checkpoint was uploaded shortly before its termination.
  2. Provision a New Instance: Create a new reclaimable GPU instance using the same cloud-init script you’ve configured.
  3. Monitor New Instance Startup: Observe the logs of the new instance (e.g., using journalctl -u my-training-job.service) to confirm that it successfully downloaded the latest checkpoint and started training from the correct epoch and step.
  4. Track Training Progress: Compare the training metrics (loss, accuracy, etc.) from before the reclaim with those after resumption. They should show a continuous trend, indicating no loss of training progress.
  5. Measure Recovery Time: Note the time it takes from destroying the old instance to the new instance actively resuming training. This helps you understand the operational overhead of a reclaim event.

By following these steps, you can build and verify a robust, fault-tolerant machine learning training workflow that leverages the cost benefits of reclaimable GPU instances without sacrificing the integrity or continuity of your valuable training jobs.

To explore more ways to build resilient and efficient online systems, consider visiting Yammbo.