Mengoptimalkan training di GPU: mixed precision AMP/BF16, optimasi memori dengan gradient accumulation dan batch size, lalu distributed training dengan DistributedDataParallel (DDP) dan FSDP untuk skala multi-GPU.

Di episode 12 kita membangun transformer kecil yang berjalan di satu GPU (atau CPU). Sekarang pertanyaannya: bagaimana melatih model yang lebih besar dan lebih cepat? Jawabannya bukan "beli GPU lebih besar", melainkan memanfaatkan yang sudah ada secara optimal — dan jika masih kurang, membagi beban ke banyak GPU.
Ini keterampilan yang menentukan karier di 2026: tim riset melatih model 7B-70B parameter dengan multi-GPU, dan ML Engineer yang paham mixed precision serta memori GPU selalu lebih bernilai. Episode ini membekali kalian dari optimasi satu GPU sampai distributed training — konsep yang sama dipakai Hugging Face dan semua lab besar.
Secara default PyTorch memakai FP32 (32-bit float). GPU modern (Volta+) jauh lebih cepat menghitung FP16 (16-bit) — hampir 2x throughput di banyak kasus — dan memakai setengah memori. Masalahnya FP16 punya jangkauan kecil: angka sangat kecil menjadi nol, angka besar menjadi infinity.
BF16 (bfloat16) memecah masalah itu: rentang eksponen sama dengan FP32, hanya presisi mantissa yang dikurangi. Untuk training, BF16 hampir selalu lebih stabil daripada FP16. GPU NVIDIA Ampere (A100/A10/L4) ke atas mendukung BF16; GPU lebih lama hanya FP16.
AMP menyimpan bobot master di FP32 sementara, melakukan forward/backward dalam FP16/BF16, dan menaikkan kembali gradien ke FP32 saat update:
from torch.amp import autocast, GradScaler
scaler = GradScaler("cuda", enabled=True)
for x, y in loader:
optimizer.zero_grad()
with autocast("cuda", dtype=torch.bfloat16):
loss = loss_fn(model(x), y)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()Aturan pakai: BF16 cukup tanpa GradScaler (tidak ada underflow); FP16 butuh GradScaler untuk menangani gradien kecil. Hasilnya: training ~1.5-2x lebih cepat dengan kualitas hampir identik.
Jika batch ideal 64 tapi GPU hanya muat batch 32, jangan langsung mengecilkan batch (kualitas training berubah). Gradient accumulation menjumlahkan gradien dari beberapa batch kecil sebelum update:
accum_steps = 2 # simulasi batch 64 dari batch 32
for i, (x, y) in enumerate(loader):
loss = loss_fn(model(x), y) / accum_steps
loss.backward()
if (i + 1) % accum_steps == 0:
optimizer.step()
optimizer.zero_grad()Bagi loss dengan accum_steps agar skala gradien konsisten. Efek sampingnya: gradien lebih bising karena update lebih jarang; akurasi biasanya tetap baik.
Tip
Lihat pemakaian memori GPU dengan nvidia-smi sambil training. Jika VRAM masih longgar (misal 40GB dari 80GB), naikkan batch size sebelum menambah kompleksitas lain. Jika sudah mentok, gradient accumulation adalah langkah berikutnya.
DP membagi batch ke semua GPU, lalu replikasi model di tiap GPU dengan sinkronisasi gradien lewat satu thread. Mudah dipakai, tetapi scaling-nya buruk (prosesor utama jadi bottleneck) dan tidak efisien — cukup untuk belajar konsep.
model = nn.DataParallel(model) # satu baris, tapi lambat di banyak GPUDDP mereplikasi model dan data di tiap proses (satu proses per GPU). Tiap proses training dengan batch-nya sendiri, lalu gradien disinkronkan antar proses (all-reduce) di setiap step. Skalanya jauh lebih baik daripada DP. Alur standar dengan torchrun:
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
dist.init_process_group("nccl") # nccl untuk NVIDIA GPU
model = DDP(model, device_ids=[local_rank])
for x, y in loader:
loss = loss_fn(model(x), y) # DataLoader sudah samar
loss.backward() # gradien auto all-reduce antar GPU
optimizer.step()Jalankan dengan:
torchrun --nproc_per_node=4 train.pyDetail krusial: DistributedSampler memastikan tiap proses melihat subset data yang berbeda; sampler.set_epoch(epoch) wajib dipanggil tiap epoch agar shuffling antar epoch tetap acak dan tidak bertabrakan.
DDP masih menuntut seluruh model muat di tiap GPU. Untuk model besar, FSDP (Fully Sharded Data Parallel) membagi parameter, gradien, dan optimizer state ke semua GPU — tiap GPU hanya menyimpan bagiannya. Mirip ZeRO dari DeepSpeed. Inilah cara melatih model 7B+ di beberapa GPU:
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy
model = FSDP(model, auto_wrap_policy=size_based_auto_wrap_policy(min_num_params=1e6))FSDP menambah komunikasi antar GPU (parameter di-gather saat forward), sehingga untuk model kecil justru bisa lebih lambat dari DDP. Aturan praktis: DDP untuk model yang muat di satu GPU, FSDP/ZeRO untuk yang tidak muat.
Warning
Sebelum distributed training, pastikan single-GPU training kalian sudah optimal: mixed precision, gradient accumulation, dan batch size maksimal. Distributed training menambah kompleksitas (sinkronisasi, komunikasi, seed) — jangan membayarnya hanya karena masih ada hambatan yang bisa diselesaikan di satu GPU.
dist.barrier() setelah setup.DistributedSampler tanpa set epoch menghasilkan urutan data sama tiap epoch.autocast("cuda") di mesin tanpa GPU error — guard dengan torch.cuda.is_available().Pada episode 13 ini, kalian telah menguasai training skala.
Inti yang harus dibawa pulang:
Di episode 14 selanjutnya kita akan membuat training bisa dilacak dan dituning: experiment tracking & hyperparameter tuning — TensorBoard, Weights & Biases, MLflow, Optuna, dan learning rate finder. Sampai jumpa di episode 14!