Siklus hidup LLM pada chapter Pengenalan LLM menempatkan pretraining sebagai tahap yang menyerap hampir seluruh komputasi: model membaca triliunan token selama berbulan-bulan sebelum pernah menjawab satu instruksi pun. Mengapa tahap ini sebesar itu, dan apa yang sebenarnya dibeli oleh tambahan data serta parameter? Chapter ini menjawabnya dari dua arah: konsep skala (korpus, compute, scaling laws) dan eksperimen terukur — kita melatih model mainan yang identik pada tiga korpus berbeda ukuran dari bahasa sintetis yang sama, lalu membandingkan loss ujinya terhadap lantai teoretis yang bisa dihitung eksak.
Korpus Web Triliunan Token #
Korpus pretraining dibangun dari teks web skala besar — hasil rayapan (misalnya Common Crawl), kode sumber, buku, dan ensiklopedia — lalu dimurnikan sebelum dipakai:
Filter dan deduplikasi menentukan kualitas akhir: halaman salin-tempel yang tidak dibuang membuat model menghafal pengulangan, bukan belajar distribusi bahasa. Angka publiknya berkembang pesat — GPT-3 (2020) dilatih pada sekitar 300 miliar token, sedangkan Llama 3 (2024) dilaporkan memakai sekitar 15 triliunan token. Kedua angka berasal dari publikasi masing-masing: paper GPT-3 dan The Llama 3 Herd of Models .
Compute dan Scaling Laws #
Melatih model pada triliunan token berarti menjalankan forward dan backward untuk setiap token — pekerjaan yang hanya selesai waktunya di klaster ribuan GPU. Studi Chinchilla (Hoffmann dkk., 2022) mengukur hubungan ketiganya dan menemukan aturan praktis: untuk anggaran compute tetap, jumlah token pelatihan yang optimal sekitar 20 kali jumlah parameter.
| Parameter model | Token optimal (≈20/param) | Contoh kegiatan publik |
|---|---|---|
| 1 miliar | 20 miliar | — |
| 8 miliar | 160 miliar | Llama 3 8B: ≈15 triliunan (jauh di atas) |
| 70 miliar | 1,4 triliunan | Llama 2 70B: ≈2 triliunan (dekat) |
| 175 miliar | 3,5 triliunan | GPT-3 175B: ≈300 miliar (di bawah) |
Model terbaru sering dilatih jauh melewati titik Chinchilla: melatih model lebih kecil lebih lama menghasilkan model yang lebih murah dijalankan per inferensi, dan titik optimal bergeser ketika biaya inference diperhitungkan. Keputusan skala karenanya bukan sekadar “lebih besar lebih baik”, melainkan optimasi tiga variabel: parameter, token, dan anggaran compute.
Satu Objective, Ditopang Skala Data #
Objective pretraining tetap satu: next-token prediction — prediksi token berikutnya dengan cross-entropy, persis mekanisme chapter Next-Token Prediction . Skala data yang besar berperan seperti cakupan: bahasa memiliki fenomena berfrekuensi tinggi (tata bahasa dasar) hingga ekor panjang (istilah teknis, nama langka, gaya khusus). Korpus kecil hanya memperlihatkan fenomena umum; korpus masif memberi model cukup banyak contoh untuk setiap fenomena ekor panjang. Demo berikut mengisolasi efek itu pada skala yang bisa kita amati penuh.
Demo: Ukuran Korpus vs Loss Uji #
Kita bangun bahasa sintetis order-2 di atas delapan huruf: huruf berikutnya ditentukan oleh dua huruf terakhir melalui rantai Markov tetap. Karena rantainya kita definisikan sendiri, lantai loss yang tak terlewati — entropi rate-nya — dapat dihitung eksak dari distribusi stationer. Tiga korpus (200/800/3200 karakter) dibangkitkan dari bahasa yang sama, dan model yang sama (inisialisasi identik) dilatih pada masing-masing:
"""Pretraining mini: efek ukuran korpus terhadap loss language model.
Bahasa sintetis order-2: karakter berikutnya ditentukan oleh dua karakter
terakhir lewat rantai Markov dengan distribusi acak-tetap (terkonsentrasi
pada dua pilihan, dicampur 5% seragam agar semua konteks dapat tercapai).
Tiga korpus latih dibangkitkan dari bahasa yang sama dengan ukuran relatif
berbeda (200/800/3200 karakter); satu korpus uji tetap dipakai untuk
ketiganya. Model mainan identik (inisialisasi dari seed yang sama) dilatih
pada tiap korpus, lalu loss ujinya dibandingkan dengan lantai teoretis:
entropi rate rantai Markov, dihitung dari distribusi stationer atas 64
pasangan-konteks. Diuji di CPU dengan PyTorch 2.14, deterministik.
"""
import numpy as np
import torch
import torch.nn as nn
rng = np.random.default_rng(42)
ALF = list("abcdefgh")
V = len(ALF)
# ---------- 1. Bahasa sintetis order-2 ----------
# tiap pasangan konteks (a,b) punya distribusi karakter berikutnya sendiri:
# terkonsentrasi pada dua pilihan, dicampur 5% seragam agar rantai
# Markovnya irreducible (semua konteks bisa tercapai dari mana saja)
transisi = {}
for a in ALF:
for b in ALF:
pilihan = rng.choice(V, size=2, replace=False)
bobot = rng.dirichlet([4.0, 2.0]) # condong: entropi per konteks rendah
baris = np.zeros(V)
baris[pilihan] = 0.95 * bobot
baris += 0.05 / V
transisi[(a, b)] = baris
def bangkit(panjang, benih):
"""Bangkitkan teks dari rantai Markov order-2."""
r = np.random.default_rng(benih)
a, b = "a", r.choice(ALF)
teks = [a, b]
for _ in range(panjang - 2):
c = r.choice(ALF, p=transisi[(a, b)])
teks.append(c)
a, b = b, c
return "".join(teks)
# ---------- 2. Lantai teoretis: entropi rate rantai ----------
id_pas = {}
i = 0
for a in ALF:
for b in ALF:
id_pas[(a, b)] = i
i += 1
P = np.zeros((V * V, V * V)) # matriks transisi antar pasangan-konteks
for a in ALF:
for b in ALF:
for c in ALF:
P[id_pas[(a, b)], id_pas[(b, c)]] += transisi[(a, b)][ALF.index(c)]
pi = np.full(V * V, 1.0 / (V * V))
for _ in range(2000): # iterasi pangkat menuju distribusi stationer
pi = pi @ P
pi /= pi.sum()
entropi_konteks = np.array([
-(transisi[(a, b)] * np.log(transisi[(a, b)])).sum()
for a in ALF for b in ALF
])
lantai = float(pi @ entropi_konteks)
print(f"entropi rate bahasa sintetis (lantai loss) : {lantai:.3f} nats/karakter")
print()
# ---------- 3. Dataset: 3 korpus latih + 1 korpus uji tetap ----------
KORPUS_UJI = bangkit(800, benih=99)
UKURAN = [("kecil", 200, 7), ("sedang", 800, 17), ("besar", 3200, 27)]
def jendela(teks):
"""Pasangan (2 karakter konteks, karakter berikutnya) -> tensor."""
id_alf = {c: i for i, c in enumerate(ALF)}
X = np.zeros((len(teks) - 2, 2 * V), dtype=np.float32)
y = np.zeros(len(teks) - 2, dtype=np.int64)
for i in range(len(teks) - 2):
X[i, id_alf[teks[i]]] = 1.0
X[i, V + id_alf[teks[i + 1]]] = 1.0
y[i] = id_alf[teks[i + 2]]
return torch.tensor(X), torch.tensor(y)
X_uji, y_uji = jendela(KORPUS_UJI)
# ---------- 4. Model + pelatihan per korpus ----------
class LMMini(nn.Module):
"""Language model mainan: one-hot 2 karakter -> karakter berikutnya."""
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2 * V, 64), nn.ReLU(), nn.Linear(64, V)
)
def forward(self, x):
return self.net(x)
def latih(X_latih, y_latih, epoch=400, lr=0.05):
"""Latih model segar (init identik) dan kembalikan model + jejak loss."""
torch.manual_seed(0) # ketiga korpus memakai inisialisasi bobot sama
model = LMMini()
opt = torch.optim.Adam(model.parameters(), lr=lr)
loss_fn = nn.CrossEntropyLoss()
jejak = []
for ep in range(epoch):
loss = loss_fn(model(X_latih), y_latih)
opt.zero_grad()
loss.backward()
opt.step()
if ep % 100 == 0 or ep == epoch - 1:
jejak.append((ep, loss.item()))
return model, jejak
hasil = []
print("=== Pelatihan pada tiga korpus (model & epoch sama, data berbeda) ===")
for nama, ukuran, benih in UKURAN:
teks = bangkit(ukuran, benih=benih)
X_latih, y_latih = jendela(teks)
konteks_unik = len({(teks[i], teks[i + 1]) for i in range(len(teks) - 1)})
model, jejak = latih(X_latih, y_latih)
with torch.no_grad():
loss_latih = nn.CrossEntropyLoss()(model(X_latih), y_latih).item()
loss_uji = nn.CrossEntropyLoss()(model(X_uji), y_uji).item()
kurva = ", ".join(f"e{ep}={l:.3f}" for ep, l in jejak)
print(f"korpus {nama:6s} ({ukuran:4d} kar): {kurva}")
print(f" konteks 2-huruf terlihat: {konteks_unik}/{V * V}"
f" | loss latih akhir {loss_latih:.3f}"
f" | loss uji akhir {loss_uji:.3f}")
hasil.append((nama, ukuran, konteks_unik, loss_latih, loss_uji))
print()
print("=== Ringkasan: ukuran korpus vs loss ===")
print(f"{'korpus':8s} {'ukuran':>7s} {'konteks':>8s} {'latih':>7s}"
f" {'uji':>7s} {'jarak lantai':>13s}")
for nama, ukuran, konteks, latih_, uji_ in hasil:
print(f"{nama:8s} {ukuran:7d} {konteks:>4d}/{V * V} {latih_:7.3f}"
f" {uji_:7.3f} {uji_ - lantai:13.3f}")
print(f"lantai teoretis (entropi rate): {lantai:.3f} nats/karakter")Output yang Dihasilkan:
entropi rate bahasa sintetis (lantai loss) : 0.792 nats/karakter
=== Pelatihan pada tiga korpus (model & epoch sama, data berbeda) ===
korpus kecil ( 200 kar): e0=2.088, e100=0.428, e200=0.428, e300=0.429, e399=0.428
konteks 2-huruf terlihat: 50/64 | loss latih akhir 0.428 | loss uji akhir 2.967
korpus sedang ( 800 kar): e0=2.100, e100=0.651, e200=0.651, e300=0.650, e399=0.651
konteks 2-huruf terlihat: 63/64 | loss latih akhir 0.651 | loss uji akhir 1.303
korpus besar (3200 kar): e0=2.099, e100=0.758, e200=0.753, e300=0.753, e399=0.753
konteks 2-huruf terlihat: 64/64 | loss latih akhir 0.753 | loss uji akhir 0.849
=== Ringkasan: ukuran korpus vs loss ===
korpus ukuran konteks latih uji jarak lantai
kecil 200 50/64 0.428 2.967 2.175
sedang 800 63/64 0.651 1.303 0.511
besar 3200 64/64 0.753 0.849 0.057
lantai teoretis (entropi rate): 0.792 nats/karakterMari kita telaah kode di atas baris per baris:
- Baris 26-34: setiap pasangan konteks
(a, b)mendapat distribusi huruf berikutnya yang terkonsentrasi pada dua pilihan, dicampur 5% seragam agar semua konteks dapat tercapai dari mana saja (rantai irreducible — syarat agar satu entropi rate berlaku untuk semua teks yang dibangkitkan). - Baris 37-46:
bangkitmenulis teks karakter demi karakter mengikuti rantai; parameterbenihmembuat korpus bisa dibangkitkan ulang persis. - Baris 50-64: rantai atas pasangan-konteks dibentuk sebagai matriks transisi 64×64, lalu distribusi stationer
pidicari dengan iterasi pangkat — frekuensi jangka panjang tiap konteks muncul di teks. - Baris 65-69: lantai loss = rata-rata entropi tiap konteks dibobot frekuensi stationernya. Model sebaik apa pun tidak bisa turun di bawah angka ini karena bahasanya memang acak.
- Baris 78-87: fungsi
jendelamengubah teks menjadi pasangan (konteks dua huruf sebagai one-hot, huruf berikutnya sebagai target). - Baris 106-120: fungsi
latihmemanggiltorch.manual_seed(0)sebelum membuat model — ketiga korpus dilatih dari inisialisasi bobot yang identik, sehingga satu-satunya perbedaan adalah datanya. - Baris 123-146: untuk tiap korpus dicetak kurva loss, jumlah konteks dua-huruf yang benar-benar terlihat, loss latih, dan loss uji terhadap korpus uji tetap, lalu dirangkum dalam tabel.
Hasilnya membentuk pola yang persis diprediksi teori skala. Korpus kecil hanya memperlihatkan 50 dari 64 konteks; loss latihnya paling rendah (0,428 — model menghafal sampel yang sempit), tetapi loss ujinya jauh paling tinggi (2,967) karena separuh konteks tidak pernah dipelajarinya. Korpus sedang melihat 63/64 konteks dan loss ujinya turun ke 1,303. Korpus besar melihat seluruh konteks dan loss ujinya 0,849 — hanya 0,057 di atas lantai entropi 0,792. Perhatikan pula pola loss latih yang naik (0,428 → 0,651 → 0,753) sambil loss uji turun: inilah tanda overfitting yang menipis seiring cakupan data, miniatur dari alasan LLM nyata terus menambah korpus.
Biaya Compute dan Lingkungan #
Pretraining skala penuh menyerap listrik dan pendinginan klaster GPU dalam jumlah besar selama berbulan-bulan, dengan jejak karbon yang dilaporkan sebagian laboratorium dalam publikasi modelnya. Praktik yang membaik mencakup klaster berisi pasokan energi terbarukan, pelaporan konsumsi energi yang transparan, dan teknik efisiensi — dari mixed precision sampai pemanfaatan ulang panas klaster. Efisiensi training juga pokok bahasan teknis pada section Distributed dan Efficient Training .
Demo kita memperlihatkan apa yang dibeli skala data; mekanisme bagaimana model membelinya — logits, softmax, cross-entropy, dan loop autoregressive — adalah pokok bahasan chapter berikutnya.