Kita kini memegang sembilan teknik yang masing-masing menyerang satu sisi konsumsi memori: dari aritmetika 16 × P state optimizer, akumulasi gradien, hitung ulang aktivasi, hingga pembagian data, model, pipa, parameter, dan penulisan ulang attention. Persoalan praktisnya tinggal satu: dari semua pilihan itu, mulai dari mana, dan bagaimana kita tahu suatu langkah benar-benar berdampak tanpa menebak? Jawabannya adalah siklus ukur–ubah–ukur kembali: pantau memori lebih dulu, terapkan satu teknik pada satu waktu, lalu bandingkan angkanya. Chapter penutup ini merangkai kotak perkakas menjadi daftar periksa terurut, membedah alat pemantauan CPU dan GPU, lalu menutup dengan pengukuran nyata dua konfigurasi pelatihan yang menggabungkan tiga teknik sekaligus.

Alat Pemantauan yang Tepat untuk Objek yang Tepat #

Di GPU, PyTorch menyediakan torch.cuda.memory_allocated() dan torch.cuda.max_memory_allocated() — byte teralokasi saat itu dan puncaknya sejak penanda reset_peak_memory_stats(). Di CPU tidak ada padanan langsung, dan jebakan pertamanya adalah salah alat: tracemalloc milik Python hanya melihat alokasi lewat pengalokas Python, sedangkan tensor torch dialokasikan di luar jalur itu. Konsekuensya kami uji nyata pada berkas di bawah. Untuk tensor CPU, pemindaian gc.get_objects() plus element_size() × numel() menjadi padanan yang jujur — pola yang sudah dipakai di chapter Activation Checkpointing dan Memory Management .

python
"""Toolkit optimasi memori: pemantauan nyata dan dua konfigurasi pelatihan."""

import gc
import tracemalloc
import warnings
from contextlib import nullcontext

import torch
import torch.utils.checkpoint as cp

warnings.filterwarnings("ignore")
torch.manual_seed(3)


def memori_tensor_hidup():
    """Total byte seluruh tensor torch yang masih direferensikan Python."""
    total = 0
    for obj in gc.get_objects():
        if isinstance(obj, torch.Tensor):
            total += obj.element_size() * obj.numel()
    return total


# ---------- 1. Pemantauan: tracemalloc vs tensor torch ----------
print("=== Pemantauan memori: tracemalloc (alat Python) ===")
tracemalloc.start()
daftar = [0.0] * 1_000_000  # list Python ~8 MB
_, puncak_list = tracemalloc.get_traced_memory()
t = torch.zeros(2_000_000)  # tensor 8 MB di luar alokator Python
_, puncak_tensor = tracemalloc.get_traced_memory()
print(f"list 1 juta float : tracemalloc melihat {puncak_list:,} byte")
print(f"tensor 8 MB       : tracemalloc hanya melihat {puncak_tensor - puncak_list:,} byte")
print(f"tensor sesungguhnya: {t.numel() * t.element_size():,} byte (tak terlacak)")
tracemalloc.stop()
del daftar, t
gc.collect()  # lepas referensi sebelum mengukur pelatihan
print("padanan CPU untuk tensor: element_size() * numel() lewat pemindaian gc")


# ---------- 2. Dua konfigurasi pelatihan diukur nyata ----------
class Blok(torch.nn.Module):
    def __init__(self, d):
        super().__init__()
        self.fc1 = torch.nn.Linear(d, d)
        self.fc2 = torch.nn.Linear(d, d)

    def forward(self, x):
        return x + self.fc2(torch.nn.functional.gelu(self.fc1(x)))


def bangun(n_blok=10, d=256):
    torch.manual_seed(3)
    return torch.nn.ModuleList(Blok(d) for _ in range(n_blok))


N = 128
X = torch.randn(N, 256)
T = torch.randn(N, 256)


def latih_dan_ukur(autocast, checkpoint, micro, n_micro, n_epoch=2):
    blok = bangun()
    opt = torch.optim.SGD(blok.parameters(), lr=0.01)
    dasar = memori_tensor_hidup()
    puncak = dasar
    for _ in range(n_epoch):
        for awal in range(0, N, micro * n_micro):  # batch efektif 32
            opt.zero_grad()
            for langkah in range(n_micro):
                idx = slice(awal + langkah * micro, awal + (langkah + 1) * micro)
                konteks = (
                    torch.autocast("cpu", dtype=torch.bfloat16)
                    if autocast
                    else nullcontext()
                )
                with konteks:
                    h = X[idx]
                    for b in blok:
                        h = (
                            cp.checkpoint(b, h, use_reentrant=False)
                            if checkpoint
                            else b(h)
                        )
                puncak = max(puncak, memori_tensor_hidup())
                loss = ((h.float() - T[idx]) ** 2).mean() / n_micro
                loss.backward()
            opt.step()
    with torch.no_grad():
        h = X
        for b in blok:
            h = b(h)
        mse = float(((h - T) ** 2).mean())
    return dasar, puncak, mse


print()
print("=== Dua konfigurasi (batch efektif 32, 10 blok, 2 epoch) ===")
d1, p1, mse1 = latih_dan_ukur(autocast=False, checkpoint=False, micro=32, n_micro=1)
print(f"baseline fp32 batch 32      : dasar {d1:,} | puncak {p1:,} "
      f"| aktivasi {p1 - d1:,} byte | MSE {mse1:.4f}")

d2, p2, mse2 = latih_dan_ukur(autocast=True, checkpoint=True, micro=8, n_micro=4)
print(f"hemat bf16+ckpt+akum 4x8    : dasar {d2:,} | puncak {p2:,} "
      f"| aktivasi {p2 - d2:,} byte | MSE {mse2:.4f}")

print()
print(f"aktivasi tertahan turun     : {(p1 - d1) / (p2 - d2):.1f}x lebih kecil")
print(f"MSE kedua konfigurasi       : {mse1:.4f} vs {mse2:.4f} (kualitas terjaga)")
print(f"gradien akumulasi disimpan di sini; state SGD hanya 8P tanpa momentum")

Output yang Dihasilkan:

text
=== Pemantauan memori: tracemalloc (alat Python) ===
list 1 juta float : tracemalloc melihat 8,000,064 byte
tensor 8 MB       : tracemalloc hanya melihat 128 byte
tensor sesungguhnya: 8,000,000 byte (tak terlacak)
padanan CPU untuk tensor: element_size() * numel() lewat pemindaian gc

=== Dua konfigurasi (batch efektif 32, 10 blok, 2 epoch) ===
baseline fp32 batch 32      : dasar 5,525,504 | puncak 6,541,316 | aktivasi 1,015,812 byte | MSE 2.4730
hemat bf16+ckpt+akum 4x8    : dasar 5,525,504 | puncak 5,666,180 | aktivasi 140,676 byte | MSE 2.4730

aktivasi tertahan turun     : 7.2x lebih kecil
MSE kedua konfigurasi       : 2.4730 vs 2.4730 (kualitas terjaga)
gradien akumulasi disimpan di sini; state SGD hanya 8P tanpa momentum

Mari kita telaah kode di atas baris per baris:

  1. Baris 24-37 (jebakan tracemalloc): list Python 1 juta float terlihat penuh oleh tracemalloc (8.000.064 byte), sedangkan tensor torch.zeros(2_000_000) yang juga berukuran 8 MB hanya menambah 128 byte jejak — alokasi tensor torch tidak lewat pengalokas Python. Kesimpulan praktisnya: di CPU, ukur tensor dengan element_size() × numel(), bukan tracemalloc; di GPU, cukup torch.cuda.memory_allocated().
  2. Baris 41-49 (Blok): blok residual yang sama dengan chapter checkpointing — dua layer Linear dan satu GELU per blok — sehingga angka pengukurannya dapat dibandingkan langsung dengan bab tersebut.
  3. Baris 61-84 (satu fungsi, dua konfigurasi): parameter autocast, checkpoint, micro, n_micro mengubah jalur tanpa mengubah struktur. Konfigurasi hemat menggabungkan tiga teknik: torch.autocast("cpu", dtype=torch.bfloat16) (aktivasi berpindah ke 2 byte per elemen), cp.checkpoint per blok (aktivasi di dalam blok dibuang), dan akumulasi 4×8 (aktivasi per forward hanya untuk 8 sampel).
  4. Baris 84 (titik ukur puncak): memori_tensor_hidup() dipanggil setelah setiap forward micro-batch dan nilainya diambil maksimumnya — puncak yang terukur mencakup aktivasi tertahan, gradien yang sedang terkumpul, dan parameter; puncak singkat selama backward recompute tidak terpindai penuh, sehingga angka ini batas bawah yang konsisten, bukan batas atas mutlak.
  5. Baris 96-109 (hasil): aktivasi tertahan turun dari 1.015.812 byte menjadi 140.676 byte — 7,2x lebih hemat — dengan MSE akhir 2,4730 di kedua konfigurasi: kombinasi tiga teknik memangkas memori tanpa merusak hasil pelatihan pada contoh ini.

Urutan Praktis dan Daftar Periksa #

Urutan cobaTeknikYang dipangkasBiayaKapan berhenti
1mixed precision (bfloat16/float16)separuh aktivasigalat pembulatan kecilbila masih jebol
2gradient accumulationaktivasi sebanding micro-batchwaktu sedikit naikbila masih jebol
3activation checkpointingmayoritas aktivasiforward 2xbila masih jebol
4parameter sharding (FSDP/ZeRO)state optimizer & parameterkomunikasi naik tajambila masih jebol
5efficient attentionmemori skor n²butuh perangkat didukungkonteks panjang

Urutan itu mengikuti dua prinsip: teknologi termurah diulang lebih dulu (biaya waktu dan kompleksitas naik ke bawah tabel), dan setiap langkah diukur ulang sebelum melangkah. Dua kebiasaan penutup melengkapi daftar: lepas tensor besar dengan del lalu gc.collect() begitu tidak dipakai — dan di GPU tambahkan torch.cuda.empty_cache() hanya di titik istirahat (batch eval, akhir epoch), karena pemanggilan berlebihan justru memaksa alokasi ulang dan memperlambat pelatihan; perilaku caching allocator-nya dibahas di docs.pytorch.org/docs/stable/notes/cuda.html .

Section ini menutup perjalanan teknik pelatihan bersumber daya terbatas. Dengan memori terkendali, langkah berikutnya adalah membawa model terlatih keluar dari notebook: mengemasnya, menjalankannya di perangkat target, dan menjaga kualitasnya di dunia produksi — pokok bahasan section Deployment dan Inference .