Chapter Memory Management
menunjukkan bahwa mengganti float64 ke float32 langsung memangkas separuh memori — dan pemotongan itu bisa dilanjutkan: float16 dan bfloat16 hanya menempati 2 byte per elemen, mempercepat transfer data, dan pada GPU modern mengaktifkan tensor core yang paling cepat. Namun setengah presisi bukan diskon gratis: float16 kehilangan sebagian besar rentang nilainya, sehingga nilai besar melimpah menjadi inf dan nilai kecil membulat menjadi nol. Mixed precision training menyelesaikan konflik ini dengan pembagian tugas — bagian yang padat hitungan (matmul, konvolusi) berjalan di presisi rendah, akumulasi dan parameter tetap float32. Chapter penutup section ini membedah tiga dtype tersebut, menguji torch.autocast pada jalur CPU dengan bfloat16, lalu membedah mekanisme GradScaler yang menjaga float16 tetap aman di GPU.
Tiga Tingkat Presisi #
| dtype | bit | resolusi (perkiraan) | rentang maksimum | catatan |
|---|---|---|---|---|
float32 | 32 | 1e-06 | ±3,4 × 10³⁸ | standar training; 4 byte per elemen |
float16 | 16 | 1e-03 | ±6,55 × 10⁴ | 10 bit mantissa; rentang sempit |
bfloat16 | 16 | 1e-02 | ±3,4 × 10³⁸ | 7 bit mantissa; rentang selaras float32 |
Dua varian 16-bit membelah anggaran bit dengan cara berbeda. float16 membelanjakan bit untuk mantissa (presisi angka) sehingga rentangnya sempit: nilai di atas 65504 melimpah menjadi inf. bfloat16 membelanjakan bit untuk eksponen sehingga rentangnya sama luas dengan float32, tetapi resolusinya lebih kasar. Karena luapan-luapan inilah float16 di GPU memerlukan loss scaling, sementara bfloat16 umumnya tidak.
Dua Mode Gagal float16 #
Kedua batas tersebut bisa kita uji secara langsung. Berkas autocast_bf16.py membuka dengan tabel anatomi dtype dari torch.finfo, lalu memancing kedua mode gagal float16, menguji autocast, mengadu training mini dua presisi, dan menutup dengan siklus GradScaler:
"""Mixed precision di CPU: autocast bfloat16 dan mekanik GradScaler.
Bagian 1: anatomi tiga dtype lewat torch.finfo (presisi vs rentang).
Bagian 2: dua mode gagal float16 — luapan (inf) dan nilai kecil membulat
ke nol — dibandingkan dengan bfloat16 yang rentangnya selaras float32.
Bagian 3: forward pass di dalam torch.autocast("cpu", bfloat16) —
bobot tetap float32, keluaran berubah dtype, hasil sedikit bergeser.
Bagian 4: training mini float32 vs autocast bfloat16 pada masalah yang
sama dengan bobot awal identik — loss berbeda tipis, keduanya konvergen.
Bagian 5: siklus GradScaler scale -> unscale_ -> step -> update.
"""
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
torch.manual_seed(42)
# ---------- 1. Anatomi tiga dtype ----------
print(f"{'dtype':16s} {'bit':>4s} {'resolusi':>10s} {'min':>13s} {'maks':>13s}")
for dt in (torch.float32, torch.float16, torch.bfloat16):
info = torch.finfo(dt)
print(f"{str(dt):16s} {info.bits:4d} {info.resolution:>10.0e} "
f"{info.min:>13.3e} {info.max:>13.3e}")
# ---------- 2. Dua mode gagal float16 ----------
a = torch.tensor([300.0], dtype=torch.float16)
b = torch.tensor([300.0], dtype=torch.bfloat16)
print()
print("300.0 x 300.0 : float16 =", (a * 300.0).item(),
"| bfloat16 =", (b * 300.0).item())
kecil = torch.tensor([1e-8])
print("gradien 1e-8 : float16 =", f"{kecil.to(torch.float16).item():.2e}",
"| bfloat16 =", f"{kecil.to(torch.bfloat16).item():.2e}",
"| float32 =", f"{kecil.item():.2e}")
print("1e-8 x 65536 : float16 =",
f"{(kecil * 65536.0).to(torch.float16).item():.2e}")
# ---------- 3. autocast CPU: tipe berubah, hasil bergeser tipis ----------
model = nn.Linear(64, 32)
x = torch.randn(8, 64)
with torch.autocast("cpu", dtype=torch.bfloat16):
y_auto = model(x)
y_biasa = model(x)
print()
print("bobot model :", model.weight.dtype)
print("keluaran autocast:", y_auto.dtype)
print("keluaran biasa :", y_biasa.dtype)
print("selisih maksimum :", (y_auto.float() - y_biasa).abs().max().item())
# ---------- 4. Training mini: float32 vs autocast bfloat16 ----------
X, y = make_moons(n_samples=300, noise=0.25, random_state=42)
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.long)
def buat_model():
torch.manual_seed(123) # bobot awal identik untuk kedua percobaan
return nn.Sequential(nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 2))
def latih(pakai_autocast, judul):
model = buat_model()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
criterion = nn.CrossEntropyLoss()
riwayat = {}
for epoch in range(1, 31):
optimizer.zero_grad()
if pakai_autocast:
with torch.autocast("cpu", dtype=torch.bfloat16):
logits = model(X_t)
loss = criterion(logits, y_t)
else:
logits = model(X_t)
loss = criterion(logits, y_t)
loss.backward()
optimizer.step()
if epoch in (1, 10, 30):
riwayat[epoch] = loss.item()
with torch.no_grad():
akurasi = (model(X_t).argmax(dim=1) == y_t).float().mean().item()
print(f"{judul:26s} ep1 {riwayat[1]:7.4f} | ep10 {riwayat[10]:7.4f} | "
f"ep30 {riwayat[30]:7.4f} | akurasi {akurasi:.3f}")
return riwayat
print()
print("training: make_moons 300 | full-batch | SGD lr=0.5 | 30 epoch")
print("logits autocast keluar sebagai", end=" ")
model_coba = buat_model()
with torch.autocast("cpu", dtype=torch.bfloat16):
print(model_coba(X_t).dtype)
r32 = latih(False, "float32 penuh")
rbf = latih(True, "autocast bfloat16")
print("loss ep30 berbeda?", abs(r32[30] - rbf[30]) > 0,
f"(selisih {abs(r32[30] - rbf[30]):.4f})")
# ---------- 5. Siklus GradScaler (pola float16 di GPU) ----------
print()
scaler = torch.amp.GradScaler("cpu")
w = torch.nn.Parameter(torch.tensor([0.001]))
optimizer = torch.optim.SGD([w], lr=0.1)
loss = (w * 1e-8).sum() # gradien analitis = 1e-8
scaler.scale(loss).backward() # loss diskalakan lalu backward
print("gradien terskala :", f"{w.grad.item():.2e}")
scaler.unscale_(optimizer) # kembalikan skala gradien
print("gradien unscale_ :", f"{w.grad.item():.2e}")
scaler.step(optimizer) # update hanya bila aman
scaler.update() # sesuaikan skala utk langkah berikutnya
print("w setelah step :", f"{w.item():.12f}",
"(0.001 - 0.1 x 1e-8)")
print("skala berikutnya :", scaler.get_scale())Output yang Dihasilkan:
dtype bit resolusi min maks
torch.float32 32 1e-06 -3.403e+38 3.403e+38
torch.float16 16 1e-03 -6.550e+04 6.550e+04
torch.bfloat16 16 1e-02 -3.390e+38 3.390e+38
300.0 x 300.0 : float16 = inf | bfloat16 = 90112.0
gradien 1e-8 : float16 = 0.00e+00 | bfloat16 = 1.00e-08 | float32 = 1.00e-08
1e-8 x 65536 : float16 = 6.55e-04
bobot model : torch.float32
keluaran autocast: torch.bfloat16
keluaran biasa : torch.float32
selisih maksimum : 0.004613995552062988
training: make_moons 300 | full-batch | SGD lr=0.5 | 30 epoch
logits autocast keluar sebagai torch.bfloat16
float32 penuh ep1 0.5467 | ep10 0.3238 | ep30 0.3089 | akurasi 0.863
autocast bfloat16 ep1 0.5465 | ep10 0.3237 | ep30 0.3087 | akurasi 0.863
loss ep30 berbeda? True (selisih 0.0001)
gradien terskala : 6.55e-04
gradien unscale_ : 1.00e-08
w setelah step : 0.000999999000 (0.001 - 0.1 x 1e-8)
skala berikutnya : 65536.0Mari kita telaah kode di atas baris per baris:
- Baris 21-24 (tabel
finfo):torch.finfomelaporkan anatomi tiap dtype — resolusi (jarak antar nilai yang masih bisa dibedakan) dan rentang min-maks; tabel inilah sumber angka pada tabel perbandingan di atas. - Baris 27-31 (mode gagal pertama):
300 × 300 = 90.000melampaui batasfloat16(65.504) sehingga hasilnyainf;bfloat16menampungnya sebagai 90112 — pembulatan kasar, tetapi jauh dari luapan. - Baris 33-38 (mode gagal kedua): gradien
1e-8membulat menjadi0.00e+00difloat16— gradien seperti ini hilang sebelum sempat memperbarui bobot. Namun1e-8 × 65536 = 6,55 × 10⁻⁴bertahan utuh: inilah inti loss scaling. - Baris 41-50 (autocast): di dalam
torch.autocast("cpu", dtype=torch.bfloat16), operasi Linear berjalan dibfloat16— keluarannya ber-dtypetorch.bfloat16— padahal bobot model tetapfloat32: autocast mengonversi masukan secara otomatis di perbatasan operasi, bukan mengubah model. Hasilnya bergeser tipis (selisih maksimum 0,0046) dibanding hitunganfloat32penuh. - Baris 63-80 (dua jalur training): fungsi
latihmenjalankan siklus standar; bedanya hanya forward pass dan loss berada di dalam konteks autocast, sementaraloss.backward()danoptimizer.step()tetap di luar — gradien dihitung dan disimpan dalam presisi penuh. - Baris 94-97 (pembacaan hasil): kedua jalur konvergen hampir berdampingan (loss akhir 0,3089 vs 0,3087; akurasi sama 0,863) — selisih 0,0001 adalah harga pembulatan
bfloat16, bukan indikasi kegagalan. - Baris 101-113 (siklus GradScaler):
scale()mengalikan loss dengan 65536 sebelumbackward()sehingga gradien terskala jadi 6,55 × 10⁻⁴;unscale_()membaginya kembali menjadi 1,00 × 10⁻⁸ sebelumstep();update()menyiapkan skala untuk langkah berikutnya — dan menurunkannya bila sebelumnya terdeteksiinf/nansehinggastep()dilewati demi keamanan.
Jalur CPU, Jalur GPU #
Semua pengujian pada chapter ini berjalan di CPU dengan bfloat16 — satu-satunya dtype setengah-presisi yang stabil di jalur CPU dan tidak memerlukan GradScaler karena rentangnya selaras float32. Pola lengkapnya ringkas: bungkus forward pass dengan torch.autocast("cpu", dtype=torch.bfloat16), biarkan backward() dan optimizer.step() di luar konteks. Di GPU, cerita berbeda dan lebih kaya: GPU Nvidia yang mendukung tensor core memperoleh percepatan besar dari float16 atau bfloat16, dan pola float16 di GPU lazim memasangkan torch.autocast("cuda", dtype=torch.float16) dengan torch.amp.GradScaler("cuda") agar gradien kecil tidak hilang dan luapan terdeteksi. Angka kecepatan GPU tidak dapat kami klaim dari mesin uji CPU ini — perbandingan kinerja sejatinya harus diukur di perangkat GPU Anda sendiri, dan hasil CPU (yang kadang justru tidak lebih cepat karena tidak semua operasi CPU dioptimalkan untuk bfloat16) tidak mewakili hasil GPU.
Rujukan resmi untuk keseluruhan pola ini — pilihan dtype per operasi, daftar operasi yang otomatis dikonversi, dan kerja sama autocast dengan GradScaler — terdokumentasi di docs.pytorch.org/docs/stable/notes/amp.html . Dengan mixed precision, rantai optimasi section ini tuntas: optimizer menentukan arah, scheduler mengatur langkah, clipping menjaga keselamatan, inisialisasi dan normalisasi menjaga kesehatan sinyal, dan presisi campuran memeras sumber daya lebih hemat. Namun model yang terlatih baik belum tentu model yang generalisasi baik ke data baru — pertanyaan itulah yang membuka section berikutnya: Regularisasi dan Generalisasi .