Tutorial: Data-Parallel Multi-GPU Training
Tutorial: Data-Parallel Multi-GPU Training
This tutorial guides you through scaling deep learning training across multiple GPUs on a single workstation using netcl's data-parallel engine.
1. The Theory: What is Data Parallelism?
Data parallelism is the most efficient scaling technique when a neural network model comfortably fits inside the VRAM of a single GPU, but you want to decrease training wall-clock time by distributing mini-batch computations across $N$ devices.
GPU 0 (Device 0)"] GB --> S1["Shard 1 (Size = B / 2)
GPU 1 (Device 1)"] S0 --> F0["Forward Pass & Tape Backward"] --> G0["Local Gradient ∇L₀"] S1 --> F1["Forward Pass & Tape Backward"] --> G1["Local Gradient ∇L₁"] G0 --> AR["all_reduce(mean)
∇L = (∇L₀ + ∇L₁) / 2"] G1 --> AR AR --> U0["GPU 0: Optimizer Update"] AR --> U1["GPU 1: Optimizer Update"]
Mathematical Formulation
Given a global batch $\mathcal{B}$ split into $N$ disjoint worker shards $\mathcal{B}_1, \dots, \mathcal{B}_N$, each worker $i$ independently computes its local loss gradient:
$$\mathbf{g}_i = \frac{1}{|\mathcal{B}_i|} \sum_{x \in \mathcal{B}_i} \nabla_\theta \ell(f(x; \theta), y)$$
Before applying the optimizer update, workers synchronize gradients using a mean reduction:
$$\bar{\mathbf{g}} = \frac{1}{N} \sum_{i=1}^N \mathbf{g}_i$$
Because each device applies identical averaged gradients $\bar{\mathbf{g}}$ to identical initial weights $\theta_t$, model weights remain mathematically synchronized across all GPUs throughout training:
$$\theta_{t+1} = \theta_t - \eta \cdot \text{Update}(\bar{\mathbf{g}})$$
2. Setting Up the Distributed Device Manager
Unlike single-device scripts, data-parallel scripts instantiate netcl.distributed.DeviceManager to discover and allocate queues across available GPUs:
import numpy as np
import netcl.autograd as ag
from netcl.core.device import manager
from netcl.distributed import DeviceManager, prepare_replicas, sync_grads, broadcast_params, shard_batch
from netcl.nn import Linear, ReLU, Dropout, Sequential
from netcl.optim import AdamW
# 1. Discover all physical GPUs on the host
dist_mgr = DeviceManager()
queues = dist_mgr.get_queues()
n_devices = len(queues)
print(f"Allocated {n_devices} device queues:")
for i, q in enumerate(queues):
print(f" Replica {i}: {q.device.name}")
3. Preparing Model Replicas
The prepare_replicas function clones model parameters across each target device queue:
# Create primary model architecture on default device
primary_queue = queues[0]
master_model = Sequential(
Linear(primary_queue, 784, 256), ReLU(), Dropout(p=0.1),
Linear(primary_queue, 256, 128), ReLU(), Dropout(p=0.1),
Linear(primary_queue, 128, 10),
)
# Replicate parameters onto all device queues
param_replicas = prepare_replicas(master_model.parameters(), queues=queues)
# Create an independent optimizer for each replica
optimizers = [
AdamW(params, lr=1e-3, weight_decay=1e-4)
for params in param_replicas
]
Each replica holds an independent copy of model parameters in its local GPU memory buffer, ready for concurrent kernel execution.
4. The Multi-GPU Training Loop
Below is the complete data-parallel training iteration:
# Helper to evaluate forward pass for a specific parameter replica
def replica_forward(params, x):
# params: [W0, b0, W1, b1, W2, b2]
h = ag.add(ag.matmul(x, ag.transpose(params[0])), params[1])
h = ag.relu(h)
h = ag.add(ag.matmul(h, ag.transpose(params[2])), params[3])
h = ag.relu(h)
return ag.add(ag.matmul(h, ag.transpose(params[4])), params[5])
# Synthetic training batch (batch size = 128, features = 784)
batch_x = np.random.randn(128, 784).astype(np.float32)
batch_y = np.random.randint(0, 10, size=(128,)).astype(np.int32)
# Step 1: Shard inputs evenly across replicas
x_shards = shard_batch(batch_x, num_shards=n_devices)
y_shards = shard_batch(batch_y, num_shards=n_devices)
losses = []
# Step 2: Concurrent forward and backward passes per replica
for r_idx, (q, params) in enumerate(zip(queues, param_replicas)):
# Transfer shard to local device memory
x_dev = Tensor.from_host(q, x_shards[r_idx])
y_dev = Tensor.from_host(q, y_shards[r_idx])
with ag.Tape() as tape:
logits = replica_forward(params, ag.tensor(x_dev))
loss = ag.cross_entropy(logits, ag.tensor(y_dev))
# Compute local gradients on replica device
tape.backward(loss)
losses.append(loss.value.to_host()[0])
# Step 3: Mean-reduce gradients across all replicas
sync_grads(param_replicas)
# Step 4: Step each replica's optimizer
for opt in optimizers:
opt.step()
opt.zero_grad()
# Step 5: Broadcast parameters to guarantee zero numerical divergence
broadcast_params(src_params=param_replicas[0], dst_param_groups=param_replicas, root=0)
mean_loss = float(np.mean(losses))
print(f"Parallel step completed. Mean Batch Loss: {mean_loss:.4f}")
5. Using the High-Level data_parallel_step Orchestrator
For production scripts, netcl provides data_parallel_step which wraps sharding, forward-backward execution, gradient reduction, and optimizer updates into a single call:
from netcl.distributed import data_parallel_step
def step_forward(queue, xb, yb, params):
with ag.Tape() as tape:
logits = replica_forward(params, ag.tensor(xb))
loss = ag.cross_entropy(logits, ag.tensor(yb))
return loss, tape
# Train across epochs
for epoch in range(5):
for batch_x, batch_y in dataloader:
loss = data_parallel_step(
step_forward,
param_replicas,
optimizers,
(batch_x, batch_y),
)
broadcast_params(src_params=param_replicas[0], dst_param_groups=param_replicas, root=0)
6. Best Practices for Workstation Multi-GPU
- Scale the Learning Rate: When training with $N$ devices and constant per-device batch size $B$, the effective global batch size increases to $N \times B$. Under the Linear Scaling Rule, consider scaling learning rate $\eta' = N \cdot \eta$ with a brief learning rate warmup.
- Pinned Host Memory:
Ensure data loading utilizes
netcl.data.DataLoaderwith pinned memory buffers so batch transfers do not stall GPU computation. - Always Broadcast After Step:
Call
broadcast_paramsperiodically or after each step to prevent floating-point non-associativity drift across different GPU hardware architectures.
Related Documentation
- Distributed Architecture: Details on shared memory staging and host collectives.
- Distributed API Reference: Full method signatures and configuration options.
- Curriculum Learning Paths: Explore the machine learning and hardware curriculum.