Multi-GPU Training: Data and Tensor Parallelism
DantinoX uses JAX’s SPMD mesh sharding for multi-GPU training, and can
compose Data Parallelism (DP) with Tensor Parallelism (TP) on a
single 2-D mesh. No pmap, no manual AllReduce — XLA handles gradient
synchronisation and (for TP) the intra-layer all-reduce.
Configuration
training:
n_devices: 2 # 0 = use all available GPUs, 1 = single device
batch_size: 64 # must be divisible by n_devices
use_bf16: true # recommended for multi-GPU runs
dantinox train \
--config configs/default_config.yaml \
--n_devices 2 \
--batch_size 64
GPU Selection
Control which GPUs are used via CUDA_VISIBLE_DEVICES:
# Use GPUs 0 and 1 for training, GPU 2 for benchmarks
CUDA_VISIBLE_DEVICES=0,1 dantinox train --config configs/diffusion_base.yaml
The training suite scripts set this automatically:
CUDA_VISIBLE_DEVICES=0,1 bash scripts/train_ar_suite.sh
Scaling Rules
When increasing n_devices, scale batch_size proportionally to keep
the per-device batch size (and thus gradient noise) constant:
|
|
Per-device batch |
Effective LR |
|---|---|---|---|
1 |
32 |
32 |
base LR |
2 |
64 |
32 |
base LR |
4 |
128 |
32 |
base LR |
8 |
256 |
32 |
base LR (or ×√8 with linear scaling) |
Low-level API
from dantinox.core.sharding import make_mesh, replicate, shard_batch, num_devices
mesh = make_mesh(n_devices=4)
print(f"Training on {num_devices(mesh)} GPUs")
# Replicate any pytree to all devices
replicated_state = replicate(nnx.state((model, optimizer)), mesh)
# Shard a batch along axis 0
x_sharded = shard_batch(x, mesh) # [batch, seq_len]
Tensor Parallelism (DP × TP)
Set tp_size > 1 (on ModelConfig, TrainingConfig, or the legacy Config)
to shard individual weight matrices across devices, Megatron-style, in
addition to (or instead of) data parallelism:
train_cfg = dx.TrainingConfig(
lr=3e-4, epochs=5,
n_devices=8, # total devices
tp_size=2, # 2-way tensor parallel → n_dp = n_devices / tp_size = 4
)
dantinox train --config configs/large.yaml --n_devices 8 --tp_size 2
Mesh shape. With tp_size=1 (the default), the Trainer builds the same
1-D data-parallel mesh described above via make_mesh. With tp_size > 1,
it instead builds a 2-D (data, model) mesh via
core.sharding.make_tp_mesh(n_tp=tp_size, n_dp=n_devices // tp_size), and
requires n_devices to be evenly divisible by tp_size (falls back to the
largest divisor otherwise, with a warning).
Weight sharding. core.sharding.apply_tp_sharding(model, mesh) shards
the backbone’s linear layers Megatron-style, before the first JIT-compiled
forward pass:
Layer |
Parallelism |
Sharded axis |
|---|---|---|
|
Column-parallel |
Output (kernel’s last axis) |
|
Row-parallel |
Input (kernel’s first axis) |
Row-parallel biases are pre-scaled by 1/tp_size so that the intra-layer
all-reduce (summing each device’s partial output) reconstructs the correct
bias exactly once rather than tp_size times. core.sharding.in_mesh_context()
is what row-parallel attention/FFN layers check at trace time to decide
whether to emit that all-reduce at all — outside a TP mesh (tp_size=1),
it’s skipped entirely and layers behave as plain dense layers.
When to use TP vs. DP. Data parallelism replicates the full model on
every device and only shards the batch — simple and usually fastest, but
bounded by how big a model fits on one device. Tensor parallelism shards the
weights themselves, letting you train models too large for a single device’s
memory, at the cost of the extra intra-layer communication. tp_size and
n_devices compose: n_devices=8, tp_size=2 gives 4-way data parallelism
over 2-way tensor-parallel model replicas.