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:

KomponenRumus byte1 juta parameterKapan dialokasikan
Parameter4 × P4 MBsaat model dibangun
Gradien4 × P4 MBsaat backward() pertama
State Adam (exp_avg, exp_avg_sq)8 × P8 MBsaat optimizer.step() pertama
State SGD + momentum4 × P4 MBsaat optimizer.step() pertama
Aktivasibatch × lebar layer × 4bervariasisaat 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:

python
"""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:

text
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,403x

Mari kita telaah kode di atas baris per baris:

  1. Baris 25-37 (tabel rumus): loop mencetak empat komponen dasar dengan rumus 4*P masing-masing; angka “per 1 juta parameter” dihitung langsung dari 4 * 1_000_000 sehingga 4 MB per komponen — bukan angka tebakan.
  2. 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.
  3. Baris 53-63 (model mini): model tiga layer Linear memiliki 1.075.208 parameter; p.numel() * p.element_size() menjumlahkan byte data seluruh parameter sungguhan dan hasilnya persis 4 * P.
  4. Baris 65-70 (gradien nyata): setelah loss.backward(), setiap parameter memiliki atribut p.grad berukuran sama dengan parameternya — terbukti 4.300.832 byte, identik dengan memori parameter.
  5. Baris 72-89 (state Adam nyata): setelah opt.step(), kamus opt.state berisi tensor exp_avg dan exp_avg_sq seukuran parameter (total 8.601.664 byte = 8*P) plus enam tensor skalar penghitung step sebesar 24 byte. Rumus 16*P terbukti cocok pada tensor sungguhan.
  6. Baris 91-95 (aktivasi dan jebakan): aktivasi untuk MLP ini dihitung dari input tiga layer Linear (batch 64) — sekitar 0,5 MB. Adapun sys.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:

text
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:

GejalaPenyebab paling umumTindakan pertama
Gagal saat batch tertentu sajaaktivasi tumbuh mengikuti batchturunkan batch, naikkan epoch (lihat Batch Size dan Epoch )
Gagal pada iterasi pertama setelah optimizer aktifstate Adam 8P baru dialokasikanganti ke SGD+momentum, atau sharding state (bab Parameter Sharding )
Gagal saat validasi, bukan pelatihanlupa with torch.no_grad():bungkus evaluasi dengan no_grad
Memori terisi perlahan antar epochtensor 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 .