Educational PyTorch examples for understanding major single-GPU and multi-GPU training patterns used in modern deep learning systems.
The repository uses moderately sized synthetic Transformer models so that differences in GPU memory usage, utilization, communication, and scaling are easier to observe. The scripts remain focused on training mechanics such as device placement, process groups, batch semantics, sharding, collectives, mixed precision, pipeline scheduling, tensor parallelism, and checkpoint formats.
The dataset is random by design. Therefore, the loss is not expected to improve meaningfully. With 10 random classes, the loss should remain near:
log(10) ≈ 2.30
The primary purpose of this repository is to compare execution behavior, not model accuracy.
| Script | Pattern | Typical launch |
|---|---|---|
train_single.py |
Baseline single-device training loop | uv run python train_single.py |
train_mixed_precision_single_gpu.py |
Single-GPU FP32/FP16/BF16 AMP comparison | uv run python train_mixed_precision_single_gpu.py --precision bf16 |
train_dataparallel.py |
Legacy nn.DataParallel |
uv run python train_dataparallel.py |
train_ddp.py |
DistributedDataParallel data parallelism |
uv run torchrun --standalone --nproc_per_node=3 train_ddp.py |
train_fsdp.py |
FSDP1 full sharding and full-state checkpointing | uv run torchrun --standalone --nproc_per_node=3 train_fsdp.py |
train_fsdp2.py |
FSDP2 fully_shard and distributed checkpointing |
uv run torchrun --standalone --nproc_per_node=3 train_fsdp2.py |
train_model_parallel.py |
Manual layer placement across 3 GPUs | uv run python train_model_parallel.py |
train_pipeline_parallel.py |
3-stage pipeline parallelism with GPipe scheduling | uv run torchrun --standalone --nproc_per_node=3 train_pipeline_parallel.py |
train_tensor_parallel.py |
Pure tensor-parallel MLP sharding with a 1D mesh | uv run torchrun --standalone --nproc_per_node=3 train_tensor_parallel.py |
train_2d_dp_tp.py |
Data-parallel groups combined with tensor-parallel groups | uv run torchrun --standalone --nproc_per_node=4 train_2d_dp_tp.py --tp-size 2 |
The interview study guide is split into focused files under notes/. The original consolidated notes remain in multi_gpu_training_notes.md.
Most examples use a shared Transformer encoder classifier with approximately 110 million parameters.
Vocabulary size: 32,000
Sequence length: 256
Model dimension: 768
Attention heads: 12
Transformer layers: 12
Feed-forward dimension: 3,072
Output classes: 10
The tensor-parallel and 2D DP + TP examples use a Transformer-style MLP model with approximately 138 million parameters.
Vocabulary size: 32,000
Sequence length: 256
Model dimension: 768
MLP hidden dimension: 6,144
MLP layers: 12
Output classes: 10
These models are intentionally large enough to make GPU differences visible, while remaining practical on modern multi-GPU systems such as NVIDIA A40-class hardware.
- Python 3.10+
- NVIDIA GPU or GPUs
- CUDA-capable PyTorch installation
uv- NumPy
- Multiple GPUs for distributed examples
- Exactly 3 visible GPUs for manual model parallelism and pipeline parallelism
Install all declared dependencies:
uv syncIf NumPy is missing, add it to the project:
uv add numpyVerify the environment:
uv run python -c "import numpy, torch; print('NumPy:', numpy.__version__); print('PyTorch:', torch.__version__); print('CUDA:', torch.cuda.is_available()); print('GPU count:', torch.cuda.device_count())"Sanity-check the repository entry point:
uv run python main.pyOpen a second terminal while a training command is running.
Refresh the standard NVIDIA GPU status display every second:
watch -n 1 nvidia-smiPress Ctrl+C to stop watching.
This view shows:
- GPU utilization
- memory usage
- temperature
- power consumption
- active processes
- process IDs
- GPU assignments
A typical workflow is:
Terminal 1 — start monitoring
watch -n 1 nvidia-smiTerminal 2 — start training
CUDA_VISIBLE_DEVICES=0,1,2 uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_pipeline_parallel.pyThis makes it easy to observe how memory and utilization change across GPUs during initialization, forward passes, backward passes, communication, and optimizer steps.
nvidia-smiwatch -n 1 nvidia-smi -i 0,1,2nvidia-smi \
--query-gpu=index,name,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw \
--format=csv \
-l 1nvidia-smi pmon -s umGPU topology is useful when interpreting peer-to-peer communication performance:
nvidia-smi topo -mLook for connections such as:
NV# NVLink connection
PIX same PCIe switch
PXB multiple PCIe bridges
PHB PCIe host bridge
SYS CPU/socket or NUMA interconnect
Pipeline, tensor, and distributed training performance may depend strongly on GPU interconnect topology.
Use CUDA_VISIBLE_DEVICES before the command.
Run on one selected physical GPU:
CUDA_VISIBLE_DEVICES=1 uv run python train_single.pyUse stable PCI bus ordering:
CUDA_DEVICE_ORDER=PCI_BUS_ID \
CUDA_VISIBLE_DEVICES=0,2 \
uv run python train_dataparallel.pyCUDA remaps visible physical GPUs to local process indices.
For example:
CUDA_VISIBLE_DEVICES=2,3Inside the program:
cuda:0 -> physical GPU 2
cuda:1 -> physical GPU 3
Therefore, scripts with a --device argument should generally use local device 0 after selecting a single physical GPU:
CUDA_VISIBLE_DEVICES=2 \
uv run python train_mixed_precision_single_gpu.py \
--device 0 \
--precision bf16CUDA_VISIBLE_DEVICES=0 \
uv run python train_single.pyUse this as the baseline for:
- iteration time
- GPU memory usage
- GPU utilization
- model replication overhead
- comparison with multi-GPU methods
FP32:
CUDA_VISIBLE_DEVICES=0 \
uv run python train_mixed_precision_single_gpu.py \
--device 0 \
--precision fp32FP16:
CUDA_VISIBLE_DEVICES=0 \
uv run python train_mixed_precision_single_gpu.py \
--device 0 \
--precision fp16BF16:
CUDA_VISIBLE_DEVICES=0 \
uv run python train_mixed_precision_single_gpu.py \
--device 0 \
--precision bf16Compare:
- peak allocated memory
- reserved memory
- step time
- numerical stability
- GPU utilization
BF16 is generally the most practical reduced-precision mode on A40-class GPUs.
CUDA_VISIBLE_DEVICES=0,1 \
uv run python train_dataparallel.pynn.DataParallel uses one Python process and splits batches across visible GPUs. GPU 0 acts as the primary device and may use more memory than the other GPUs.
Watch for uneven memory usage:
watch -n 1 nvidia-smiTwo GPUs:
CUDA_VISIBLE_DEVICES=0,1 \
uv run torchrun \
--standalone \
--nproc_per_node=2 \
train_ddp.pyThree GPUs:
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_ddp.pyFor torchrun, --nproc_per_node must equal the number of visible GPUs.
DDP creates one process per GPU and synchronizes gradients through collectives.
Observe:
- similar memory usage across GPUs
- similar utilization across GPUs
- gradient synchronization overhead
- global batch-size growth with world size
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_fsdp.pyFSDP shards model parameters, gradients, and optimizer-related state across ranks.
Observe:
- lower persistent model-state memory per GPU
- temporary memory increases during parameter all-gathers
- communication during forward and backward passes
- checkpoint creation at the end of training
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_fsdp2.pyFSDP2 uses fully_shard and DTensor-backed state.
The script may generate:
fsdp2_tiny_transformer_full_state.pt
fsdp2_tiny_transformer_sharded_dcp/
Observe:
- sharded parameter memory
- resharding behavior
- distributed checkpoint writing
- differences from FSDP1
This example expects exactly 3 visible GPUs:
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run python train_model_parallel.pyThe model is divided by layers:
GPU 0:
embeddings
first 4 Transformer layers
GPU 1:
middle 4 Transformer layers
GPU 2:
final 4 Transformer layers
normalization
classifier head
loss
Observe:
- model memory distributed across GPUs
- activation transfers between devices
- sequential execution between model partitions
- GPU utilization gaps caused by lack of pipeline overlap
Pipeline parallelism expects exactly 3 ranks:
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_pipeline_parallel.pyCurrent pipeline configuration:
Pipeline stages: 3
Layers per stage: 4
Total layers: 12
Batch size: 96
Microbatches: 6
Microbatch size: 16
The global batch is divided into microbatches and processed using GPipe scheduling.
Watch GPU activity in another terminal:
watch -n 1 nvidia-smiExpected startup messages may include NCCL warnings about unbatched point-to-point operations. These warnings do not necessarily indicate a failure.
NumPy must be installed because pipeline stage metadata exchange may internally use NumPy:
uv add numpyObserve:
- pipeline fill and drain periods
- utilization overlap between stages
- memory differences among stages
- point-to-point activation communication
- pipeline bubbles
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_tensor_parallel.pyAll tensor-parallel ranks process the same logical batch.
The MLP projection matrices are sharded:
fc1: column parallel
fc2: row parallel
Observe:
- model tensor shards across GPUs
- communication inside each MLP block
- similar input data on all ranks
- reduced parameter size for sharded linear layers
- synchronization overhead at every layer
Four GPUs with TP size 2:
CUDA_VISIBLE_DEVICES=0,1,2,3 \
uv run torchrun \
--standalone \
--nproc_per_node=4 \
train_2d_dp_tp.py \
--tp-size 2Conceptual layout:
DP replica 0:
ranks 0 and 1 form TP group 0
DP replica 1:
ranks 2 and 3 form TP group 1
Within a TP group, ranks process the same batch. Across DP replicas, different batches are processed.
Observe:
- tensor-parallel communication within each TP group
- gradient synchronization across DP replicas
- memory distribution across all GPUs
- interaction between DP and TP communication
TORCH_DISTRIBUTED_DEBUG=DETAIL \
CUDA_VISIBLE_DEVICES=0,1 \
uv run torchrun \
--standalone \
--nproc_per_node=2 \
train_ddp.pyNCCL_DEBUG=INFO \
CUDA_VISIBLE_DEVICES=0,1 \
uv run torchrun \
--standalone \
--nproc_per_node=2 \
train_ddp.pyFor additional NCCL subsystem details:
NCCL_DEBUG=INFO \
NCCL_DEBUG_SUBSYS=INIT,COLL,P2P \
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run torchrun \
--standalone \
--nproc_per_node=3 \
train_pipeline_parallel.pyCUDA_LAUNCH_BLOCKING=1 \
CUDA_VISIBLE_DEVICES=0 \
uv run python train_single.pyThis is useful for debugging but may significantly slow execution.
Error:
RuntimeError: Numpy is not available
Fix:
uv add numpyVerify:
uv run python -c "import numpy; print(numpy.__version__)"When one rank fails, torchrun normally sends termination signals to the remaining ranks.
Example:
Sending process ... closing signal SIGTERM
The SIGTERM messages are usually consequences of the first rank failure. Locate the earliest traceback and fix that root cause.
Reduce the batch size in the relevant script or through a supported command-line argument.
For mixed precision:
CUDA_VISIBLE_DEVICES=0 \
uv run python train_mixed_precision_single_gpu.py \
--device 0 \
--precision bf16 \
--batch-size 16For 2D DP + TP:
CUDA_VISIBLE_DEVICES=0,1,2,3 \
uv run torchrun \
--standalone \
--nproc_per_node=4 \
train_2d_dp_tp.py \
--tp-size 2 \
--batch-size 16Also check whether another process is occupying GPU memory:
nvidia-smiCheck visible GPUs:
CUDA_VISIBLE_DEVICES=0,1,2 \
uv run python -c "import torch; print(torch.cuda.device_count())"For distributed runs, ensure:
number of CUDA_VISIBLE_DEVICES entries
=
--nproc_per_node
Use a different rendezvous port:
CUDA_VISIBLE_DEVICES=0,1 \
uv run torchrun \
--standalone \
--master-port=29601 \
--nproc_per_node=2 \
train_ddp.py- Run
train_single.pyas the baseline. - Compare FP32, FP16, and BF16 with
train_mixed_precision_single_gpu.py. - Compare
train_dataparallel.pywithtrain_ddp.py. - Compare DDP with FSDP1 and FSDP2.
- Run manual model parallelism and observe sequential device activity.
- Run pipeline parallelism and observe microbatch overlap.
- Run pure tensor parallelism.
- Run the 2D DP + TP example.
For every experiment, keep this running in a separate terminal:
watch -n 1 nvidia-smiRecord:
- GPU memory used
- GPU utilization
- power draw
- step time
- total runtime
- effective batch size
- number of GPUs
- precision mode
- any communication warnings
src/
data.py synthetic random-token dataset
model.py shared Transformer classifier
runtime.py CUDA and runtime helpers
notes/
README.md study-guide index
... topic-specific training notes
train_single.py
train_mixed_precision_single_gpu.py
train_dataparallel.py
train_ddp.py
train_fsdp.py
train_fsdp2.py
train_model_parallel.py
train_pipeline_parallel.py
train_tensor_parallel.py
train_2d_dp_tp.py
main.py
pyproject.toml
uv.lock
FSDP and FSDP2 examples may generate files such as:
fsdp_tiny_transformer_full_state.pt
fsdp2_tiny_transformer_full_state.pt
fsdp2_tiny_transformer_sharded_dcp/
These are training outputs and should normally remain ignored by Git.
This repository is a learning and experimentation project rather than a formal benchmark suite.
The examples favor explicit code over heavy abstraction so that the following behaviors remain visible:
- process creation
- device placement
- batch distribution
- gradient synchronization
- parameter sharding
- activation movement
- point-to-point communication
- pipeline scheduling
- tensor sharding
- checkpoint formats
When reporting results, include the GPU model, number of GPUs, PyTorch version, CUDA version, precision, batch size, model size, and the command used.