Galat out of memory pada GPU hampir selalu muncul di saat paling buruk: pelatihan berjalan stabil, lalu satu batch yang sedikit lebih besar masuk dan seluruh proses berhenti. Akar masalahnya sederhana — total kebutuhan memori pelatihan jauh lebih besar daripada ukuran berkas model, karena gradien, state optimizer, dan aktivasi menempati memori yang sama. Kabar baiknya, semua komponen tersebut bisa dihitung dengan aritmetika sederhana sebelum satu tensor pun dialokasikan. Chapter ini membedah komponen memori pelatihan satu per satu, memverifikasi rumusnya pada model mini yang dijalankan nyata, lalu memetakan gejala umum kegagalan memori ke tindakan perbaikannya.
Anatomi Memori Pelatihan #
Mari kita mulai dari rumus dasarnya. Untuk model dengan P parameter bertipe float32, setiap komponen menempati memori sebagai berikut:
| Komponen | Rumus byte | 1 juta parameter | Kapan dialokasikan |
|---|---|---|---|
| Parameter | 4 × P | 4 MB | saat model dibangun |
| Gradien | 4 × P | 4 MB | saat backward() pertama |
State Adam (exp_avg, exp_avg_sq) | 8 × P | 8 MB | saat optimizer.step() pertama |
| State SGD + momentum | 4 × P | 4 MB | saat optimizer.step() pertama |
| Aktivasi | batch × lebar layer × 4 | bervariasi | saat forward() |
Dua fakta penting langsung terlihat dari tabel. Pertama, optimizer Adam menyimpan dua tensor tambahan seukuran parameter — exp_avg (rata-rata gradien bergerak) dan exp_avg_sq (rata-rata kuadrat gradien) — sehingga total state mencapai 16 byte per parameter. Kedua, aktivasi adalah satu-satunya komponen yang tumbuh mengikuti ukuran batch dan panjang sequence, bukan jumlah parameter; inilah sebabnya kehabisan memori justru sering terjadi saat batch dinaikkan. Peta komponen tersebut dapat dilihat pada diagram berikut:
Hitungan Nyata untuk Berbagai Ukuran Model #
Rumus di atas hanya berguna bila angkanya benar. Berkas estimasi_memori.py menghitung tabel memori untuk model 1 juta hingga 1 miliar parameter, lalu memverifikasi rumus 16 × P pada model mini PyTorch yang benar-benar dijalankan — memori parameter, gradien, dan state Adam dihitung dari tensor sungguhan, bukan dari tebakan:
"""Anatomi memori pelatihan: rumus byte per komponen + verifikasi nyata."""
import sys
import torch
torch.manual_seed(42)
def ke_mb(byte):
return byte / 1024**2
def ke_gb(byte):
return byte / 1024**3
# ---------- 1. Rumus komponen memori untuk P parameter float32 ----------
print("Komponen memori pelatihan (P = jumlah parameter, float32):")
print(f"{'Komponen':26s} {'Rumus':>6s} {'Per 1 juta param':>18s}")
for nama, rumus in [
("Parameter", "4*P"),
("Gradien", "4*P"),
("Adam exp_avg (m)", "4*P"),
("Adam exp_avg_sq (v)", "4*P"),
]:
print(f"{nama:26s} {rumus:>6s} {4 * 1_000_000:>14,} B")
print()
print("Total state pelatihan Adam (tanpa aktivasi): 16*P byte")
print("Pembanding: SGD 8*P, SGD+momentum 12*P")
print()
print(f"{'P':>14s} {'Parameter':>10s} {'Gradien':>10s} {'State Adam':>10s} {'Total 16P':>10s}")
for P in [1_000_000, 7_000_000, 70_000_000, 1_000_000_000]:
if P >= 1_000_000_000:
baris = [f"{ke_gb(b):>8.1f} GB" for b in (4 * P, 4 * P, 8 * P, 16 * P)]
else:
baris = [f"{ke_mb(b):>8.1f} MB" for b in (4 * P, 4 * P, 8 * P, 16 * P)]
print(f"{P:>14,} {baris[0]:>10s} {baris[1]:>10s} {baris[2]:>10s} {baris[3]:>10s}")
# ---------- 2. Verifikasi nyata pada model mini di CPU ----------
print()
print("=== Verifikasi nyata pada model mini (batch 64) ===")
model = torch.nn.Sequential(
torch.nn.Linear(16, 1024),
torch.nn.Tanh(),
torch.nn.Linear(1024, 1024),
torch.nn.Tanh(),
torch.nn.Linear(1024, 8),
)
P = sum(p.numel() for p in model.parameters())
byte_param = sum(p.numel() * p.element_size() for p in model.parameters())
print(f"jumlah parameter : {P:,}")
print(f"memori parameter : {byte_param:,} byte (rumus 4*P: {4 * P:,})")
x = torch.randn(64, 16)
target = torch.randn(64, 8)
loss = ((model(x) - target) ** 2).mean()
loss.backward()
byte_grad = sum(p.grad.numel() * p.grad.element_size() for p in model.parameters())
print(f"memori gradien : {byte_grad:,} byte (rumus 4*P: {4 * P:,})")
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
opt.step()
byte_moment = 0
byte_step = 0
for state in opt.state.values():
for k, t in state.items():
if isinstance(t, torch.Tensor):
if k in ("exp_avg", "exp_avg_sq"):
byte_moment += t.numel() * t.element_size()
else: # penghitung langkah "step" per parameter
byte_step += t.numel() * t.element_size()
print(f"memori state Adam : {byte_moment:,} byte (rumus 8*P: {8 * P:,})")
print(f"penghitung step : {byte_step:,} byte (6 tensor skalar float32)")
total = byte_param + byte_grad + byte_moment + byte_step
print(f"total state : {total:,} byte (rumus 16*P: {16 * P:,})")
print(f"rumus 16*P cocok : {16 * P == byte_param + byte_grad + byte_moment}")
# ---------- 3. Aktivasi dan jebakan pengukuran ----------
d_in = [16, 1024, 1024]
aktivasi = sum(64 * d * 4 for d in d_in)
print()
print(f"aktivasi tersimpan (input 3 layer Linear, batch 64): {aktivasi:,} byte")
print(f"sys.getsizeof(model) : {sys.getsizeof(model)} byte (objek Python saja)")
print(f"sys.getsizeof salah faktor ~{total // max(sys.getsizeof(model), 1):,}x")Output yang Dihasilkan:
Komponen memori pelatihan (P = jumlah parameter, float32):
Komponen Rumus Per 1 juta param
Parameter 4*P 4,000,000 B
Gradien 4*P 4,000,000 B
Adam exp_avg (m) 4*P 4,000,000 B
Adam exp_avg_sq (v) 4*P 4,000,000 B
Total state pelatihan Adam (tanpa aktivasi): 16*P byte
Pembanding: SGD 8*P, SGD+momentum 12*P
P Parameter Gradien State Adam Total 16P
1,000,000 3.8 MB 3.8 MB 7.6 MB 15.3 MB
7,000,000 26.7 MB 26.7 MB 53.4 MB 106.8 MB
70,000,000 267.0 MB 267.0 MB 534.1 MB 1068.1 MB
1,000,000,000 3.7 GB 3.7 GB 7.5 GB 14.9 GB
=== Verifikasi nyata pada model mini (batch 64) ===
jumlah parameter : 1,075,208
memori parameter : 4,300,832 byte (rumus 4*P: 4,300,832)
memori gradien : 4,300,832 byte (rumus 4*P: 4,300,832)
memori state Adam : 8,601,664 byte (rumus 8*P: 8,601,664)
penghitung step : 24 byte (6 tensor skalar float32)
total state : 17,203,352 byte (rumus 16*P: 17,203,328)
rumus 16*P cocok : True
aktivasi tersimpan (input 3 layer Linear, batch 64): 528,384 byte
sys.getsizeof(model) : 48 byte (objek Python saja)
sys.getsizeof salah faktor ~358,403xMari kita telaah kode di atas baris per baris:
- Baris 25-37 (tabel rumus): loop mencetak empat komponen dasar dengan rumus
4*Pmasing-masing; angka “per 1 juta parameter” dihitung langsung dari4 * 1_000_000sehingga 4 MB per komponen — bukan angka tebakan. - Baris 39-50 (tabel skala): untuk setiap nilai P, byte dikonversi ke MB (di bawah 1 miliar) atau GB (di atasnya). Model 70 juta parameter — ukuran distil kecil bahasa — sudah membutuhkan 1068,1 MB hanya untuk state
float32+Adam, sebelum satu aktivasi pun dihitung. - Baris 53-63 (model mini): model tiga layer
Linearmemiliki 1.075.208 parameter;p.numel() * p.element_size()menjumlahkan byte data seluruh parameter sungguhan dan hasilnya persis4 * P. - Baris 65-70 (gradien nyata): setelah
loss.backward(), setiap parameter memiliki atributp.gradberukuran sama dengan parameternya — terbukti 4.300.832 byte, identik dengan memori parameter. - Baris 72-89 (state Adam nyata): setelah
opt.step(), kamusopt.stateberisi tensorexp_avgdanexp_avg_sqseukuran parameter (total 8.601.664 byte =8*P) plus enam tensor skalar penghitungstepsebesar 24 byte. Rumus16*Pterbukti cocok pada tensor sungguhan. - Baris 91-95 (aktivasi dan jebakan): aktivasi untuk MLP ini dihitung dari input tiga layer
Linear(batch 64) — sekitar 0,5 MB. Adapunsys.getsizeof(model)hanya melaporkan 48 byte karena fungsi tersebut mengukur objek pembungkus Python, bukan datanya — jebakan yang sudah kita bedah di chapter Memory Management .
Mendiagnosis Kegagalan Out of Memory #
Galat memori pada GPU biasanya tampak seperti ini di terminal:
RuntimeError: CUDA out of memory. Tried to allocate 2.34 GiB.
GPU 0 has a total capacity of 23.99 GiB of which 1.02 GiB is free.Pesan tersebut menyebutkan ukuran alokasi yang gagal dan kapasitas perangkat, tetapi bukan penyebabnya. Tabel berikut merangkum pola yang paling sering kita temui:
| Gejala | Penyebab paling umum | Tindakan pertama |
|---|---|---|
| Gagal saat batch tertentu saja | aktivasi tumbuh mengikuti batch | turunkan batch, naikkan epoch (lihat Batch Size dan Epoch ) |
| Gagal pada iterasi pertama setelah optimizer aktif | state Adam 8P baru dialokasikan | ganti ke SGD+momentum, atau sharding state (bab Parameter Sharding ) |
| Gagal saat validasi, bukan pelatihan | lupa with torch.no_grad(): | bungkus evaluasi dengan no_grad |
| Memori terisi perlahan antar epoch | tensor hasil disimpan tanpa .detach() | lepas referensi dan pemulung memori |
Dengan peta komponen memori di tangan, pertanyaan berikutnya adalah taktis: bila aktivasi menjadi komponen yang paling cepat membengkak karena batch besar tidak muat, bagaimana kita tetap mendapatkan manfaat batch besar tanpa mengalokasikannya sekaligus? Chapter berikutnya menjawabnya lewat Gradient Accumulation — teknik menumpuk gradien dari beberapa micro-batch kecil hingga setara satu batch besar.
Rujukan resmi untuk pengelolaan memori CUDA dan mekanisme caching allocator PyTorch tersedia di docs.pytorch.org/docs/stable/notes/cuda.html .