Setiap bab sejauh ini berakhir di tensor logits: vektor skor mentah untuk tiap token kandidat. Menjalankan LLM berarti memutuskan, berulang kali, token mana yang diambil dari vektor itu — dan keputusan tersebut menentukan karakter keluaran jauh melampaui kualitas model itu sendiri. Distribusi yang sama bisa menghasilkan jawaban kaku yang selalu identik atau paragraf yang bervariasi antar permintaan, hanya karena strategi decoding-nya berbeda. Bab ini membedah lima strategi utama, menguji semuanya pada distribusi contoh dengan angka yang bisa dihitung ulang, lalu membahas KV cache — mekanisme yang menjaga generasi ribuan token tetap cepat.

Temperature: Melengkungkan Distribusi #

Model mengeluarkan logits z; probabilitas diperoleh lewat softmax yang dimodifikasi satu parameter:

text
p(token_i) = exp(z_i / T) / Σ_j exp(z_j / T)

T < 1 : distribusi mencuat  — token kuat makin dominan
T = 1 : distribusi asli model
T > 1 : distribusi merata   — token lemah ikut berpeluang

Temperature mengubah skala logits sebelum softmax, sehingga selisih skor antar token ikut meregang atau menyempit. Ukuran dampaknya terbaca langsung dari entropi distribusi: semakin rendah entropi, semakin mudah hasil diprediksi. Sisi praktisnya: T → 0 secara limit menjadi greedy decoding, dan pemilihan T adalah knob rasa pertama yang dijumpai pada setiap penyedia API.

Top-k dan Top-p: Memangkas Ekor #

Distribusi bahasa bermasalah pada ekornya: ratusan token berpeluang mikro membangun “derau” yang sesekali terpilih dan merusak koherensi. Dua filter memotongnya:

  • Top-k menyisakan k token berprobabilitas tertinggi, sisanya dinol-kan, lalu probabilitas yang tersisa dinormalisasi ulang. Parameternya tetap (mis. k=2) terlepas bentuk distribusinya.
  • Top-p (nucleus) menyisakan set kandidat terkecil yang jumlah probabilitasnya mencapai p (mis. 0,9). Set ini mengecil pada distribusi tajam dan melebar pada distribusi datar — adaptif terhadap keyakinan model.

Keduanya kerap dipadukan dengan temperature: distribusi dilengkungkan dulu, ekornya dipangkas kemudian, barulah sampling dilakukan.

Demo: Lima Strategi pada Angka Nyata #

Berkas decoding_demo.py memakai satu vektor logits atas delapan token untuk menguji temperature, top-k, dan top-p, kemudian model bigram lima token untuk mengadu greedy, beam search, dan pencarian exhaustive:

python
"""Strategi decoding: temperature, top-k, top-p, beam search, KV cache.

Bagian 1: satu vektor logits dibagi temperature 0.1 / 1.0 / 2.0 —
          distribusi dan entropinya dibandingkan.
Bagian 2: filter top-k = 2 menyisakan dua kandidat terkuat.
Bagian 3: filter top-p = 0.9 (nucleus) menyisakan set kandidat minimal.
Bagian 4: sampling 12 token dari tiap temperature (RNG berbenih).
Bagian 5: greedy vs beam search width 2 vs pencarian exhaustive pada
          model bigram mainan 5 token.
Bagian 6: estimasi memori KV cache model 32 lapis untuk 2048 token.
"""

import itertools

import numpy as np

rng = np.random.default_rng(42)

TOKEN = ["ikan", "kopi", "teh", "roti", "susu", "gula", "es", "air"]
LOGITS = np.array([2.0, 1.2, 0.5, 0.1, -0.3, -1.0, -1.5, -2.0])


def softmax_dengan_T(z, T):
    z = z / T
    e = np.exp(z - z.max())
    return e / e.sum()


def entropi(p):
    q = p[p > 0]
    return float(-(q * np.log(q)).sum())


# ---------- 1. Temperature ----------
print("== 1. temperature pada logits yang sama ==")
for T in (0.1, 1.0, 2.0):
    p = softmax_dengan_T(LOGITS, T)
    baris = " ".join(f"{v:.3f}" for v in p[:4])
    print(f"  T={T:3.1f}: p[:4] = [{baris}]  entropi = {entropi(p):.4f}")

# ---------- 2. Top-k ----------
p1 = softmax_dengan_T(LOGITS, 1.0)
k = 2
idx_topk = np.argsort(p1)[::-1][:k]
mask = np.zeros_like(p1, dtype=bool)
mask[idx_topk] = True
p_topk = np.where(mask, p1, 0.0)
p_topk = p_topk / p_topk.sum()
print()
print("== 2. top-k = 2 (renormalisasi) ==")
for i in idx_topk:
    print(f"  {TOKEN[i]:5s}: {p1[i]:.3f} -> {p_topk[i]:.3f}")

# ---------- 3. Top-p ----------
p_des = p1[np.argsort(p1)[::-1]]
kum = np.cumsum(p_des)
n_ambil = int(np.searchsorted(kum, 0.9)) + 1
urut = np.argsort(p1)[::-1][:n_ambil]
print()
print("== 3. top-p = 0.9 (nucleus) ==")
print("  kumulatif:", " ".join(f"{v:.3f}" for v in kum[:n_ambil]))
for i in urut:
    print(f"  {TOKEN[i]:5s}: {p1[i]:.3f}")
print(f"  kandidat tersisa: {n_ambil} dari {len(TOKEN)}")

# ---------- 4. Sampling seeded ----------
print()
print("== 4. sampling 12 token (seed 42) ==")
for T in (0.1, 1.0, 2.0):
    p = softmax_dengan_T(LOGITS, T)
    sampel = rng.choice(len(TOKEN), size=12, p=p)
    teks = " ".join(TOKEN[i] for i in sampel)
    print(f"  T={T:3.1f}: {teks}")

# ---------- 5. Greedy vs beam search vs exhaustive ----------
P = rng.random((5, 5))
P = P / P.sum(axis=1, keepdims=True)          # model bigram: P[next | kini]


def logp_sekuens(seq):
    return sum(np.log(P[seq[i], seq[i + 1]]) for i in range(len(seq) - 1))


greedy = [0]
for _ in range(4):
    greedy.append(int(np.argmax(P[greedy[-1]])))

beam = [(0.0, [0])]
for _ in range(4):
    kandidat = []
    for skor, seq in beam:
        for t in range(5):
            kandidat.append((skor + np.log(P[seq[-1], t]), seq + [t]))
    kandidat.sort(key=lambda x: -x[0])
    beam = kandidat[:2]

semua = itertools.product(range(5), repeat=4)
terbaik = max(((logp_sekuens((0,) + s), (0,) + s) for s in semua),
              key=lambda x: x[0])
print()
print("== 5. greedy vs beam (w=2) vs exhaustive, model bigram 5 token ==")
print(f"  greedy      : {greedy}  logp {logp_sekuens(greedy):.4f}")
print(f"  beam w=2    : {beam[0][1]}  logp {beam[0][0]:.4f}")
print(f"  exhaustive  : {list(terbaik[1])}  logp {terbaik[0]:.4f}")

# ---------- 6. Estimasi memori KV cache ----------
n_lapis, n_kepala, d_kepala, panjang = 32, 32, 128, 2048
byte = 2 * n_lapis * n_kepala * d_kepala * panjang * 2  # K+V, fp16
print()
print("== 6. memori KV cache (32 lapis, 2048 token, fp16) ==")
print(f"  {byte / 1e9:.2f} GB untuk satu sekuens satu batch")
print(f"  batch 4 pengguna: {4 * byte / 1e9:.2f} GB")

Output yang Dihasilkan:

text
== 1. temperature pada logits yang sama ==
  T=0.1: p[:4] = [1.000 0.000 0.000 0.000]  entropi = 0.0030
  T=1.0: p[:4] = [0.495 0.222 0.110 0.074]  entropi = 1.4642
  T=2.0: p[:4] = [0.296 0.198 0.140 0.114]  entropi = 1.8874

== 2. top-k = 2 (renormalisasi) ==
  ikan : 0.495 -> 0.690
  kopi : 0.222 -> 0.310

== 3. top-p = 0.9 (nucleus) ==
  kumulatif: 0.495 0.717 0.828 0.902
  ikan : 0.495
  kopi : 0.222
  teh  : 0.110
  roti : 0.074
  kandidat tersisa: 4 dari 8

== 4. sampling 12 token (seed 42) ==
  T=0.1: ikan ikan ikan ikan ikan ikan ikan ikan ikan ikan ikan ikan
  T=1.0: kopi teh ikan ikan kopi ikan teh kopi teh ikan gula roti
  T=2.0: susu ikan kopi ikan ikan roti roti air kopi kopi kopi ikan

== 5. greedy vs beam (w=2) vs exhaustive, model bigram 5 token ==
  greedy      : [0, 3, 1, 0, 3]  logp -4.8769
  beam w=2    : [0, 3, 4, 4, 4]  logp -4.5326
  exhaustive  : [0, 3, 2, 2, 2]  logp -4.3744

== 6. memori KV cache (32 lapis, 2048 token, fp16) ==
  1.07 GB untuk satu sekuens satu batch
  batch 4 pengguna: 4.29 GB

Mari kita telaah kode di atas baris per baris:

  1. Baris 22-30 (softmax dan entropi): pembagian logits dengan T terjadi sebelum np.exp, dan pengurangan z.max() menjaga stabilitas numerik tanpa mengubah hasil. Fungsi entropi menghitung −Σ p log p — meteran keragaman yang dipakai di seluruh bab ini.
  2. Baris 34-42 (temperature): pada logits yang sama, entropi turun dari 1,4642 (T=1) ke 0,0030 (T=0,1) — distribusi praktis menjadi satu puncak; naik ke T=2 meratakannya ke 1,8874, mendekati entropi maksimal 8 token seragam (ln 8 ≈ 2,08).
  3. Baris 45-54 (top-k): dua kandidat terkuat bertahan; setelah renormalisasi, ikan naik dari 0,495 ke 0,690 — pemotongan ekor memusatkan peluang ke pilihan yang layak.
  4. Baris 57-67 (top-p): kumulatif 0,495 → 0,717 → 0,828 → 0,902 berhenti tepat melewati 0,9, menyisakan 4 dari 8 kandidat. Pada distribusi yang lebih tajam, set ini otomatis mengecil — itulah sifat adaptif nucleus.
  5. Baris 70-77 (sampling): rng.choice berbenih 42 membuat hasil dapat direproduksi; keluarannya memperlihatkan spektrum penuh — T=0,1 dua belas kali ikan, T=1 campuran empat token, T=2 menjamur ke tujuh token berbeda.
  6. Baris 81-104 (tiga pencarian): greedy memilih token terbaik tiap langkah tanpa pandang depan dan berakhir di logp −4,8769. Beam width 2 menyimpan dua jalur parsial terbaik, memperluasnya, dan menemukan −4,5326. Pencarian exhaustive atas seluruh 625 jalur membuktikan batas atasnya −4,3744 — beam mendekati optimal dengan biaya jauh lebih kecil, tetapi tidak menjangkaunya.
  7. Baris 107-113 (KV cache): 2 × 32 × 32 × 128 × 2048 × 2 byte = 1,07 GB untuk satu sekuens — satu faktor 2 untuk pasangan K dan V, satu lagi untuk 2 byte fp16.

KV Cache: Mengapa Inference Cepat #

Attention pada token baru harus memperhatikan seluruh token sebelumnya. Tanpa cache, setiap langkah menghitung ulang proyeksi K dan V untuk seluruh prefiks — biaya yang tumbuh kuadratik sepanjang generasi. KV cache menyimpan hasil proyeksi itu sekali: langkah ke-t cukup menghitung K,V untuk satu token baru dan membaca sisanya dari cache. Konsekuensinya ada dua. Pertama, kompleksitas per langkah turun dari sebanding-t-token-penuh menjadi satu token plus pembacaan cache. Kedua, memori menjadi harga yang dibayar: seperti dihitung pada demo, model 32 lapis dengan konteks 2048 token pada fp16 menyimpan 1,07 GB per sekuens — dan angka itu berlipat seiring batch serta panjang konteks, menjelaskan mengapa memori inference habis jauh sebelum hitungan bobot (yang kini bisa dipangkas lewat Quantization LLM ).

Memilih Strategi #

StrategiPerilakuCocok untuk
Greedy / T→0deterministik, repetitifkode, ekstraksi data, jawaban pendek
Temperature 0,7-1,0 + top-pterkendali namun bervariasichat, penulisan umum
Temperature tinggieksploratif, kadang melencengide awal, brainstorming
Beam searchmemaksimalkan probabilitas sekuensterjemahan, ringkasan terformat
Sampling + stop conditionmenghormati panjang wajarsemua tugas generatif

Satu perangkat penutup yang tak kalah penting: stop condition. Decoding harus tahu kapan berhenti — token EOS yang dipelajari model, batas max_new_tokens, atau detektor pola berulang. Tanpa ketiganya, model bisa terus menulis dengan kalimat yang berputar-putar.

Distribusi dan strategi kini lengkap; satu lapisan terakhir memisahkan keluaran berguna dari keluaran yang salah format. Model chat tidak dilatih pada teks polos, melainkan pada struktur percakapan dengan peran dan token khusus — dan pengguna yang mengabaikan struktur itu akan menuai jawaban yang turun kualitasnya. Struktur itulah pokok bahasan Prompt Format dan Chat Template .