Galat paling menyakitkan di deep learning bukan salah rumus, melainkan out of memory: pelatihan berjalan baik-baik saja sampai batch yang sedikit lebih besar masuk dan program berhenti. Kuncinya, memori tensor bisa dihitung sebelum dialokasikan — kalikan ukuran satu elemen dengan jumlah elemen — sehingga keputusan seperti “apakah batch 64 gambar 224×224 muat?” menjadi aritmetika sederhana, bukan tebak-tebakan. Chapter penutup section ini menghitung memori berbagai tensor secara nyata, membedakan operasi in-place dari operasi yang mengalokasikan tensor baru, lalu merangkum kebiasaan hemat memori — termasuk alat profil milik CUDA yang akan dipakai begitu kita pindah ke GPU.

Menghitung Memori Sebuah Tensor #

Rumus dasarnya satu baris: byte = t.element_size() × t.numel(). Ukuran elemen ditentukan dtype: float16/bfloat16 menempati 2 byte, float32 dan int32 menempati 4 byte, float64 dan int64 menempati 8 byte. Berkas ukuran_memori.py menghitung semuanya sekaligus membedah jebakan pengukuran sys.getsizeof:

python
"""Menghitung memori tensor: element_size x numel dan objek pembungkusnya."""

import sys

import torch


def info(nama, t):
    data = t.element_size() * t.numel()
    print(f"{nama:28s} {str(tuple(t.shape)):15s} {str(t.dtype):15s} {data:>11,} byte")


info("int64   (1000,)", torch.zeros(1000, dtype=torch.int64))
info("float16 (1000,)", torch.zeros(1000, dtype=torch.float16))
info("float32 (1000,)", torch.zeros(1000, dtype=torch.float32))
info("float64 (1000,)", torch.zeros(1000, dtype=torch.float64))
info("batch float32 (64,3,224,224)", torch.zeros(64, 3, 224, 224))

t = torch.zeros(1000, 1000)
print()
print("t.untyped_storage().nbytes() :", t.untyped_storage().nbytes())
print("t.element_size()*t.numel()   :", t.element_size() * t.numel())
print("sys.getsizeof(t)             :", sys.getsizeof(t))
print("sys.getsizeof(storage)       :", sys.getsizeof(t.untyped_storage()))

potongan = t[:, 0]
print("potongan t[:, 0] storage     :", potongan.untyped_storage().nbytes())

Output yang Dihasilkan:

text
int64   (1000,)              (1000,)         torch.int64           8,000 byte
float16 (1000,)              (1000,)         torch.float16         2,000 byte
float32 (1000,)              (1000,)         torch.float32         4,000 byte
float64 (1000,)              (1000,)         torch.float64         8,000 byte
batch float32 (64,3,224,224) (64, 3, 224, 224) torch.float32    38,535,168 byte

t.untyped_storage().nbytes() : 4000000
t.element_size()*t.numel()   : 4000000
sys.getsizeof(t)             : 72
sys.getsizeof(storage)       : 4000056
potongan t[:, 0] storage     : 4000000

Mari kita telaah kode di atas baris per baris:

  1. Baris 8-10 (fungsi info): element_size() mengembalikan byte per elemen dan numel() jumlah elemen — hasil kalinya dicetak dengan pemisah ribuan agar mudah dibaca.
  2. Baris 13-17 (empat dtype): 1000 elemen menghabiskan 2.000–8.000 byte mengikuti dtype; mengganti float64 menjadi float32 langsung memangkas setengah, dan float16 seperempat — alat penghemat paling kasar yang kita miliki.
  3. Baris 17 (batch gambar nyata): satu batch 64 gambar RGB 224×224 pada float32 menempati 38.535.168 byte atau sekitar 36,75 MB — dan itu baru satu salinan; gradien serta momentum optimizer menambah salinan lain per parameter.
  4. Baris 21-24 (dua cara mengukur): untyped_storage().nbytes() dan element_size() × numel sepakat pada 4.000.000 byte — keduanya mengukur blok data, bukan objek Python.
  5. Baris 23 (jebakan sys.getsizeof): sys.getsizeof(t) melaporkan hanya 72 byte — ukuran objek pembungkus torch.Tensor, bukan datanya. Objek storage di baris berikutnya jujur membawa 4.000.056 byte (4 juta data + 56 byte buku besar objek). Kesimpulannya: jangan memakai sys.getsizeof untuk memperkirakan memori tensor.
  6. Baris 26-27 (view berbagi storage): t[:, 0] hanya berisi 1000 elemen, tetapi storage-nya masih blok 4 MB milik t — view tidak menghemat memori bila induknya masih hidup, dan memori baru benar-benar bebas setelah seluruh tensor yang menunjuk blok itu dihapus.

Operasi In-place dan Pemecahan Batch #

Dua teknik penghematan berikutnya bekerja pada dua sisi berbeda. Operasi in-place (add_, mul_, penanda garis bawah di akhir nama) menulis di blok memori lama sehingga tidak ada alokasi baru; sementara chunk memecah batch besar menjadi beberapa potongan kecil supaya puncak konsumsi memori terkendali. Berkas inplace_dan_chunk.py menguji keduanya:

python
"""Operasi in-place vs tensor baru serta pemrosesan batch per chunk."""

import torch

torch.manual_seed(42)

A = torch.ones(3, 3)
alamat = A.data_ptr()

A.add_(5.0)
print("A.add_(5.0)   -> storage sama  :", A.data_ptr() == alamat)
B = A.add(5.0)
print("B = A.add(5.0) -> storage beda :", B.data_ptr() != alamat)

print()
batch = torch.randn(8, 2)
print("batch shape:", tuple(batch.shape))
hasil = []
for i, potongan in enumerate(batch.chunk(3, dim=0)):
    print(f"chunk {i}: shape {tuple(potongan.shape)}")
    hasil.append(potongan * 2.0)
gabungan = torch.cat(hasil, dim=0)
print("gabungan shape:", tuple(gabungan.shape),
      "| identik dengan batch * 2:", torch.equal(gabungan, batch * 2.0))

Output yang Dihasilkan:

text
A.add_(5.0)   -> storage sama  : True
B = A.add(5.0) -> storage beda : True

batch shape: (8, 2)
chunk 0: shape (3, 2)
chunk 1: shape (3, 2)
chunk 2: shape (2, 2)
gabungan shape: (8, 2) | identik dengan batch * 2: True

Mari kita telaah kode di atas baris per baris:

  1. Baris 10-13 (bukti in-place): A.data_ptr() menyimpan alamat blok data; setelah add_, alamat itu tidak berubah — penjumlahan ditulis di tempat yang sama. Sebaliknya A.add(5.0) (tanpa garis bawah) mengalokasikan tensor baru B di alamat berbeda, dan A tetap tak tersentuh.
  2. Baris 19-21 (chunk): batch.chunk(3, dim=0) memecah 8 baris menjadi tiga potongan (3, 3, 2) — setiap potongan diproses bergantian sehingga pada satu saat hanya satu potongan beserta hasilnya yang hidup di memori.
  3. Baris 22-24 (perakitan kembali): torch.cat menyusun potongan hasil menjadi (8, 2), dan torch.equal membuktikan isinya identik dengan memproses sekaligus — chunk menghemat puncak memori tanpa mengubah hasil.

Kebiasaan Hemat Memori #

PraktikEfekCatatan
del tensor lalu torch.cuda.empty_cache()referensi dan cache CUDA dilepasempty_cache hanya relevan pada GPU
with torch.no_grad(): saat inferensigraph tak dibangunpaling besar dampaknya pada model besar
.detach() untuk hasil yang disimpangraph boleh diambil pemulung memoripas untuk log prediksi tiap epoch
Operasi in-place add_, mul_, zero_tanpa alokasi tensor baruhindari pada tensor yang dibutuhkan autograd
dtype lebih kecil (float16, bfloat16)memori terpangkas, GPU lebih cepatakurasi turun sedikit, uji dulu
chunk / batch kecilpuncak memori terkendalihasil tetap identik bila dirakit kembali
Profil torch.cuda.memory_allocated()byte teralokasi terpantau langsungpadanan uji CPU kita: element_size() × numel

Referensi pengelolaan memori CUDA PyTorch — termasuk mekanisme caching allocator dan pemakaian memory_allocated — tersedia di docs.pytorch.org/docs/stable/notes/cuda.html . Section ini melengkapi bekal dasar kita: tensor dan shape, indexing, operasi, broadcasting, autograd, pemilihan perangkat, hingga memori. Semuanya akan langsung terpakai pada section berikutnya saat kita menyusun neural network sungguhan dengan modul torch.nn di PyTorch.