SSD Nodes Learn 🎉 VPS $5.50/aydan başlayan
Rehberler Matt ConnorYazan Matt Connor

LLM performansında prefill ve decode farkı nedir?

Prefill islemi hesaplama gucuyle, decode ise bellek bant genisligiyle sinirlidir. Ilk token suresi ve saniyedeki token sayisini ayri olcerek model performansini optimize edin.

Prefill ve decode karşılaştırması

Prefill ve decode ayrımı, self-hosted LLM (büyük dil modeli) kurulumlarında karşılaşılan gecikme sorunlarının çoğunu açıklayan temel unsurdur. Prefill aşaması, tüm istemi tek bir geçişte okur ve işlem gücü (compute) ile sınırlıdır. Decode aşaması ise yanıtı her seferinde tek bir token olacak şekilde yazar ve bellek bant genişliği (memory bandwidth) ile sınırlıdır. İlk token'a ulaşma süresi (time to first token) bir prefill değeridir; saniyedeki token sayısı (tokens per second) ise bir decode değeridir. Her iki aşama da aynı GPU (grafik işlem birimi) üzerinde, aynı ağırlıklarla ve aynı süreç içerisinde çalıştığından, bunları tek bir iş yükü olarak değerlendirmek doğaldır. Ancak bu aşamalar, tek bir cihazı paylaşan iki farklı program gibi davranır. Bu iki süreci birbirinden ayırdığınızda, karmaşık görünen birçok performans sonucu anlam kazanmaya başlar.

Prefill işlemi neden hesaplama (compute) sınırlıdır?

Prefill, tüm istemi (prompt) her katmandan bir kez geçirir. 2.000 token'lık bir istem, her matris çarpımı işlemine 2.000 satırlık iş yükü bindirir; bu nedenle GPU, yüklediği her bir ağırlık baytı için yoğun aritmetik işlem yapar. Taşınan bayt başına düşen aritmetik işlem oranı "aritmetik yoğunluk" olarak adlandırılır ve prefill aşamasında bu oran yüksektir. Cihaz, hesaplama sınırına yakın çalışır ve bellek veri yolu (memory bus) boşta kalır.

Prefill iki çıktı üretir: her istem token'ı için KV cache (anahtar ve değer tensörleri) ve ilk çıktı token'ı. Bu işlem tamamlanana kadar okuyucuya hiçbir veri ulaşmaz; bu nedenle prefill süresi ile ilk token'a kadar geçen süre (TTFT) neredeyse aynı ölçümdür.

Prefill maliyeti, istem uzunluğu arttıkça artar. Doğrusal kısım, katman başına düşen matris işlemleridir. Karesel kısım ise her token'ın kendinden önceki tüm token'lara odaklandığı attention mekanizmasıdır ve uzun bağlamlarda (context) önem kazanır. Bu yüzden istemi iki katına çıkarmak, TTFT süresini en az iki katına çıkarır.

Bunu bir dakika içinde gözlemleyebilirsiniz. Sunucunuza önce 200 token'lık, ardından 2.000 token'lık bir istem gönderin ve her seferinde aynı sayıda çıktı token'ı isteyin. TTFT süresinin hızla arttığını, ancak ilk token'dan sonraki akış hızının neredeyse değişmediğini göreceksiniz.

Decode işlemi neden bellek bant genişliği ile sınırlıdır?

Decode işlemi her adımda bir token üretir. Bu tek token'ı üretmek için GPU'nun modeldeki her bir ağırlığı bellekten okuması, her ağırlığı birkaç işlem için kullanması ve ardından atması gerekir. Aritmetik yoğunluk 1'e yakındır, bu nedenle işlem birimleri zamanlarının çoğunu bekleyerek geçirir.

Decode işlemi yavaştır çünkü her token, tüm modelin bellekten okunmasını gerektirir; bu yüzden bellek veri yolu hızı belirler ve işlem birimleri boşta kalır.

Bu durum, tek akışlı decode hızının tavanını kağıt üzerinde hesaplanabilir bir aritmetik haline getirir. Bellek bant genişliğini, ağırlıkların kapladığı bayt miktarına bölün.

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
  }
]

Bant genişliği sütunu, her satıcının yayınladığı teknik spesifikasyon değerini içerir. Tavan sütunu ise bu değerin, 16 bit hassasiyetle saklanan 8 milyar parametreli bir modelin boyutu olan 16 GB'a bölünmesiyle elde edilir. Bu bir kıyaslama (benchmark) sonucu değil, aritmetik bir hesaptır. Ölçtüğünüz hız bunun altında kalacaktır; bu değerin ne kadar altında olduğunu bilmek, servis yığınınızı mı yoksa donanımınızı mı iyileştirmeniz gerektiğini gösterdiği için faydalıdır.

6 satırı sırasıyla okuduğunuzda örüntü netleşir. Çift kanallı DDR5 kullanan bir CPU yaklaşık 90 GB/s hızla veri taşır ve bu, söz konusu model için decode hızını 6 token/saniye civarında sınırlar. Bir L40S bu değerin yakınında bir sonuç verir. 3350 GB/s yayınlanmış bant genişliğine sahip bir H100 SXM ise 209 civarında bir performans sergiler.

Kuantizasyonun (nicemleme) decode hızı üzerindeki en güçlü etken olmasının nedeni de budur. Aynı modeli 16 bit yerine 8 bit ile sakladığınızda, token başına okunan bayt miktarını yarıya indirirsiniz; böylece tavan hız yaklaşık iki katına çıkar. Hiçbir hesaplama gücü eklemediniz, sadece daha az bellek verisi taşıdınız.

Kendi sunucumda her aşamayı nasıl ölçebilirim?

Ollama, yanıt gövdesinde bölünmüş verileri döndürür. Akışsız (non-streaming) bir tamamlama isteğinde bulunun ve sayaçları okuyun.

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}'

Gerçekten çektiğiniz bir model etiketini kullanın; bunu ollama list size gösterecektir. prompt_eval_count ve prompt_eval_duration ön doldurma (prefill) değerleridir: istem belirteç sayısı ve buna harcanan süre. eval_count ve eval_duration ise kod çözme (decode) değerleridir. Süreler nanosaniye cinsindendir, bu nedenle kod çözme hızı eval_count / eval_duration * 1e9, ön doldurma hızı ise prompt_eval_count / prompt_eval_duration * 1e9 olur. Aynı istek üzerinde ön doldurma hızının, kod çözme hızından çok daha yüksek çıkmasını bekleyin. Buradaki diğer tüm açıklamalar bu farkın nedenini ortaya koyar.

vLLM gibi OpenAI uyumlu bir sunucu için curl, ilk bayt süresini sizin yerinize ölçebilir.

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, gövdenin ilk baytının ulaştığı andır; dolayısıyla "stream": true ile birlikte bu değer TTFT (ilk belirteç süresi) ve bağlantı kurulumunu içerir. Kurulum maliyetini çıkarmak için time_pretransfer değerini çıkarın. Komutu iki kez çalıştırın ve ikinci sonucu baz alın; çünkü ilk çağrı soğuk model yüklemesini içerebilir.

vLLM ayrıca bu ayrımı /metrics adresinde Prometheus metrikleri olarak yayınlar. curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' komutunu çalıştırdığınızda vllm:time_to_first_token_seconds ve vllm:inter_token_latency_seconds histogramlarını elde edersiniz. Kuyruk derinliği için vllm:num_requests_running ve vllm:num_requests_waiting, önbellek baskısı için ise vllm:kv_cache_usage_perc değerlerini ekleyin. Bu beş isim, tüm kontrol panelini oluşturur.

Yük altında vllm bench serve --model <name> --num-prompts 200 --request-rate 4, çalışan sunucuyu yönetir ve ilk belirteç süresi ile çıktı belirteci başına gecikmeyi yüzdelik dilimlerle raporlar; bu, iki aşamanın birbirini nasıl etkilediğini görmenin tek yoludur. Herhangi bir ayar yapmadan önce temiz bir temel değer belirleyin: yerel bir LLM üzerinde saniyedeki belirteç sayısını ölçme yöntemindeki adımlar, yeniden başlatma sonrasında da geçerliliğini koruyan bir temel oluşturmanızı sağlar.

Uzun bir sistem istemi neden ilk belirteci geciktirir ancak akış hızını etkilemez?

Çünkü sistem istemi, yalnızca ön dolum (prefill) işlemidir. İlk belirteç görünmeden önce, istemin geri kalanıyla aynı geçişte bir kez işlenir. Bu geçişten sonra yalnızca KV önbellek girdileri olarak varlığını sürdürür ve kod çözme (decode) işlemi, diğer her şeyle birlikte bunları da okur. Dolayısıyla 3.000 belirteçlik bir sistem istemi, her istekte TTFT süresini artırırken saniye başına düşen belirteç sayısını neredeyse hiç değiştirmez.

Neredeyse, tam olarak değil. Bu fazladan KV girdileri her kod çözme adımında tekrar okunduğu için, çok uzun bir istem kod çözme hızını bir miktar yavaşlatır. Bir sonraki bölüm bunu ele almaktadır.

Çözüm, aynı öneki tekrar hesaplamayı durdurmaktır. Önek önbelleğe alma (prefix caching) özelliğine sahip bir sunucu, paylaşılan bir önekin KV önbelleğini tutar ve yeniden kullanır; böylece aynı sistem istemini taşıyan ikinci istek, ön dolumun o kısmını tamamen atlar. vLLM buna otomatik önek önbelleğe alma adını verir; varsayılan ayarlar sürümler arasında değiştiği için vllm serve --help üzerinden kendi sürümünüzü kontrol edin. GPU içindeki bu KV önbelleği, API sağlayıcısının sizden ücretlendirdiği istem önbelleğinden farklı bir şeydir ve KV önbelleği ile istem önbelleği arasındaki fark, her ikisini de yapılandırmadan önce okunmalıdır.

Kod çözme işlemi bağlam doldukça neden yavaşlar?

Bunun KV önbelleği ile ilgili iki nedeni vardır.

Birincisi bant genişliğidir. Her kod çözme adımında, dikkat mekanizması (attention) önceki her token'ın anahtar ve değerlerini okur. Ağırlıklar, token başına sabit bir maliyete sahiptir. KV önbelleği ise büyüyen bir maliyettir. Boyutunu modelin config.json değerinden hesaplayabilirsiniz: token başına bayt miktarı, 2 çarpı num_hidden_layers, çarpı num_key_value_heads, çarpı kafa boyutu (hidden_size bölü num_attention_heads), çarpı eleman başına bayt sayısıdır. Baştaki 2 çarpanı, bir anahtar ve bir değeri temsil eder.

Yaygın bir 8 milyar parametreli yapı, 32 katman, GQA (gruplandırılmış sorgu dikkati) altında 8 anahtar ve değer kafası, 128 kafa boyutu ve 16 bit hassasiyet için bu değer: 2 x 32 x 8 x 128 x 2 = 131.072 bayt, yani token başına yaklaşık 128 KiB eder. Dolayısıyla 8.000 token'lık bir konuşma, istek başına yaklaşık 1 GB KV önbelleği taşır.

İkincisi kapasitedir. Bu 1 GB'lık alan, ağırlıkları veya başka bir kullanıcının bağlamını tutamayan bellektir. Sunucu, KV havuzunu başlangıçta bir kez boyutlandırır; vLLM üzerinde bu işlem --gpu-memory-utilization aracılığıyla yapılır ve havuz dolduğunda yeni istekler beklemeye alınır. vllm:num_requests_waiting değerinin yükselirken vllm:kv_cache_usage_perc değerinin 1 civarında seyretmesi, tam olarak bu durumun göstergesidir. Bazı yığınlar, kuyruğa almak yerine çalışan bir isteği öncelikli olarak durdurup (preempt) önbelleğini daha sonra yeniden hesaplar; kullanıcı bunu akışın ortasında bir duraksama olarak deneyimler.

Uzun bağlam size iki şekilde maliyet çıkarır: başlangıçta daha fazla ön doldurma (prefill) işi ve yanıtın geri kalanı için token başına daha fazla bellek okuma.

Toplu işlem (batching) verimi neden artırır ve kuyruk gecikmesini (tail latency) neden kötüleştirir?

Kod çözme (decode) işlemi bant genişliği ile sınırlı olduğundan, hesaplama tarafında ek isteklerin maliyeti neredeyse sıfırdır. Ağırlıkların bir kez okunması, toplu işteki her dizi için bir token üretebilir; bu nedenle toplam verim, KV havuzu tükenene veya toplu iş tekrar hesaplama sınırına takılacak kadar büyüyene dek neredeyse doğrusal olarak artar. Sürekli toplu işlem (continuous batching) ise toplu işi her adımda yeniden oluşturur; böylece tamamlanan bir istek çıkar ve kuyruktaki bir istek, komşularını beklemeden işleme dahil olur.

Fatura, yüzdelik dilimlerde ortaya çıkar. Her kullanıcının bir sonraki token'ı artık paylaşılan bir adımın en yavaş kısmını beklemek zorundadır; bu nedenle medyan değer olan p50 kabul edilebilir düzeyde kalırken, 100 istekten en yavaş 1'ini temsil eden p99 değeri uzar. İnsanların fark ettiği p99'dur, çünkü bu, cümlenin ortasındaki duraksamadır.

Ön doldurma (prefill) bu durumu daha belirgin hale getirir. Akışın ortasında gelen büyük bir istem, cihazı uzun bir adım boyunca meşgul eder ve o sırada akış alan herkes bir kesinti yaşar. Parçalı ön doldurma (chunked prefill), uzun bir istemi parçalara bölüp her parçayı kod çözme toplu işlerine karıştırarak bu sorunun büyük kısmını ortadan kaldırır. Ağustos 2026 itibarıyla vLLM V1 motoru bunu varsayılan olarak yapar ve dengeyi --max-num-batched-tokens üzerinden sunar. vLLM ayarlama belgeleri bu ödünleşimi açıkça belirtir: 2048 civarındaki daha küçük değerler, daha az ön doldurma işlemi kod çözme işlemlerini böldüğü için daha iyi token arası gecikme (ITL) sağlar; daha büyük değerler ise daha fazla ön doldurma token'ı tek bir toplu işe sığdığı için daha iyi TTFT (ilk token'a kadar geçen süre) sağlar. Bu tek bayrak, ön doldurma ile kod çözme arasındaki dengedir ve üzerinde oynayabileceğiniz bir sayı olarak sunulur. p99 değerinin ne zaman kabul edilemez hale geldiği bir kapasite sorunudur ve kendi kendine barındırılan bir LLM'in kaç eşzamanlı kullanıcıya hizmet verebileceği konusu aynı metrikler üzerinden incelenir.

Daha büyük bir GPU neden bazen hiçbir şeyi değiştirmez?

Çünkü daha büyük olması genellikle daha fazla işlem gücü anlamına gelir, ancak kod çözme (decode) işlem gücüne ihtiyaç duymaz.

Yukarıdaki tablodaki iki satırı karşılaştırın. A100 80GB, 2039 GB/s'lik yayınlanmış bir bant genişliğine sahipken, L40S 864 GB/s'ye sahiptir ve kod çözme tavanı tam olarak bunu takip eder: saniyede 127 token'a karşılık 54. RTX 4090 çoğu ölçüme göre çok hızlı bir karttır ve 1008 GB/s'lik değeri, tavanını 63 seviyesine koyar. İki kart arasında başka ne farklı olursa olsun, tek akışlı kod çözme işlemi teknik özellik sayfasındaki bant genişliği hattını takip eder.

Dolayısıyla kod çözmeyi hızlandırmanın iki yolu vardır: token başına daha az bayt okumak (ağırlıkları nicemlemek veya daha küçük bir model çalıştırmak) ya da daha fazla bant genişliği satın almak. Prefill durumu ise bunun tam tersidir. İşlem gücü gerektirir, bu nedenle daha hızlı bir kart, uzun istemlerde TTFT süresini gerçekten kısaltır. Eğer şikayet ilk token'ın dört saniye sürmesi ise, daha iyi donanım bunu çözebilir. Eğer şikayet metnin yavaş yazılması ise, donanım değişikliği muhtemelen bunu çözmeyecektir.

Prefill ve decode işlemlerini ayrı çalışanlar (worker) üzerinde mi çalıştırmalısınız?

Büyük sunucu yığınları tam olarak bunu yapar ve bu teknik, prefill ve decode ayrıştırması olarak adlandırılır. Bir çalışan havuzu yalnızca prefill işlemini, ikinci bir havuz ise yalnızca decode işlemini yürütür; ilki tarafından oluşturulan KV cache, hızlı bir ara bağlantı üzerinden ikinciye aktarılır. Bu yöntem, her iki aşamanın farklı donanım ve zamanlama gereksinimleri olması nedeniyle işe yarar. Prefill, hesaplama gücü ve büyük token grupları gerektirir. Decode ise bant genişliği ve aynı anda yürütülen çok sayıda dizi gerektirir. Bunları ayırmak, her havuzun kendi ölçeğinde büyümesini sağlar ve tek bir devasa istemin tüm aktif akışları durdurmasını engeller.

Tek bir GPU'ya sahip tek bir VPS (sanal özel sunucu) üzerinde bunu yapmak neredeyse hiçbir zaman mantıklı değildir. Tek bir cihazı kendi içinde bölmüş olursunuz ve bir işaretçi (pointer) aktarımını, gigabaytlarca cache verisinin ağ üzerinden transferine dönüştürürsünüz. Bu teknik, her aşamaya tüm makineleri adayacak kadar hızlandırıcıya ve her iki havuzu da meşgul edecek kadar sürekli trafiğe sahip olduğunuzda avantaj sağlar. Bunun altındaki seviyelerde, chunked prefill özelliği tek bir bayrak (flag) ile benzer bir izolasyon sağlar.

Değerler hatalı olduğunda ne değiştirilmeli

TTFT süresi çok yüksekse:

  • İstemi kısaltın. Prefill maliyeti istem belirteçlerini (tokens) takip eder ve sistem istemi her istekte ücretlendirilir.
  • Tekrarlanan bir önekin her seferinde değil, yalnızca bir kez hesaplanması için prefix caching özelliğini açın.
  • Her adımda daha fazla prefill işleminin gerçekleşmesi için --max-num-batched-tokens değerini yükseltin.
  • Modeli suçlamadan önce kuyruğu kontrol edin. vllm:num_requests_waiting değerinin sıfırdan büyük olması, isteğin henüz başlamadığını gösterir; bu bir kapasite sorunudur.

Saniye başına üretilen belirteç (tokens per second) sayısı çok düşükse:

  • Ağırlıkları nicemleyin (quantize). Ağırlık başına daha az bayt, belirteç başına daha az bayt okunması demektir.
  • Kartınızın yayınlanan bellek bant genişliğini yukarıdaki tabloyla karşılaştırın ve sınıra ne kadar yakın olduğunuzu görün.
  • Prefill işlemlerinin decode sürecini daha az kesintiye uğratması için --max-num-batched-tokens değerini düşürün.
  • Bağlam uzunluğunu (context length) kontrol edin. Binlerce belirtece ulaşan bir konuşma, her adımda çok daha büyük bir KV önbelleği okur.

Burada çalışma zamanı (runtime) da önemlidir, çünkü Ollama ve vLLM, prefill ve decode işlemlerini farklı şekilde zamanlar; birinde işe yarayan bir ayar diğerinde hiçbir etki yaratmayabilir. Önce her iki aşamada da ölçüm yapın, ardından yalnızca tek bir şeyi değiştirin.

FAQ

İlk token neden saniyeler sürüyor ancak sonrası hızlı akıyor?

Bekleme süresi prefill (ön yükleme), akış ise decode (kod çözme) aşamasıdır. Prefill, herhangi bir çıktı oluşmadan önce tüm istemi (prompt) tek bir hesaplama yoğunluklu geçişte işler; bu nedenle maliyeti istem uzunluğuyla doğru orantılı artar. Decode aşaması ise her adımda bir token üretir ve bu hız bellek bant genişliği tarafından belirlenir; istemin ne kadar uzun olduğundan neredeyse bağımsızdır. Her istekte gönderilen uzun bir sistem istemi genellikle bu gecikmenin nedenidir. Prefix caching, bu maliyetin tekrarlanan kısmını ortadan kaldırır.

Daha uzun bir istem, saniyedeki token hızını yavaşlatır mı?

Bir miktar, ancak TTFT'den (ilk token süresi) farklı bir nedenle. Her decode adımı, önceki tüm token'ların anahtar ve değerlerini (KV cache) okur; bu nedenle daha büyük bir KV cache, token başına daha fazla bayt okunması anlamına gelir. Yaygın bir 8 milyar parametreli model yapısında cache, token başına yaklaşık 128 KiB yer kaplar; yani 8.000 token'lık bir bağlam, her adımda yaklaşık 1 GB verinin işlenmesi demektir. Uzun bir istemin asıl etkisi akış hızından ziyade TTFT üzerindedir.

Hangi GPU özelliği decode hızını belirler?

Bellek bant genişliği. Yayınlanan bant genişliğini bellekteki ağırlıkların boyutuna böldüğünüzde, tek bir akış için teorik üst sınırı elde edersiniz. Daha fazla işlem gücüne sahip ancak aynı bant genişliğine sahip bir kart, daha hızlı akış sağlamaz. 8 bite nicemleme (quantization) yapılmasının decode hızını yaklaşık iki katına çıkarmasının nedeni de budur: işlem gücüne dokunmadan token başına okunan bayt miktarını yarıya indirir.

Kullanıcı eklediğimde toplam iş hacmi neden artıyor ama her kullanıcı yavaşlama hissediyor?

Ağırlıkların tek bir okuması, batch içindeki her dizi için bir token üretilmesini sağlar; bu nedenle saniyedeki toplam token sayısı batch boyutuyla birlikte artar. Ancak her bir token artık paylaşılan bir adımı beklediği için, kullanıcı başına gecikme aynı anda yükselir. Toplam iş hacmi rakamına değil, p99 token arası gecikmeye odaklanın ve isteklerin çalışmak yerine kuyrukta bekleyip beklemediğini görmek için vllm:num_requests_waiting değerini kontrol edin.