Blog'a Dön

Buğra Şıkel

Production’da LLM Sunumlarında Throughput Darboğazı Yaratan 5 Continuous Batching Hatası

Production'da LLM Sunumlarında Throughput Darboğazı Yaratan 5 Continuous Batching Hatası

Giriş

Büyük dil modellerini (LLM) üretim ortamına alırken, salt donanım yatırımının ötesine geçip inference mimarisini optimize etmek şarttır. 2022’de Orca makalesiyle literatüre giren iteration-level scheduling (sektördeki adıyla continuous batching), statik batching’in yarattığı atıl compute kapasitesi problemini çözdü. Ancak vLLM, Text Generation Inference (TGI) veya TensorRT-LLM gibi sunucu motorlarını varsayılan ayarlarıyla ayağa kaldırdığınızda, kağıt üzerinde vadedilen 2000+ token/s throughput değerlerine ulaşmanız pek olası değildir.

Özellikle 4x veya 8x A100/H100 topolojilerinde, RAG (Retrieval-Augmented Generation) veya çok turlu (multi-turn) chat iş yükleri altında sistem bir anda kilitlenebilir, 150ms olan p99 Time-To-First-Token (TTFT) değeriniz saniyeler seviyesine fırlayabilir. Production ortamında Llama-3-70B ve Mixtral 8x7B modellerini binlerce anlık istekle ölçeklendirirken karşılaştığımız darboğazların %80’i, donanım yetersizliğinden değil, continuous batching motorunun yanlış yapılandırılmasından kaynaklanıyordu.

İçindekiler

  • Hata 1: Yanlış PagedAttention Block Size Yapılandırması
  • Hata 2: Preemption Stratejilerinde Recompute vs Swap Trade-off’unu Atlamak
  • Hata 3: Prefix Caching (Prompt Caching) Mekanizmasını İhmal Etmek
  • Hata 4: Max Context Length ile Batched Tokens Değerlerini Dengesiz Bırakmak
  • Hata 5: Event Loop’u Bloke Eden Tokenizer Bottleneck’leri
  • Pratik Öneriler / Production Notları
  • Sık Sorulan Sorular
  • Sonuç

Hata 1: Yanlış PagedAttention Block Size Yapılandırması

Continuous batching’in kalbinde, işletim sistemlerindeki sanal bellek (virtual memory) mimarisinden ilham alan PagedAttention bulunur. KV Cache, ardışık bellek blokları yerine sabit boyutlu page’lere (bloklara) bölünür. Bu blokların boyutu (block size) kritik bir parametredir.

Neden Olur?

Çoğu framework varsayılan block size değerini 16 veya 32 token olarak belirler. Eğer iş yükünüz çok kısa yanıtlar (örn. 5-10 token) üretiyorsa, 32 token’lık bir block size atadığınızda belleğin %60-80’i içsel parçalanma (internal fragmentation) nedeniyle boşa gider. Tam tersi, 8000 token’lık çıktılar üreten bir summarization görevinde block size’ı 8 yaparsanız, PagedAttention block tablosunun (metadata) yönetimi GPU üzerinde VRAM’den ziyade memory bandwidth darboğazı (lookup overhead) yaratır.

Nasıl Tespit Edilir?

vLLM’in sunduğu Prometheus metriklerinden vllm:gpu_cache_usage_perc metrisi %99’a dayanmasına rağmen, vllm:num_requests_running metrisi donanımın teorik limitinin çok altındaysa (örneğin A100 80GB üzerinde sadece 12 request), KV cache parçalanma kurbanı olmuştur.

Nasıl Çözülür?

İş yükünüzün istatistiksel dağılımına (P50, P90 çıktı uzunlukları) bakarak block size’ı ayarlamalısınız. Çıktı token sayısı ortalama 50’nin altındaysa block size 16, çıktılar 1000+ token ise block size 64 hatta 128 kullanılmalıdır.

Gerçek Olay Örneği

Bir e-ticaret botu projesinde, kullanıcıların %90’ına 15-20 token’lık kısa yanıtlar dönülüyordu. Varsayılan --block-size 32 ayarı, her istekte 12-17 token’lık VRAM alanını rezerve edip çöpe atıyordu. --block-size 16 değerine geçiş yaparak aynı H100 GPU üzerinde eşzamanlı işlenen istek (concurrent requests) sayısını 48’den 82’ye çıkardık; throughput %70 artış gösterdi.

Hata 2: Preemption Stratejilerinde Recompute vs Swap Trade-off’unu Atlamak

Continuous batching, kuyrukta bekleyen istekleri işlemek için GPU VRAM’inde yer kalmadığında (KV cache dolduğunda), aktif bir isteği durdurmak (preempt) zorundadır.

Neden Olur?

Framework’ler preempt edilen istekler için iki strateji sunar: Recomputation (KV cache’i tamamen sil, sıra tekrar geldiğinde prefill aşamasını baştan hesapla) ve Swapping (KV cache’i GPU VRAM’den CPU RAM’e aktar, sıra gelince geri yükle). İhtiyaçlarınıza uygun stratejiyi seçmemek p99 latency spike’larına yol açar.

Nasıl Tespit Edilir?

Sisteminiz ani yük (burst) aldığında vllm:num_preemptions_total metrisi artar. Bu sırada kullanıcıların API timeout hataları aldığı görülür. P99 TTFT metrikleriniz aniden 3-4 saniyeden 15-20 saniyeye zıplıyorsa, preemption konfigürasyonunuz iş yükünüzle uyuşmuyordur.

Trade-off Analizi: Swapping mi, Recompute mu?

  • Recompute Kullanın: Eğer input (prompt) token sayınız düşük (örn. < 200 token) ancak output token sayınız yüksekse. Compute-bound olan prefill aşaması, küçük prompt’lar için çok hızlıdır (A100’de birkaç milisaniye). PCIe üzerinden 80GB/s ile veri taşımak yerine baştan hesaplamak (recompute) daha az gecikme yaratır.
  • Swapping Kullanın: Eğer RAG kullanıyorsanız ve input token sayınız 4000-8000 arasındaysa. 8000 token’lık bir prompt’un KV cache’ini baştan hesaplamak devasa bir TFLOPS israfıdır. PCIe Gen4 x16 (64 GB/s) veya Gen5 üzerinden bu cache’i CPU RAM’ine swap’lemek ve geri almak, recompute’dan katbekat düşük p99 gecikmesi sağlar.

Gerçek Olay Örneği

12.000 token context içeren kurumsal bir döküman analiz platformunda, Llama-3-8B kullanırken varsayılan preemption stratejisi (recompute) aktifti. Eşzamanlı istek sayısı 100’ü aştığında, preempt edilen istekler sırası geldiğinde 12k token’ı baştan hesaplıyor, bu da GPU utilization’ı %100’de kilitlerken throughput’u 12 token/s’ye düşürüyordu. --swap-space 32 (32 GB RAM tahsisi) parametresini ekleyerek swap mekanizmasını devreye aldık. P99 decode latency 14 saniyeden 1.2 saniyeye geriledi.

Hata 3: Prefix Caching (Prompt Caching) Mekanizmasını İhmal Etmek

Birçok production sisteminde, her API isteğinin başına eklenen uzun bir “System Prompt” veya RAG mimarisinden gelen sabit bir metin şablonu bulunur.

Neden Olur?

Varsayılan continuous batching ayarlarında her istek birbirinden bağımsız değerlendirilir. Eğer 100 kullanıcı aynı 1500 token’lık sistem prompt’unu tetikliyorsa, bu 1500 token’ın KV cache’i GPU üzerinde 100 kez ayrı ayrı hesaplanır ve VRAM’de 100 kopyası tutulur. Bu, muazzam bir memory bandwidth ve VRAM israfıdır.

Nasıl Tespit Edilir?

Eğer gelen isteklerin Prefill Time (Time-to-first-token) değerleri sürekli yüksek seyrediyorsa ve GPU profil analizinde (örneğin Nsight Systems ile) zamanın çoğunun matris çarpımı (GEMM) operasyonlarında değil, attention hesaplamalarında harcandığını görüyorsanız, prefix caching eksikliğinden şüphelenebilirsiniz.

Nasıl Çözülür?

Radix-Tree tabanlı prefix caching mekanizmasını aktifleştirmek gerekir. Bu özellik, aynı başlangıç dizilimine (prefix) sahip isteklerin KV cache’lerini RAM üzerinde tek bir kopya olarak tutar (zero-copy sharing).

Gerçek Olay Örneği

Müşteri hizmetleri LLM sunumumuzda 800 kelimelik standart bir şirket politikası prompt’u her kullanıcının mesajının başına ekleniyordu. vLLM başlatma script’imize aşağıdaki parametreyi ekledik:

python -m vllm.entrypoints.openai.api_server 
  --model meta-llama/Meta-Llama-3-70B-Instruct 
  --tensor-parallel-size 4 
  --gpu-memory-utilization 0.90 
  --enable-prefix-caching 
  --port 8000

Sonuç: İlk istekte prefill 120ms sürerken, takip eden tüm isteklerde prefill süresi 12ms’ye (sadece farklı olan son birkaç kullanıcı kelimesinin hesaplanma süresi) düştü. Genel sunucu throughput’u %140 artış gösterdi.

Hata 4: Max Context Length ile Batched Tokens Değerlerini Dengesiz Bırakmak

Bir LLM 32.768 token context destekliyor olabilir, ancak production’da bu sınırı varsayılan olarak bırakmak VRAM allocation algoritmalarını zehirler.

Neden Olur?

vLLM ve TGI başlangıçta (startup) GPU belleğinde ne kadar KV cache bloğu oluşturabileceğini hesaplar. Eğer max_model_len değerini 32k olarak bırakırsanız, scheduler en kötü senaryoyu (worst-case) baz alarak her sekans için devasa yer ayırma eğilimine girer (özellikle chunked prefill kapalıysa). Bu da max_num_seqs (eşzamanlı işlenen istek sayısı) değerinin 4-5 gibi komik rakamlara düşmesine neden olur.

Nasıl Tespit Edilir?

Sistem başlatıldığında konsol loglarında ayrılan blok sayısı (# GPU blocks) beklenenden düşükse veya max_num_batched_tokens sınırına çok çabuk ulaşılıyorsa bu darboğazdasınız demektir.

Nasıl Çözülür?

İş yükünüzün asla geçmeyeceği katı bir limit belirleyin. Kullanıcıların %99.9’u 4096 token’ı aşmıyorsa, model desteklese bile limiti burada kesin. Ayrıca max_num_batched_tokens değerini ayarlayarak prefill ve decode fazlarının scheduler içindeki ağırlığını dengeleyin.

Gerçek Olay Örneği

Mistral-7B v0.2 (32k context) modelini 1x A100 40GB üzerinde sunarken batch size 8’i geçmiyordu. Parametreleri iş yükümüz olan Q&A dokümanlarına göre (max 4k token) optimize ettik:

--max-model-len 4096 
--max-num-batched-tokens 8192 
--max-num-seqs 64

Bu konfigürasyon, batch size sınırını 64’e çıkardı. Artık GPU VRAM’in %90’ı fiilen 64 farklı kullanıcının üretimi için eşzamanlı kullanılabiliyordu.

Hata 5: Event Loop’u Bloke Eden Tokenizer Bottleneck’leri

Continuous batching scheduler’ı mikrosaniyeler mertebesinde asenkron kararlar almak zorundadır. Ancak Python’un GIL (Global Interpreter Lock) mimarisi ve event loop yapıları, yanlış tasarlandığında CPU tarafında darboğaz yaratır.

Neden Olur?

Metinleri token’lara (ve geri metne) çeviren Tokenizer, CPU üzerinde çalışır. Eğer detokenization işlemi (özellikle karmaşık regex veya streaming senaryolarında) asyncio event loop ile aynı thread’i bloke ederse, GPU’da hesaplama bitmiş olsa bile token’ın kullanıcıya iletilmesi (veya yeni token için GPU’ya emir verilmesi) 5-10ms gecikir. Eşzamanlı 100 istekte bu gecikmeler kümülatif olarak saniyeleri bulur.

Nasıl Tespit Edilir?

GPU utilization %50-60 bantlarında geziyor, ancak CPU tarafında tek bir core %100 kullanımda sabit kalıyorsa ve throughput beklenen değerlerin yarısındaysa CPU bottleneck yaşıyorsunuzdur. cProfile veya py-spy ile alınan alev grafiklerinde (flame graphs) zamanın detokenize veya asyncio.events._run üzerinde yoğunlaştığı görülür.

Nasıl Çözülür?

HuggingFace’in Rust tabanlı (fast) tokenizer’larını kullanmak zorunludur. Ayrıca API sunucusunun (örn. FastAPI/Uvicorn) workers sayısını artırmak veya doğrudan TGI (tamamı Rust ile yazılmış web framework ve scheduler) gibi thread izolasyonu daha iyi çözümlere geçmek, CPU tarafındaki bu darboğazı ortadan kaldırır.

Gerçek Olay Örneği

Yüksek throughput’lu bir translation servisinde saniyede 1500+ token üretilirken API yanıtlarında kopmalar (503 HTTP Timeout) gözlemlendi. GPU profillerinde her iterasyon arası 8ms’lik boşluklar (idle time) tespit ettik. Sorunun Python’daki HuggingFace Transformers kütüphanesinin varsayılan (yavaş) tokenizer’ı olduğunu fark edip, vLLM’i trust_remote_code=True argümanı olmadan yerleşik Rust tokenizer’larını kullanmaya zorladığımızda aradaki 8ms’lik idle time 0.4ms’ye indi.

Pratik Öneriler / Production Notları

  • Chunked Prefill Aktif Edin: Çok uzun prompt’lar (RAG vb.) decode aşamasındaki aktif isteklerin p99 latency’sini bozmasın diye prompt’u parçalara bölerek işleyen chunked prefill (vLLM 0.4.x sonrası) özelliğini mutlaka test edin.
  • Prometheus Metrik Alarmları: vllm:num_preemptions_total metriği dakikada 5’i geçiyorsa auto-scaling (HPA) mekanizmanız yeni bir replica ayağa kaldırmalıdır.
  • VRAM Marjı: gpu_memory_utilization değerini asla 1.0 yapmayın. Modelin forward pass sırasında ihtiyaç duyduğu geçici aktivasyon bellekleri (activation memory) için her zaman %5 ile %10 arası bir pay bırakın (önerilen: 0.90 – 0.95).

Sık Sorulan Sorular

Continuous Batching’de TTFT (Time-To-First-Token) neden artar?

Batching yapısı gereği, prefill (ilk prompt’u işleme) aşaması çok yoğun compute gücü gerektirir. Eğer sistemde halihazırda decode aşamasında olan 50 istek varken yeni ve büyük bir istek gelirse, GPU kaynakları bölündüğü için TTFT artar. Chunked prefill bu durumu minimize etmek için geliştirilmiştir.

Tensor Parallelism (TP) ile Continuous Batching nasıl etkileşime girer?

TP, modelin ağırlıklarını GPU’lara böler. Continuous batching motoru, ağ trafiğini (NCCL over NVLink) senkronize etmek için tüm GPU’ların aynı anda aynı scheduler adımında (iteration) ilerlemesini zorunlu kılar. Bu yüzden TP > 4 durumlarında PagedAttention blok tahsis metrikleri node’lar arası iletişim maliyeti yaratabilir.

X framework yerine Y kullanmalı mıyım? (vLLM vs TGI vs TensorRT-LLM)

Eğer modeliniz standart mimarilerdense (Llama, Mistral) ve hızlı dağıtım isteniyorsa vLLM (Community desteği geniş). Tamamen NVIDIA donanımına kilitlendiyseniz ve C++ seviyesinde maksimum H100 optimizasyonu (In-flight batching) istiyorsanız TensorRT-LLM. Üretim ortamında Rust’ın bellek güvenliği ve gRPC stream yönetiminden faydalanmak istiyorsanız HuggingFace TGI tercih edilmelidir.

Sonuç

Büyük dil modellerini üretim ortamına almak, model ağırlıklarını bir sunucuya yüklemekten ibaret değildir. Continuous batching, PagedAttention ve KV Cache yönetimi gibi mikro düzeydeki süreçler; block size kararları, preemption stratejileri ve tokenizer thread’lerinin doğru yapılandırılmaması durumunda throughput için bir felakete dönüşebilir.

Mimari kararlarınızı alırken varsayılan ayarları körü körüne kabul etmeyin. Gerçek iş yükünüze uygun prompt uzunluklarını analiz edin, Grafana üzerinden gpu_cache_usage_perc metriklerini izleyin ve sisteminizi memory-bandwidth limiti ile compute limiti arasındaki ince çizgide dengeleyin. Sisteminizin p99 gecikmelerini kontrol altına almak için en kritik adım olan limit testlerinizi (load testing) gecikmeden planlayın.

Bunları da beğenebilirsiniz

Yüksek Hacimli Sensör Verileri İçin AI Destekli Asenkron Kuyruk Yönetimi Stratejileri: Performans ve Verimlilik Optimizasyonu

Bu blog yazısı, IoT ve endüstriyel otomasyon gibi alanlarda yüksek hacimli sensör verilerini işlemek için yapay zeka destekli asenkron kuyruk yönetimi stratejilerini inceliyor. Gecikmeyi azaltma, ölçeklenebilirliği artırma ve kaynak kullanımını optimize etme yöntemlerini keşfedin.

Devamını Oku

Edge Cihazlarda YOLOv8 ile Gerçek Zamanlı Nesne Tespiti: Docker ve NVIDIA Jetson Üzerinde Performans Optimizasyonu

Bu kapsamlı rehberde, YOLOv8 modelini kullanarak NVIDIA Jetson edge cihazlarda gerçek zamanlı nesne tespitini nasıl optimize edeceğinizi öğreneceksiniz. Docker ve TensorRT entegrasyonuyla performansı zirveye taşıyın.

Devamını Oku

PHP ile Sayısal Para Değerinin Okunuşunu Yazdırmak

Merhabalar, bu yazımızda bir para miktarı girildiğinde bunun okunuşunu ya da yazılışı da diyebiliriz, bize çıktı olarak verecek bir php fonksiyonu yazacağız. Türkçe bir şekilde…

Devamını Oku