Tutorial — Distributed Training¶
Notebook: distributed_training.ipynb
(download)
What you'll build¶
A single script that trains a model with DDP-style gradient sync and
ZeRO-1 optimizer-state sharding, runnable as 1-process-per-GPU via
SneppX_ALG.launch. The notebook also covers DistributedSampler and the
FSDP (ZeRO-3) path.
Setup¶
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
$env:PYTHONPATH = "bindings/python"
import numpy as np
from SneppX_ALG import (
Transformer, DistributedWrapper, AdamW, CrossEntropyLoss,
DistributedSampler, get_world_size, get_rank,
init_process_group, destroy_process_group, FullyShardedDataParallel,
FSDPConfig, ShardingStrategy, launch, TensorDataset, Tensor,
)
from SneppX_ALG.interface_bindings.data_loader import DataLoader
HAS_C = __import__("SneppX_ALG")._HAS_C_BACKEND
1. The training function (runs in each rank)¶
def main():
init_process_group(backend="nccl") # reads WORLD_SIZE/RANK env
rank = get_rank(); world = get_world_size()
print(f"rank {rank}/{world}")
model = Transformer(vocab_size=800, dim=256, num_heads=4, num_layers=4, ffn_dim=1024, max_seq_len=64)
dp_model = DistributedWrapper(model, device="cuda" if world > 1 else "cpu")
# synthetic sharded data
X = Tensor.randn((256, 64)); y = Tensor(np.random.randint(0, 800, (256, 64)))
ds = TensorDataset(X, y)
sampler = DistributedSampler(ds, num_replicas=world, rank=rank, shuffle=True)
loader = DataLoader(ds, batch_size=32, sampler=sampler)
opt = AdamW(dp_model.parameters(), lr=2e-4, weight_decay=0.01)
for epoch in range(2):
sampler.set_epoch(epoch)
for xb, yb in loader:
logits = dp_model(xb)
loss = CrossEntropyLoss()(logits.reshape((-1, 800)), yb.reshape((-1,)))
opt.zero_grad(); loss.backward(); dp_model.sync_gradients(); opt.step()
if rank == 0:
print(f"epoch {epoch} loss={loss.item():.4f}")
destroy_process_group()
2. Launch 2 GPUs (or 2 processes)¶
if __name__ == "__main__":
launch(main, num_nodes=1, num_gpus=2)
launch spawns torch.distributed.run under the hood when
num_nodes * num_gpus > 1; otherwise it calls main() directly — so the
notebook cell can be run as-is for a smoke test with WORLD_SIZE=1.
3. ZeRO-1 (shard optimizer state) with FSDP¶
For larger models, wrap in FullyShardedDataParallel (FULL_SHARD = ZeRO-3):
base = Transformer(vocab_size=800, dim=512, num_heads=8, num_layers=12, ffn_dim=2048, max_seq_len=128)
fsdp_cfg = FSDPConfig(
sharding=ShardingStrategy.FULL_SHARD,
mixed_precision=MixedPrecision(param="bf16", reduce="fp32"),
)
model = FullyShardedDataParallel(base, fsdp_cfg)
# model.parameters() are now sharded; grads all-reduce on backward (C backend)
4. ZeRO-2/3 directly (CPU/CUDA kernel)¶
/* kernel/distributed/zero.c */
SNEPPX_ZeROOptimizer* zopt = SNEPPX_zero_optimizer_create(
opt, world_size, rank, /*stage=*/3);
SNEPPX_zero_step(zopt, grads, n_grads, params, n_params);
Key takeaways¶
DistributedWrapper.sync_gradients()divides grads byworld_sizeafter the all-reduce — so use the local loss, notloss / world_size.DistributedSampler.set_epoch()must be called each epoch to re-shuffle across ranks.launchshort-circuits for single-process runs — great for smoke tests.- FSDP
FULL_SHARDis the recommended way to fit 7B+ models on 4× GPUs. - NCCL must be on
PATH/LD_LIBRARY_PATH; without it the Python side falls back to no-op collectives (pass-throughs data).
Next steps¶
- Combine with RLHF on LoRA adapters for PPO.
- Use Profiling & Benchmarks to spot communication bubbles.