SSD Nodes Learn 🎉 VPS mulai $5.50/bln
Panduan Matt ConnorOleh Matt Connor

Prefill vs Decode: Mengapa Token Pertama Lambat

Prefill menentukan time to first token karena terikat komputasi, sedangkan decode menentukan tokens per second karena terikat bandwidth memori. Ukur keduanya terpisah.

Prefill vs decode dalam satu paragraf

Prefill vs decode adalah satu perbedaan yang menjelaskan sebagian besar pertanyaan tentang latensi LLM (large language model) yang di-host sendiri. Prefill membaca seluruh prompt dalam satu lintasan dan dibatasi oleh kemampuan komputasi. Decode menulis jawaban satu token setiap kali dan dibatasi oleh bandwidth memori. Time to first token adalah nilai prefill. Tokens per second adalah nilai decode.

Kedua fase berjalan pada GPU (graphics processing unit) yang sama, menggunakan weights yang sama, di dalam proses yang sama. Karena itu, wajar jika keduanya dianggap sebagai satu workload. Namun, perilakunya seperti dua program berbeda yang berbagi satu perangkat. Jika dipisahkan, berbagai hasil yang membingungkan tidak lagi membingungkan.

Mengapa komputasi prefill terikat pada komputasi?

Prefill meneruskan seluruh prompt melalui setiap layer satu kali. Prompt dengan 2,000 token memberikan 2,000 baris pekerjaan pada setiap operasi perkalian matriks. Karena itu, GPU melakukan banyak operasi aritmetika untuk setiap byte bobot yang dimuat. Rasio antara operasi aritmetika dan byte yang dipindahkan disebut intensitas aritmetika, dan prefill memiliki intensitas yang tinggi. Perangkat bekerja mendekati batas komputasinya, sedangkan bus memori masih memiliki kapasitas cadangan.

Prefill menghasilkan dua hal: KV cache (tensor key dan value) untuk setiap token prompt, serta token output pertama. Tidak ada hasil yang diterima pembaca sampai proses tersebut selesai. Karena itu, waktu prefill dan time to first token (TTFT) memiliki nilai pengukuran yang hampir sama.

Biaya prefill meningkat seiring panjang prompt. Bagian linear berasal dari pekerjaan perkalian matriks pada setiap layer. Bagian kuadrat berasal dari attention, ketika setiap token memperhatikan semua token sebelumnya. Bagian ini mulai berpengaruh pada context yang panjang. Jadi, menggandakan panjang prompt setidaknya menggandakan TTFT.

Anda dapat mengamati hal ini dalam satu menit. Kirim prompt dengan 200 token ke server Anda, lalu prompt dengan 2,000 token. Minta jumlah token output yang sama pada kedua pengujian. TTFT meningkat tajam. Kecepatan streaming setelah token pertama hampir tidak berubah.

Mengapa decode dibatasi oleh bandwidth memori?

Decode menghasilkan satu token pada setiap langkah. Untuk menghasilkan satu token tersebut, GPU harus membaca semua bobot model dari memori, menggunakan setiap bobot untuk beberapa operasi, lalu membuangnya. Intensitas aritmetiknya mendekati 1, sehingga unit komputasi menghabiskan sebagian besar waktunya untuk menunggu.

Decode lambat karena setiap token memerlukan pembacaan seluruh model dari memori. Karena itu, bus memori menentukan lajunya dan unit komputasi menganggur.

Dengan demikian, batas atas kecepatan decode satu aliran ditentukan oleh perhitungan aritmetika sederhana. Bagi bandwidth memori dengan jumlah byte yang ditempati bobot.

ChartPublished memory bandwidth and the decode ceiling it implies for a 16 GB model
The data behind this chart
[
  {
    "device": "CPU, dual channel DDR5-5600",
    "mem_bandwidth_gb_s": 90,
    "decode_ceiling_tok_s": 6
  },
  {
    "device": "NVIDIA A10G",
    "mem_bandwidth_gb_s": 600,
    "decode_ceiling_tok_s": 38
  },
  {
    "device": "NVIDIA L40S",
    "mem_bandwidth_gb_s": 864,
    "decode_ceiling_tok_s": 54
  },
  {
    "device": "NVIDIA RTX 4090",
    "mem_bandwidth_gb_s": 1008,
    "decode_ceiling_tok_s": 63
  },
  {
    "device": "NVIDIA A100 80GB SXM",
    "mem_bandwidth_gb_s": 2039,
    "decode_ceiling_tok_s": 127
  },
  {
    "device": "NVIDIA H100 SXM",
    "mem_bandwidth_gb_s": 3350,
    "decode_ceiling_tok_s": 209
  }
]

Kolom bandwidth memuat angka spesifikasi yang dipublikasikan oleh setiap vendor. Kolom ceiling memuat angka tersebut dibagi 16 GB, yaitu ukuran model dengan 8 miliar parameter yang disimpan pada presisi 16 bit. Ini adalah perhitungan aritmetika, bukan hasil benchmark. Laju yang Anda ukur akan berada di bawah angka tersebut. Mengetahui selisihnya berguna karena dapat menunjukkan apakah Anda perlu memperbaiki stack serving atau perangkat keras.

Baca 6 baris tersebut secara berurutan. Polanya terlihat jelas. CPU dengan DDR5 dual-channel mencapai sekitar 90 GB/s, sehingga kecepatan decode untuk model tersebut dibatasi hingga sekitar 6 token per detik. L40S mencapai sekitar 54. H100 SXM, dengan bandwidth yang dipublikasikan sebesar 3350 GB/s, berada di sekitar 209.

Inilah alasan kuantisasi juga menjadi pengungkit tunggal terkuat untuk meningkatkan kecepatan decode. Simpan model yang sama pada 8 bit, bukan 16 bit, dan jumlah byte yang dibaca untuk setiap token menjadi setengahnya. Dengan demikian, batas atasnya kira-kira menjadi dua kali lipat. Anda tidak menambahkan komputasi. Anda hanya memindahkan lebih sedikit data dari memori.

Bagaimana cara mengukur setiap fase pada server saya sendiri?

Ollama mengembalikan pemisahan fase dalam body respons. Minta completion tanpa streaming, lalu baca penghitungnya.

curl -s http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "Explain memory bandwidth in two sentences.",
  "stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'

Gunakan tag model yang benar-benar sudah Anda pull, yang akan ditampilkan oleh ollama list. prompt_eval_count dan prompt_eval_duration adalah prefill: jumlah token prompt dan waktu yang digunakan untuk memprosesnya. eval_count dan eval_duration adalah decode. Durasi dinyatakan dalam nanodetik, sehingga kecepatan decode adalah eval_count / eval_duration * 1e9 dan kecepatan prefill adalah prompt_eval_count / prompt_eval_duration * 1e9. Pada request yang sama, kecepatan prefill biasanya jauh lebih tinggi daripada kecepatan decode. Perbedaan inilah yang dijelaskan oleh bagian lain di sini.

Untuk server yang kompatibel dengan OpenAI, seperti vLLM, curl dapat mengukur waktu hingga byte pertama untuk Anda.

curl -N -s -o /dev/null \
  -w 'pretransfer %{time_pretransfer}s  first_byte %{time_starttransfer}s\n' \
  http://localhost:8000/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'

time_starttransfer adalah saat byte pertama dari body tiba. Jadi, bersama "stream": true, nilainya mencakup TTFT dan penyiapan koneksi. Kurangi time_pretransfer untuk menghapus biaya penyiapan. Jalankan dua kali dan gunakan hasil kedua, karena pemanggilan pertama dapat mencakup pemuatan model yang belum ada di cache.

vLLM juga memublikasikan pemisahan fase sebagai metrik Prometheus pada /metrics. Jalankan curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' untuk mendapatkan histogram vllm:time_to_first_token_seconds dan vllm:inter_token_latency_seconds. Tambahkan vllm:num_requests_running dan vllm:num_requests_waiting untuk kedalaman antrean, serta vllm:kv_cache_usage_perc untuk tekanan cache. Kelima nama tersebut sudah mencakup seluruh dashboard.

Saat server menerima beban, vllm bench serve --model <name> --num-prompts 200 --request-rate 4 menjalankan pengujian pada server dan melaporkan waktu hingga token pertama serta latensi per token output menggunakan persentil. Hanya dengan cara ini Anda dapat melihat kedua fase tersebut saling memengaruhi. Sebelum mengubah konfigurasi apa pun, ambil baseline yang bersih: metode pada mengukur token per detik pada LLM lokal menghasilkan baseline yang tetap dapat digunakan setelah reboot.

Mengapa system prompt yang panjang menunda token pertama, tetapi tidak memperlambat kecepatan streaming?

Karena system prompt hanya menambah pekerjaan prefill. System prompt diproses satu kali, dalam pass yang sama dengan bagian prompt lainnya, sebelum token pertama muncul. Setelah pass tersebut selesai, isinya hanya tersimpan sebagai entri KV cache, lalu proses decode membacanya bersama entri lainnya. Jadi, system prompt sepanjang 3,000 token menambah TTFT pada setiap request, tetapi hampir tidak mengubah jumlah token per detik.

Hampir, tetapi tidak sepenuhnya. Entri KV tambahan tersebut dibaca kembali pada setiap langkah decode, sehingga prompt yang sangat panjang sedikit memperlambat proses decode. Bagian berikutnya membahas hal ini.

Solusinya adalah menghentikan penghitungan ulang prefix yang sama. Server dengan prefix caching menyimpan KV cache dari prefix yang digunakan bersama dan menggunakannya kembali. Dengan demikian, request kedua yang membawa system prompt yang sama dapat melewati bagian prefill tersebut sepenuhnya. vLLM menyebut fitur ini automatic prefix caching. Periksa vllm serve --help pada versi Anda karena nilai defaultnya berubah pada beberapa rilis. KV cache dalam GPU ini berbeda dari prompt cache yang ditagihkan oleh penyedia API. Sebelum menyetel salah satunya, baca perbedaan antara KV cache dan prompt cache.

Mengapa proses decode melambat saat context semakin penuh?

Ada dua alasan, dan keduanya berkaitan dengan KV cache.

Alasan pertama adalah bandwidth. Pada setiap langkah decode, attention membaca key dan value dari setiap token sebelumnya. Bobot model merupakan biaya tetap per token. KV cache terus bertambah. Ukurannya dapat dihitung dari config.json model: byte per token sama dengan 2 dikalikan num_hidden_layers, num_key_value_heads, dan dimensi head (hidden_size dibagi num_attention_heads), lalu dikalikan jumlah byte per elemen. Angka 2 di awal menghitung satu key dan satu value.

Untuk konfigurasi umum dengan 8 billion parameter, 32 layer, 8 key dan value head dalam GQA (grouped query attention), dimensi head 128, serta presisi 16 bit, hasilnya adalah 2 x 32 x 8 x 128 x 2 = 131,072 byte, atau sekitar 128 KiB per token. Dengan demikian, percakapan sepanjang 8,000 token membawa sekitar 1 GB KV cache untuk setiap request.

Alasan kedua adalah kapasitas. Memori sebesar 1 GB tersebut tidak dapat digunakan untuk menyimpan bobot atau context pengguna lain. Server menentukan ukuran pool KV satu kali saat startup, pada vLLM melalui --gpu-memory-utilization. Saat pool penuh, request baru menunggu. vllm:num_requests_waiting yang terus meningkat sementara vllm:kv_cache_usage_perc berada di dekat 1 merupakan tanda pasti kondisi tersebut. Beberapa stack melakukan preemption terhadap request yang sedang berjalan, lalu menghitung ulang cache-nya nanti, bukan memasukkannya ke antrean. Bagi pengguna, hal ini tampak sebagai jeda di tengah stream.

Context yang panjang menimbulkan dua biaya: pekerjaan prefill yang lebih besar di awal dan pembacaan memori yang lebih banyak untuk setiap token sepanjang sisa jawaban.

Mengapa batching meningkatkan throughput tetapi memperburuk tail latency?

Karena decode dibatasi oleh bandwidth, request tambahan hampir tidak menambah beban pada sisi komputasi. Satu pembacaan bobot dapat menghasilkan satu token untuk setiap sequence dalam batch. Karena itu, throughput total meningkat hampir secara linear seiring ukuran batch, sampai KV pool habis atau batch cukup besar sehingga kembali dibatasi oleh komputasi. Continuous batching membentuk ulang batch pada setiap langkah. Request yang selesai dapat keluar dan request yang mengantre dapat masuk tanpa menunggu request lain dalam batch selesai.

Biayanya terlihat pada persentil. Next token setiap pengguna kini harus menunggu bagian paling lambat dari satu langkah bersama. Akibatnya, p50 atau median tetap dapat diterima, sedangkan p99, yaitu 1 request paling lambat dari setiap 100 request, menjadi lebih panjang. p99 paling mudah dirasakan pengguna karena berupa jeda di tengah kalimat.

Prefill memperjelas dampak ini. Prompt besar yang tiba saat proses streaming berlangsung akan menggunakan perangkat selama satu langkah panjang. Semua request yang sedang melakukan streaming akan mengalami jeda. Chunked prefill mengurangi sebagian besar dampak tersebut dengan membagi prompt panjang menjadi beberapa bagian, lalu mencampurkan setiap bagian ke dalam batch decode. Per Agustus 2026, engine vLLM V1 melakukan hal ini secara default dan menyediakan pengaturan keseimbangannya melalui --max-num-batched-tokens. Dokumentasi tuning vLLM menjelaskan trade-off ini secara langsung: nilai yang lebih kecil, sekitar 2048, memberikan inter token latency (ITL) yang lebih baik karena lebih sedikit prefill yang menginterupsi decode. Nilai yang lebih besar memberikan TTFT yang lebih baik karena lebih banyak token prefill dapat dimasukkan ke dalam satu batch. Flag tersebut mengekspos trade-off antara prefill dan decode sebagai angka yang dapat Anda atur. Batas ketika p99 tidak lagi dapat diterima bergantung pada kapasitas. berapa banyak pengguna serentak yang dapat dilayani oleh LLM yang di-self-host membahas hal ini menggunakan metrik yang sama.

Mengapa GPU yang lebih besar terkadang tidak mengubah apa pun?

Karena ukuran yang lebih besar biasanya berarti komputasi yang lebih besar, sedangkan decode tidak membutuhkan komputasi.

Bandingkan dua baris pada bagan di atas. A100 80GB memiliki bandwidth yang dipublikasikan sebesar 2039 GB/s, sedangkan L40S memiliki 864 GB/s. Batas decode juga mengikuti angka tersebut: 127 token per detik dibandingkan dengan 54. RTX 4090 merupakan kartu yang sangat cepat menurut sebagian besar ukuran, dan bandwidth 1008 GB/s menempatkan batasnya pada 63. Terlepas dari perbedaan lain antara dua kartu, decode single-stream mengikuti angka bandwidth pada lembar spesifikasi.

Jadi, ada dua cara untuk mempercepat decode: membaca lebih sedikit byte untuk setiap token (mengkuantisasi weight, atau menjalankan model yang lebih kecil), atau membeli bandwidth yang lebih besar. Prefill merupakan kasus yang berlawanan. Prefill membutuhkan komputasi, sehingga kartu yang lebih cepat benar-benar mengurangi TTFT pada prompt yang panjang. Jika masalahnya adalah token pertama membutuhkan waktu empat detik, hardware yang lebih baik mungkin dapat mengatasinya. Jika masalahnya adalah teks muncul secara perlahan, kemungkinan besar hardware yang lebih baik tidak akan membantu.

Jalankan prefill dan decode pada worker terpisah?

Stack serving berskala besar memang melakukan hal ini. Teknik tersebut disebut disaggregation prefill dan decode. Satu pool worker hanya menjalankan prefill, sedangkan pool kedua hanya menjalankan decode. KV cache yang dibuat oleh pool pertama kemudian ditransfer ke pool kedua melalui interkoneksi berkecepatan tinggi. Teknik ini berfungsi karena kedua fase memerlukan perangkat keras dan penjadwalan yang berbeda. Prefill memerlukan komputasi dan batch token yang besar. Decode memerlukan bandwidth dan banyak sequence yang berjalan secara bersamaan. Pemisahan ini memungkinkan setiap pool diskalakan secara independen. Cara ini juga mencegah satu prompt yang sangat besar menahan semua stream aktif.

Pada satu VPS (virtual private server) dengan satu GPU, penerapan ini hampir tidak pernah sepadan. Anda membagi satu perangkat untuk dirinya sendiri. Selain itu, pointer akan diubah menjadi transfer jaringan yang membawa cache berukuran gigabyte. Teknik ini mulai bermanfaat setelah Anda memiliki akselerator yang cukup untuk mendedikasikan seluruh mesin bagi setiap fase, serta trafik stabil yang cukup untuk menjaga kedua pool tetap sibuk. Sebelum mencapai kondisi tersebut, chunked prefill memberikan sebagian besar isolasi yang sama hanya dengan satu flag.

Hal yang perlu diubah ketika angkanya buruk

Jika TTFT terlalu tinggi:

  • Persingkat prompt. Biaya prefill mengikuti jumlah token prompt, dan system prompt dibebankan pada setiap request.
  • Aktifkan prefix caching agar prefix yang berulang dihitung satu kali, bukan pada setiap request.
  • Tingkatkan --max-num-batched-tokens agar lebih banyak pekerjaan prefill diproses dalam setiap langkah.
  • Periksa queue sebelum menyalahkan model. Nilai vllm:num_requests_waiting di atas nol berarti request belum dimulai. Ini menunjukkan masalah kapasitas.

Jika token per detik terlalu rendah:

  • Kuantisasi weight. Semakin sedikit byte per weight, semakin sedikit byte yang dibaca untuk setiap token.
  • Bandingkan memory bandwidth yang dipublikasikan untuk kartu Anda dengan chart di atas, lalu periksa seberapa dekat nilainya dengan batas maksimum.
  • Kurangi --max-num-batched-tokens agar prefill lebih jarang menginterupsi decode.
  • Periksa panjang context. Percakapan yang telah berkembang hingga ribuan token membaca KV cache yang jauh lebih besar pada setiap langkah.

Runtime juga berpengaruh di sini karena Ollama dan vLLM menjadwalkan prefill dan decode secara berbeda, dan pengaturan yang membantu salah satunya mungkin tidak berpengaruh pada yang lain. Lakukan pengukuran terlebih dahulu pada kedua fase, lalu ubah satu hal saja.

FAQ

Mengapa token pertama memerlukan waktu beberapa detik, tetapi token berikutnya mengalir dengan cepat?

Waktu tunggu tersebut adalah prefill, sedangkan proses streaming adalah decode. Prefill memproses seluruh prompt dalam satu tahap yang dibatasi oleh komputasi sebelum output tersedia. Karena itu, biayanya bertambah seiring panjang prompt. Decode kemudian menghasilkan satu token pada setiap langkah dengan kecepatan yang ditentukan oleh bandwidth memori. Kecepatan ini hampir tidak bergantung pada panjang prompt. System prompt yang panjang pada setiap request biasanya menjadi penyebabnya. Caching prefix menghilangkan pengulangan biaya tersebut.

Apakah prompt yang lebih panjang memperlambat jumlah token per detik?

Sedikit, tetapi penyebabnya berbeda dari TTFT. Pada setiap langkah decode, sistem membaca key dan value dari semua token sebelumnya. Karena itu, KV cache yang lebih besar berarti lebih banyak byte yang dibaca untuk setiap token. Pada layout umum dengan 8 miliar parameter, ukuran cache sekitar 128 KiB per token. Jadi, context sepanjang 8,000 token berarti sekitar 1 GB data yang diakses pada setiap langkah. Dampak terbesar dari prompt yang panjang tetap terjadi pada TTFT, bukan pada kecepatan streaming.

Spesifikasi GPU mana yang memprediksi kecepatan decode?

Bandwidth memori. Bagi bandwidth yang dipublikasikan dengan ukuran weights di memori. Hasilnya adalah batas aritmetis untuk satu stream. GPU dengan komputasi lebih besar tetapi bandwidth yang sama tidak akan melakukan streaming lebih cepat. Hal ini juga menjelaskan mengapa kuantisasi ke 8 bit kira-kira menggandakan kecepatan decode. Kuantisasi tersebut mengurangi separuh byte yang dibaca untuk setiap token tanpa mengubah komputasi.

Mengapa throughput meningkat ketika saya menambahkan pengguna, tetapi setiap pengguna merasa lebih lambat?

Satu pembacaan weights dapat melayani satu token untuk setiap sequence dalam batch. Karena itu, total token per detik meningkat seiring ukuran batch. Namun, setiap token individual kini harus menunggu shared step. Akibatnya, latensi per pengguna juga meningkat. Pantau inter token latency p99, bukan angka throughput agregat, dan periksa vllm:num_requests_waiting untuk mengetahui apakah request sedang mengantre, bukan berjalan.