Prefill vs Decode LLM: Kenapa Token Pertama Lambat?
Fasa prefill terikat dengan pengiraan manakala decode terikat dengan lebar jalur memori. Ketahui punca sebenar kependaman LLM dan cara mengukur prestasi kedua-dua fasa ini.
Prefill berbanding decode, dalam satu perenggan
Perbezaan antara prefill dan decode merupakan faktor utama yang menjelaskan kebanyakan persoalan mengenai kependaman (latency) LLM (large language model) yang dihoskan sendiri. Prefill membaca keseluruhan prompt dalam satu hantaran dan dihadkan oleh kuasa pengkomputeran. Decode pula menulis jawapan satu token pada satu masa dan dihadkan oleh lebar jalur memori. Time to first token ialah metrik bagi fasa prefill, manakala tokens per second ialah metrik bagi fasa decode.
Kedua-dua fasa ini berjalan pada GPU (graphics processing unit) yang sama, menggunakan pemberat (weights) yang sama, di dalam proses yang sama, maka adalah wajar untuk menganggapnya sebagai satu beban kerja. Namun, ia berkelakuan seperti dua program berbeza yang berkongsi satu peranti. Asingkan kedua-duanya dan senarai panjang keputusan yang mengelirukan akan menjadi jelas.
Mengapa fasa prefill terikat dengan pengiraan (compute bound)?
Fasa prefill menolak keseluruhan prompt melalui setiap lapisan sebanyak sekali. Prompt sepanjang 2,000 token memberikan setiap pendaraban matriks beban kerja sebanyak 2,000 baris, jadi GPU melakukan banyak pengiraan aritmetik bagi setiap bait berat (weight) yang dimuatkan. Nisbah ini, iaitu jumlah aritmetik bagi setiap bait yang dipindahkan, dipanggil keamatan aritmetik (arithmetic intensity), dan prefill mempunyai keamatan yang tinggi. Peranti berjalan hampir pada had pengiraannya dan bas memori mempunyai lebihan kapasiti.
Prefill menghasilkan dua perkara: cache KV (tensor key dan value) untuk setiap token prompt, dan token output pertama. Tiada apa-apa yang sampai kepada pembaca sehingga proses tersebut selesai, itulah sebabnya masa prefill dan masa untuk token pertama (TTFT) adalah ukuran yang hampir sama.
Kos prefill meningkat mengikut panjang prompt. Bahagian linear adalah kerja matriks bagi setiap lapisan. Bahagian kuadratik adalah perhatian (attention), di mana setiap token memberi perhatian kepada setiap token sebelumnya, dan ia mula menjadi signifikan pada konteks yang panjang. Jadi, menggandakan prompt sekurang-kurangnya menggandakan TTFT.
Anda boleh memerhatikan perkara ini dalam masa satu minit. Hantar prompt 200 token ke pelayan anda, kemudian prompt 2,000 token, dengan meminta bilangan token output yang sama setiap kali. TTFT akan meningkat dengan mendadak. Kelajuan penstriman selepas token pertama hampir tidak berubah.
Mengapa lebar jalur memori menjadi kekangan bagi proses penyahkodan (decode)?
Proses penyahkodan menghasilkan satu token bagi setiap langkah. Untuk menghasilkan token tunggal tersebut, GPU perlu membaca setiap pemberat (weight) dalam model daripada memori, menggunakan setiap pemberat untuk beberapa operasi, kemudian membuangnya. Keamatan aritmetik adalah hampir kepada 1, jadi unit pengiraan menghabiskan kebanyakan masa dalam keadaan menunggu.
Proses penyahkodan menjadi perlahan kerana setiap token memerlukan keseluruhan model dibaca daripada memori, maka bas memori menentukan kelajuan manakala unit pengiraan melahu.
Ini menjadikan had siling bagi kelajuan penyahkodan aliran tunggal sebagai pengiraan aritmetik yang boleh dilakukan di atas kertas. Bahagikan lebar jalur memori dengan bait yang diduduki oleh pemberat tersebut.
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
}
]Lajur lebar jalur mengandungi angka spesifikasi yang diterbitkan oleh setiap vendor. Lajur siling ialah angka tersebut dibahagikan dengan 16 GB, iaitu saiz model 8 bilion parameter yang disimpan pada ketepatan 16 bit. Ini adalah pengiraan aritmetik, bukan hasil penanda aras (benchmark). Kadar yang anda ukur akan berada di bawah angka tersebut, dan mengetahui sejauh mana perbezaannya adalah berguna, kerana ia memberitahu anda sama ada perlu membaiki tindanan (stack) penyajian atau perkakasan anda.
Baca 6 baris dalam jadual mengikut urutan dan coraknya jelas. CPU pada DDR5 dwi-saluran memindahkan kira-kira 90 GB/s, yang mengehadkan penyahkodan kepada hampir 6 token sesaat untuk model tersebut. L40S berada pada tahap hampir 54. H100 SXM, dengan lebar jalur yang diterbitkan sebanyak 3350 GB/s, berada pada tahap hampir 209.
Inilah sebabnya kuantisasi merupakan tuas tunggal paling kuat bagi kelajuan penyahkodan. Simpan model yang sama pada 8 bit dan bukannya 16 bit, maka anda mengurangkan separuh bait yang dibaca bagi setiap token, jadi had siling meningkat kira-kira dua kali ganda. Anda tidak menambah sebarang pengiraan. Anda hanya mengurangkan pemindahan memori.
Bagaimanakah cara saya mengukur setiap fasa pada pelayan saya sendiri?
Ollama mengembalikan pecahan tersebut dalam badan respons. Minta pelengkapan bukan penstriman dan baca pembilang tersebut.
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 telah anda tarik, yang akan ditunjukkan oleh ollama list kepada anda. prompt_eval_count dan prompt_eval_duration ialah pra-isi (prefill): kiraan token gesaan, dan masa yang dihabiskan untuknya. eval_count dan eval_duration ialah penyahkodan (decode). Tempoh adalah dalam nanosaat, jadi kelajuan penyahkodan ialah eval_count / eval_duration * 1e9 dan kelajuan pra-isi ialah prompt_eval_count / prompt_eval_duration * 1e9. Jangkakan kadar pra-isi jauh lebih tinggi daripada kadar penyahkodan pada permintaan yang sama. Jurang itulah yang dijelaskan oleh segala-galanya di sini.
Untuk pelayan yang serasi dengan OpenAI seperti vLLM, curl boleh mengukur masa bait 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 ialah saat bait badan pertama tiba, jadi dengan "stream": true ia adalah TTFT ditambah penyediaan sambungan. Tolak time_pretransfer untuk membuang kos penyediaan. Jalankan dua kali dan simpan hasil kedua, kerana panggilan pertama mungkin termasuk pemuatan model sejuk.
vLLM juga menerbitkan pecahan tersebut sebagai metrik Prometheus pada /metrics. Jalankan curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' dan anda akan mendapat 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 baris gilir, dan vllm:kv_cache_usage_perc untuk tekanan cache. Lima nama tersebut adalah keseluruhan papan pemuka.
Di bawah beban, vllm bench serve --model <name> --num-prompts 200 --request-rate 4 memacu pelayan yang sedang berjalan dan melaporkan masa ke token pertama serta kependaman setiap token output dengan persentil, yang merupakan satu-satunya cara untuk melihat kedua-dua fasa tersebut saling bersaing. Sebelum anda menala apa-apa, ambil garis dasar yang bersih: kaedah dalam mengukur token sesaat pada LLM tempatan memberikan anda satu garis dasar yang kekal selepas but semula.
Mengapa prompt sistem yang panjang melambatkan token pertama tetapi tidak menjejaskan kelajuan penstriman?
Ini kerana prompt sistem merupakan kerja pra-isi (prefill) dan tiada yang lain. Ia diproses sekali sahaja, dalam pas yang sama dengan baki prompt yang lain, sebelum token pertama muncul. Selepas pas tersebut, ia hanya wujud sebagai entri KV cache, dan proses penyahkodan (decode) membaca entri tersebut bersama-sama data lain. Oleh itu, prompt sistem sepanjang 3,000 token akan menambah TTFT pada setiap permintaan, sementara kelajuan token sesaat hampir tidak berubah.
Hampir, bukan tepat. Entri KV tambahan tersebut dibaca semula pada setiap langkah penyahkodan, jadi prompt yang sangat panjang akan melambatkan sedikit proses penyahkodan. Bahagian seterusnya akan membincangkan perkara ini.
Penyelesaiannya adalah dengan berhenti mengira semula awalan (prefix) yang sama. Pelayan dengan prefix caching menyimpan KV cache bagi awalan yang dikongsi dan menggunakannya semula, jadi permintaan kedua yang membawa prompt sistem yang sama akan melangkau bahagian pra-isi tersebut sepenuhnya. vLLM memanggil ini sebagai automatic prefix caching; semak vllm serve --help pada versi anda, kerana tetapan lalai telah berubah merentas keluaran. KV cache dalam GPU itu adalah perkara yang berbeza daripada prompt cache yang dikenakan caj oleh penyedia API, dan perbezaan antara KV cache dan prompt cache wajar dibaca sebelum anda menala mana-mana daripadanya.
Mengapa proses nyahkod menjadi perlahan apabila konteks penuh?
Terdapat dua sebab, kedua-duanya berkaitan dengan KV cache.
Sebab pertama ialah lebar jalur (bandwidth). Pada setiap langkah nyahkod, mekanisme attention membaca kunci (keys) dan nilai (values) bagi setiap token sebelumnya. Berat model (weights) merupakan kos tetap bagi setiap token. KV cache pula bersaiz semakin meningkat. Anda boleh mengira saiznya daripada config.json model tersebut: bait bagi setiap token bersamaan dengan 2 didarab dengan num_hidden_layers, didarab dengan num_key_value_heads, didarab dengan dimensi kepala (hidden_size dibahagi dengan num_attention_heads), didarab dengan bait bagi setiap elemen. Angka 2 di hadapan mewakili satu kunci dan satu nilai.
Bagi susun atur 8 bilion parameter yang biasa, 32 lapisan, 8 kepala kunci dan nilai di bawah GQA (grouped query attention), dimensi kepala 128, pada ketepatan 16 bit, jumlahnya ialah 2 x 32 x 8 x 128 x 2 = 131,072 bait, iaitu kira-kira 128 KiB bagi setiap token. Oleh itu, perbualan sepanjang 8,000 token membawa kira-kira 1 GB KV cache bagi setiap permintaan.
Sebab kedua ialah kapasiti. 1 GB tersebut merupakan memori yang tidak boleh menyimpan berat model atau konteks pengguna lain. Pelayan menetapkan saiz pool KV sekali sahaja semasa permulaan, pada vLLM melalui --gpu-memory-utilization, dan apabila pool penuh, permintaan baharu akan menunggu. vllm:num_requests_waiting yang meningkat sementara vllm:kv_cache_usage_perc berada hampir pada tahap 1 adalah tanda tepat bagi keadaan tersebut. Sesetengah stack akan melakukan preempt terhadap permintaan yang sedang berjalan dan mengira semula cache-nya kemudian daripada meletakkannya dalam baris gilir, yang dialami oleh pengguna sebagai gangguan di tengah-tengah aliran teks.
Konteks yang panjang merugikan anda dua kali ganda: lebih banyak kerja prefill pada permulaan, dan lebih banyak bacaan memori bagi setiap token untuk baki jawapan seterusnya.
Mengapakah pemprosesan kelompok (batching) membantu daya pemprosesan tetapi menjejaskan kependaman ekor (tail latency)?
Oleh sebab penyahkodan (decode) dihadkan oleh lebar jalur, permintaan tambahan hampir tidak memerlukan kos dari segi pengkomputeran. Satu bacaan pemberat (weights) boleh menghasilkan token untuk setiap jujukan dalam kelompok, jadi jumlah daya pemprosesan meningkat hampir secara linear mengikut saiz kelompok sehingga kolam KV kehabisan ruang atau kelompok menjadi terlalu besar sehingga dihadkan semula oleh pengkomputeran. Continuous batching membina semula kelompok pada setiap langkah, jadi permintaan yang selesai akan keluar dan permintaan yang beratur akan masuk tanpa perlu menunggu permintaan lain.
Kesan negatifnya dapat dilihat pada persentil. Token seterusnya bagi setiap pengguna kini perlu menunggu bahagian paling perlahan dalam langkah yang dikongsi, jadi p50 (median) kekal boleh diterima, manakala p99 (1 permintaan paling perlahan daripada 100) akan menjadi lebih panjang. p99 adalah apa yang disedari oleh pengguna, kerana ia merupakan jeda di tengah-tengah ayat.
Prefill menjadikan kesan ini lebih ketara. Prompt besar yang tiba di tengah-tengah aliran akan menduduki peranti untuk satu langkah yang panjang, dan semua orang yang sedang menstrim akan mengalami gangguan. Chunked prefill menghapuskan sebahagian besar masalah ini dengan memotong prompt yang panjang kepada beberapa bahagian dan mencampurkan setiap bahagian ke dalam kelompok penyahkodan. Setakat Ogos 2026, enjin vLLM V1 melakukan perkara ini secara lalai dan mendedahkan keseimbangan tersebut melalui --max-num-batched-tokens. Dokumentasi penalaan vLLM menyatakan pertukaran (tradeoff) ini dengan jelas: nilai yang lebih kecil, sekitar 2048, memberikan kependaman antara token (ITL) yang lebih baik kerana kurang gangguan prefill terhadap penyahkodan, manakala nilai yang lebih besar memberikan TTFT yang lebih baik kerana lebih banyak token prefill dimuatkan ke dalam satu kelompok. Flag tunggal itu adalah perbandingan antara prefill dan decode, yang didedahkan sebagai nombor yang boleh anda ubah. Tahap di mana p99 tidak lagi boleh diterima adalah persoalan mengenai kapasiti, dan berapa ramai pengguna serentak yang boleh dilayani oleh satu LLM yang dihoskan sendiri membincangkan perkara ini dengan metrik yang sama.
Mengapa GPU yang lebih besar kadangkala tidak membawa perubahan?
Ini kerana saiz yang lebih besar biasanya bermaksud lebih banyak kuasa pengiraan, sedangkan proses nyahkod (decode) tidak memerlukan kuasa pengiraan yang tinggi.
Bandingkan dua baris dalam carta di atas. A100 80GB mempunyai lebar jalur yang diterbitkan sebanyak 2039 GB/s berbanding L40S pada 864 GB/s, dan had nyahkod mengikutinya dengan tepat: 127 token sesaat berbanding 54. RTX 4090 merupakan kad yang sangat pantas mengikut kebanyakan ukuran dan lebar jalur 1008 GB/s miliknya meletakkan hadnya pada 63. Walau apa pun perbezaan lain antara dua kad, nyahkod aliran tunggal (single stream decode) akan mengikut garis lebar jalur pada helaian spesifikasi.
Oleh itu, terdapat dua cara untuk menjadikan nyahkod lebih pantas: membaca lebih sedikit bait bagi setiap token (kuantisasi pemberat, atau jalankan model yang lebih kecil), atau membeli lebar jalur yang lebih besar. Prefill adalah kes yang sebaliknya. Ia memerlukan kuasa pengiraan, jadi kad yang lebih pantas benar-benar memendekkan TTFT pada prompt yang panjang. Jika aduannya ialah token pertama mengambil masa empat saat, perkakasan yang lebih baik mungkin dapat menyelesaikannya. Jika aduannya ialah teks ditaip dengan perlahan, perkakasan baharu mungkin tidak akan membantu.
Adakah anda perlu menjalankan prefill dan decode pada worker yang berasingan?
Struktur penyajian (serving stacks) berskala besar melakukan perkara ini, dan teknik tersebut dipanggil penyahagregatan prefill dan decode. Satu kumpulan worker hanya menjalankan prefill, manakala kumpulan kedua hanya menjalankan decode, dan KV cache yang dibina oleh kumpulan pertama dipindahkan ke kumpulan kedua melalui perhubungan (interconnect) berkelajuan tinggi. Teknik ini berkesan kerana kedua-dua fasa tersebut memerlukan perkakasan dan penjadualan yang berbeza. Prefill memerlukan kuasa pengkomputeran dan kelompok token yang besar. Decode pula memerlukan lebar jalur dan banyak jujukan serentak. Memisahkan kedua-duanya membolehkan setiap kumpulan diskalakan secara kendiri, dan ia menghalang satu prompt yang sangat besar daripada menyekat setiap aliran yang aktif.
Pada satu VPS (virtual private server) dengan satu GPU, tindakan ini hampir tidak berbaloi untuk dilakukan. Anda akan membahagikan satu peranti untuk menentang dirinya sendiri, dan anda akan menukarkan penuding (pointer) kepada pemindahan cache bersaiz gigabait melalui rangkaian. Teknik ini memberikan hasil apabila anda mempunyai cukup pemecut (accelerator) untuk mendedikasikan keseluruhan mesin bagi setiap fasa, serta trafik yang stabil untuk memastikan kedua-dua kumpulan sentiasa sibuk. Di bawah tahap tersebut, chunked prefill memberikan tahap pengasingan yang hampir sama melalui satu flag sahaja.
Perkara yang perlu diubah apabila angka tidak memuaskan
Apabila TTFT terlalu tinggi:
- Pendekkan prompt. Kos prefill mengambil kira token prompt, dan system prompt dibayar pada setiap permintaan.
- Hidupkan prefix caching supaya awalan yang berulang dikira sekali sahaja dan bukan setiap kali.
- Tingkatkan
--max-num-batched-tokenssupaya lebih banyak kerja prefill dilakukan dalam setiap langkah. - Periksa baris gilir sebelum menyalahkan model.
vllm:num_requests_waitingmelebihi sifar bermakna permintaan belum bermula, yang merupakan masalah kapasiti.
Apabila token sesaat terlalu rendah:
- Lakukan kuantisasi pada weight. Kurang bait bagi setiap weight bermakna kurang bait dibaca bagi setiap token.
- Semak lebar jalur memori kad anda yang diterbitkan berbanding carta di atas dan lihat sejauh mana anda menghampiri had maksimum.
- Rendahkan
--max-num-batched-tokenssupaya prefill kurang mengganggu proses decode. - Periksa panjang konteks. Perbualan yang berkembang kepada ribuan token membaca KV cache yang jauh lebih besar pada setiap langkah.
Runtime juga penting di sini, kerana Ollama dan vLLM menjadualkan prefill dan decode secara berbeza, dan tetapan yang membantu satu runtime mungkin tidak memberi kesan kepada yang lain. Ukur dahulu, dalam kedua-dua fasa, kemudian ubah satu perkara sahaja.
FAQ
Mengapa token pertama saya mengambil masa beberapa saat tetapi yang seterusnya distrim dengan pantas?
Tempoh menunggu tersebut ialah prefill, manakala penstriman ialah decode. Prefill memproses keseluruhan prompt dalam satu laluan terikat pengiraan sebelum sebarang output wujud, jadi kosnya meningkat mengikut panjang prompt. Decode kemudian mengeluarkan satu token bagi setiap langkah pada kadar yang ditetapkan oleh lebar jalur memori (memory bandwidth), yang hampir tidak bergantung pada panjang prompt tersebut. System prompt yang panjang pada setiap permintaan biasanya menjadi punca. Prefix caching menghapuskan bahagian kos yang berulang itu.
Adakah prompt yang lebih panjang melambatkan token sesaat?
Sedikit, dan atas sebab yang berbeza daripada TTFT. Setiap langkah decode membaca keys dan values bagi semua token sebelumnya, jadi KV cache yang lebih besar bermakna lebih banyak bait dibaca bagi setiap token. Untuk susun atur 8 bilion parameter yang biasa, cache tersebut adalah kira-kira 128 KiB bagi setiap token, jadi konteks 8,000 token adalah lebih kurang 1 GB yang dicapai pada setiap langkah. Kesan yang lebih besar daripada prompt yang panjang masih tertumpu pada TTFT, bukan pada kelajuan penstriman.
Spesifikasi GPU manakah yang meramalkan kelajuan decode?
Lebar jalur memori (memory bandwidth). Bahagikan lebar jalur yang diterbitkan dengan saiz pemberat (weights) dalam memori dan anda akan mendapat had siling aritmetik bagi satu aliran. Kad dengan pengiraan yang lebih tinggi tetapi lebar jalur yang sama tidak akan menstrim dengan lebih pantas. Itulah sebabnya melakukan kuantisasi kepada 8 bit secara kasarnya menggandakan kelajuan decode: ia mengurangkan separuh bait yang dibaca bagi setiap token tanpa menjejaskan pengiraan.
Mengapa throughput meningkat apabila saya menambah pengguna tetapi setiap pengguna berasa lebih perlahan?
Satu bacaan pemberat (weights) menyediakan token untuk setiap urutan dalam batch, jadi jumlah token sesaat meningkat mengikut saiz batch. Setiap token individu kini menunggu langkah yang dikongsi, jadi kependaman (latency) bagi setiap pengguna meningkat pada masa yang sama. Pantau kependaman antara token p99, bukan nombor throughput agregat, dan semak vllm:num_requests_waiting untuk melihat sama ada permintaan sedang beratur dan bukannya sedang berjalan.