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:
"""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:
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 : 4000000Mari kita telaah kode di atas baris per baris:
- Baris 8-10 (fungsi
info):element_size()mengembalikan byte per elemen dannumel()jumlah elemen — hasil kalinya dicetak dengan pemisah ribuan agar mudah dibaca. - Baris 13-17 (empat dtype): 1000 elemen menghabiskan 2.000–8.000 byte mengikuti dtype; mengganti
float64menjadifloat32langsung memangkas setengah, danfloat16seperempat — alat penghemat paling kasar yang kita miliki. - Baris 17 (batch gambar nyata): satu batch 64 gambar RGB 224×224 pada
float32menempati 38.535.168 byte atau sekitar 36,75 MB — dan itu baru satu salinan; gradien serta momentum optimizer menambah salinan lain per parameter. - Baris 21-24 (dua cara mengukur):
untyped_storage().nbytes()danelement_size() × numelsepakat pada 4.000.000 byte — keduanya mengukur blok data, bukan objek Python. - Baris 23 (jebakan
sys.getsizeof):sys.getsizeof(t)melaporkan hanya 72 byte — ukuran objek pembungkustorch.Tensor, bukan datanya. Objekstoragedi baris berikutnya jujur membawa 4.000.056 byte (4 juta data + 56 byte buku besar objek). Kesimpulannya: jangan memakaisys.getsizeofuntuk memperkirakan memori tensor. - Baris 26-27 (view berbagi storage):
t[:, 0]hanya berisi 1000 elemen, tetapi storage-nya masih blok 4 MB milikt— 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:
"""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:
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: TrueMari kita telaah kode di atas baris per baris:
- Baris 10-13 (bukti in-place):
A.data_ptr()menyimpan alamat blok data; setelahadd_, alamat itu tidak berubah — penjumlahan ditulis di tempat yang sama. SebaliknyaA.add(5.0)(tanpa garis bawah) mengalokasikan tensor baruBdi alamat berbeda, danAtetap tak tersentuh. - 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. - Baris 22-24 (perakitan kembali):
torch.catmenyusun potongan hasil menjadi(8, 2), dantorch.equalmembuktikan isinya identik dengan memproses sekaligus — chunk menghemat puncak memori tanpa mengubah hasil.
Kebiasaan Hemat Memori #
| Praktik | Efek | Catatan |
|---|---|---|
del tensor lalu torch.cuda.empty_cache() | referensi dan cache CUDA dilepas | empty_cache hanya relevan pada GPU |
with torch.no_grad(): saat inferensi | graph tak dibangun | paling besar dampaknya pada model besar |
.detach() untuk hasil yang disimpan | graph boleh diambil pemulung memori | pas untuk log prediksi tiap epoch |
Operasi in-place add_, mul_, zero_ | tanpa alokasi tensor baru | hindari pada tensor yang dibutuhkan autograd |
dtype lebih kecil (float16, bfloat16) | memori terpangkas, GPU lebih cepat | akurasi turun sedikit, uji dulu |
chunk / batch kecil | puncak memori terkendali | hasil tetap identik bila dirakit kembali |
Profil torch.cuda.memory_allocated() | byte teralokasi terpantau langsung | padanan 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.