Use Ray for Distributed Training
Learn how to use Ray for distributed training in this Applied AI engineering tutorial. Hands-on steps, practical examples, and what to study next.
Focus: use ray for distributed training
You’ve spent days training a model on a single GPU, watching the progress bar crawl and your deadlines slip. As your dataset grows and your architecture gets deeper, the gap between what you need and what your laptop can deliver becomes a chasm. The solution isn’t a bigger machine — it’s a smarter way to use the machines you already have. That’s exactly what Ray gives you: a way to take your existing PyTorch training script and scale it across multiple GPUs and machines, often with just a few lines of code.
The problem this lesson solves
Training modern deep learning models is compute-hungry. A ResNet-50 on ImageNet can take days on a single GPU. A transformer language model? Weeks. The typical approach — bumping up to a more expensive instance — hits a wall: single-GPU performance has plateaued, and the cost curve is steep. Meanwhile, most teams already have access to spare GPUs in the cloud or on-premises, but they’re sitting idle because the software to orchestrate them is too complex.
That’s the real pain point: distributed training is hard. You have to split your data, synchronize gradients, handle worker failures, and manage the cluster lifecycle — all while keeping your code readable and debuggable. Naive implementations break on the first network hiccup. Before Ray, the standard tool was Horovod or hand-rolled parameter servers, and both demanded deep expertise.
Ray changes the equation. It abstracts away the cluster mechanics, letting you focus on the training logic. This lesson walks you through using Ray for distributed training end-to-end: the mental model, the step-by-step mechanics, a hands-on exercise, and the troubleshooting you’ll need in production.
Core concept / mental model
Think of Ray as a conductor for an orchestra of workers. You, the data scientist, write the music (the training script). The conductor (Ray) doesn’t care about the melody — it ensures every musician (a CPU core, a GPU, a node) plays in time, follows the beat, and recovers if one drifts out of sync.
Ray’s architecture breaks down into three layers:
- Ray Core — the low-level primitives: remote functions (
@ray.remote) and actors (stateful objects). These let you distribute any Python code, not just ML. - Ray Data — a distributed data pipeline that replaces your Pandas/loading bottlenecks. It loads, shuffles, and batches data in parallel, feeding the training job without starving the GPUs.
- Ray Train — the layer you’ll use most for distributed training. It wraps your PyTorch (or TensorFlow) training loop and handles the distributed strategy: data parallelism, gradient synchronization, and checkpointing.
Here’s the key mental model: Ray Train makes a single-machine training script look like a regular function, then scales it by replicating that function across workers and synchronizing their gradients. You’re not rewriting your model architecture — you’re just telling Ray, “run this on N workers.”
For data parallelism, the model weights are copied to each worker. Each worker sees a different batch of data. After every forward/backward pass, the gradients are averaged across workers (like a group project where everyone shares their partial answers, then all update together). Ray handles the communication via NCCL on GPUs or Gloo on CPUs.
Pro tip: Ray is not a replacement for your deep learning framework. It’s an orchestration layer. PyTorch still does the heavy lifting; Ray just makes it scalable.
How it works step by step
The mechanics of using Ray for distributed training follow a clear sequence. Let’s break it down:
Step 1: Initialize a Ray cluster
You start a Ray runtime — either on a single machine (for local testing) or a cluster (for production). In code, you call ray.init() with your cluster address. For a single node, you can even use ray.init() with no arguments.
Step 2: Define your training function
Write a Python function that takes a dataset reference and a config dict. Inside, you build your model, define your loss and optimizer, and run the training loop — just like a normal script.
Step 3: Set up distributed training with Trainer
Ray Train provides a TorchTrainer (or TensorflowTrainer) class. You pass it your training function and a ScalingConfig that specifies the number of workers and the compute resources (e.g., two workers with one GPU each).
Step 4: Run, checkpoint, and scale
The trainer handles the entire lifecycle: it spawns workers, initializes the distributed communication, loads data, runs the training loop, and saves checkpoints. You can scale by changing num_workers — no code changes.
Hands-on walkthrough
Let’s put this into practice. We’ll train a simple neural network on synthetic data, first on a single worker, then scale to two.
First, install Ray (if you haven’t):
pip install "ray[default]" "ray[train]" torch
Now, create a file ray_train_demo.py:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
from ray import train
from ray.train import ScalingConfig
from ray.train.torch import TorchTrainer
# 1. Define a simple model
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10, 64)
self.fc2 = nn.Linear(64, 1)
def forward(self, x):
x = F.relu(self.fc1(x))
return self.fc2(x)
# 2. Define the training function (this runs on each worker)
def train_func(config):
# Get the current worker's data shard
data = train.get_dataset_shard("train")
# Build the model and move to the right device
model = Net()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = train.torch.prepare_model(model)
# Optimizer and loss
optimizer = torch.optim.Adam(model.parameters(), lr=config["lr"])
loss_fn = nn.MSELoss()
# Wrap the data loader for distributed training
dataloader = DataLoader(data, batch_size=32)
dataloader = train.torch.prepare_data_loader(dataloader)
# Training loop
model.train()
for epoch in range(config["epochs"]):
total_loss = 0.0
for X, y in dataloader:
X, y = X.float().to(device), y.float().to(device)
optimizer.zero_grad()
output = model(X)
loss = loss_fn(output, y)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}, loss: {total_loss/len(dataloader):.4f}")
# 3. Create a Ray Dataset (synthetic)
def create_dataset():
import ray
X = torch.randn(10000, 10)
y = torch.randn(10000, 1)
ds = ray.data.from_torch(TensorDataset(X, y))
return ds
if __name__ == "__main__":
# Initialize Ray (local)
ray.init()
# Create the dataset
train_ds = create_dataset()
# 4. Configure the trainer
trainer = TorchTrainer(
train_func,
scaling_config=ScalingConfig(num_workers=2, use_gpu=False), # set use_gpu=True for GPUs
datasets={"train": train_ds},
run_config=ray.train.RunConfig(checkpoint_config=ray.train.CheckpointConfig(num_to_keep=1)),
train_loop_config={"lr": 0.001, "epochs": 3},
)
# 5. Run training
result = trainer.fit()
print("Training complete!")
To run it:
python ray_train_demo.py
Expected output (loss values will vary):
Epoch 1, loss: 0.8734
Epoch 2, loss: 0.8012
Epoch 3, loss: 0.7345
Training complete!
Notice how the train_func is written exactly like a single-GPU training loop. Ray handles the data sharding, gradient synchronization, and device placement behind the scenes. That’s the magic.
Scaling to multiple GPUs
To use multiple GPUs, change the ScalingConfig:
scaling_config=ScalingConfig(num_workers=4, use_gpu=True)
Ray will allocate one GPU per worker. You can also set resources_per_worker to control memory and CPU.
Compare options / when to choose what
Ray is not the only distributed training tool. Here’s how it stacks up against the alternatives:
| Tool | Best for | Ease of use | Cluster management | Integration with Ray ecosystem |
|---|---|---|---|---|
| Ray Train | Teams wanting a unified ecosystem for data, training, and serving | High | Built-in (via Ray Cluster) | Excellent — integrates with Ray Serve and Ray Data |
| Horovod | Existing PyTorch/TensorFlow projects needing minimal changes | Medium | Separate (e.g., OpenMPI) | Limited |
| PyTorch DDP (DistributedDataParallel) | Fine-grained control over distributed training | Low | Manual — you handle init_method and world size |
None |
| PyTorch Lightening + DDP | Quick prototyping with high-level API | High | Manual (but simpler) | Partial |
| DeepSpeed | Extreme optimization (ZeRO, memory savings) | Medium | Requires DeepSpeed engine | Limited |
When to choose Ray Train: - You already use Ray for other parts of your ML pipeline (data processing, model serving). - You want a single tool that handles everything from data loading to checkpointing. - You need fault tolerance and automatic retries for long-running jobs.
When to choose alternatives: - You only need to scale on a single multi-GPU node — PyTorch DDP is enough. - You have a legacy codebase already using Horovod. - You need to squeeze the last drop of performance with ZeRO-3 style optimization — DeepSpeed might be better.
Troubleshooting & edge cases
Even with Ray, things can go wrong. Here are the common pitfalls and how to fix them:
1. Workers hang or no progress
- Cause: The Ray cluster isn’t properly initialized, or the distributed backend (NCCL) fails.
- Fix: Ensure
ray.init()is called exactly once. For NCCL issues, setNCCL_DEBUG=INFOto see the communication logs. Tryuse_gpu=Falseto test with CPU first.
2. Out-of-memory errors
- Cause: Dataset is too large and gets duplicated on each worker, or the batch size is too big.
- Fix: Use
ray.dataand let it shard. Reduce batch size. Setresources_per_workerwith memory limits.
3. Data loading is a bottleneck
- Cause: Workers are idle waiting for data.
- Fix: Use Ray Data with
.prefetch()and setdatasetsin the trainer to pipeline data loading.
4. Model not converging
- Cause: Learning rate too high for the increased batch size (due to aggregation).
- Fix: Scale the learning rate linearly with the number of workers (e.g., double LR when using two workers).
5. Checkpointing failure
- Cause: Checkpoint directory not accessible from all workers.
- Fix: Make sure the filesystem is shared (e.g., NFS or cloud storage). Use Ray’s default checkpointing which writes to the driver.
What you learned & what's next
You’ve covered a lot: you now understand how Ray can take a single-GPU training script and scale it across a cluster with minimal changes. You learned the core mental model of Ray as an orchestration layer, saw a full hands-on example with TorchTrainer, compared Ray to alternative tools, and know how to troubleshoot common issues. You can now explain the core idea behind using Ray for distributed training — it’s about abstraction: Ray handles the messy cluster mechanics, leaving you to focus on model quality.
As a next step in your Applied AI engineering journey, consider exploring Ray Serve for deploying the model you just trained as a production API. That’s the natural follow-on: you’ve scaled the training, now scale the inference. Ray Serve integrates seamlessly — it can load the checkpoints you saved and serve them with autoscaling, batching, and multi-model support.
Keep building on the momentum. The full power of Ray isn’t just distributed training; it’s the entire pipeline — data, training, and serving — on a single platform.
Practice recap
As a quick exercise, run the provided example with num_workers=1 and record the loss curves. Then change to num_workers=2 (or 4 if you have multiple CPU cores) and note how the training time changes. Try increasing the dataset size to 1 million samples and observe how Ray Data sharding keeps the throughput steady. Finally, experiment with adjusting the learning rate by a factor proportional to num_workers and see how it affects convergence.
Common mistakes
- Calling
ray.init()inside the training function — each worker will reinitialize the cluster, causing hangs. Call it once in the driver script. - Setting
use_gpu=Truebut not having CUDA available in the environment leads to cryptic errors. Always test on CPU first. - Forgetting to scale the learning rate with the number of workers. Doubling workers without adjusting LR often results in a loss explosion.
- Using a regular
torch.utils.data.DataLoaderwithoutprepare_data_loader— it won’t shard or wrap with the distributed sampler, leading to every worker seeing the same data.
Variations
- For TensorFlow users, Ray also provides a
TensorflowTrainerwith a very similar API toTorchTrainer. - If you need fine-grained control, you can drop to Ray Core’s
@ray.remotefor custom distributed loops — but you lose the built-in checkpointing and failure handling of Ray Train. - For large model training, you can integrate Ray with DeepSpeed ZeRO-3 via
ScalingConfigoptions likedeepspeed_config, giving you memory optimization and Ray’s orchestration in one.
Real-world use cases
- Retraining a recommendation model on 100 GB of user interaction logs across a multi-GPU cluster — Ray Data handles the shuffling.
- Fine-tuning a large language model on multiple GPUs in a cloud VM — with Ray Train, you can scale from 1 to 8 GPUs just by changing
num_workers. - Running nightly distributed training jobs for a computer vision model in a K8s cluster — Ray’s fault tolerance automatically restarts failed workers.
Key takeaways
- Ray abstracts distributed training, letting you scale your existing PyTorch loop without rewriting the model code.
- Ray Train uses data parallelism: each worker trains on a shard of data and gradients are synchronized — this drives scaling.
- The
TorchTrainerAPI takes a training function, aScalingConfig, and optional datasets, then handles the cluster lifecycle for you. - Compare Ray with DDP, Horovod, or DeepSpeed based on your needs for ease of use, cluster management, and advanced optimizations.
- Always test on a single worker first, then scale; troubleshoot NCCL issues with
NCCL_DEBUG=INFO.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.