Yapay Zeka - AI

vLLM Nedir? PagedAttention ile Production LLM Serving

Ürfet Demirtaş 10 dk okuma

vLLM, açık kaynak bir LLM inference ve serving motorudur. Aynı GPU üzerinde çok sayıda isteği, KV cache’i israf etmeden ve kartı boş bırakmadan cevaplamak için tasarlanmıştır.

Kurumsal tarafta model seçimi uzun uzun konuşuluyor; serving katmanı çoğu zaman “çalıştı, yeter” diye kalıyor. Laptopta Ollama ile tek cevap almak ile aynı ağırlığı API arkasına koyup eşzamanlı kullanıcıya açmak aynı iş değil. İkincisinde GPU’nun hem zamanı hem belleği paylaşılıyor. Darboğaz, checkpoint dosyasının gigabyte’ı kadar, attention sırasında biriken KV cache’in nasıl tutulduğu.

vLLM tam bu katman. UC Berkeley ekibinin 2023 SOSP bildirisi Efficient Memory Management for Large Language Model Serving with PagedAttention üzerine kurulu. Fikir işletim sisteminden tanıdık: KV cache’i sanal bellek gibi sayfalara böl, istekleri her token adımında batch’e alıp çıkar, boşa giden VRAM’i eşzamanlılığa çevir. Bugün kullanılan motor V1. HTTP yüzeyi OpenAI uyumlu olduğu için arkadaki .NET API’nin “bir inference mikroservisi” sözleşmesi değişmiyor. Kod Apache-2.0; indireceğiniz modelin lisansı ayrı konu.

Asıl şişen şey ağırlık değil, KV cache

Transformer bir sonraki token’ı üretirken önceki token’ların key ve value vektörlerine ihtiyaç duyar. Bu vektörler her katmanda durur. İstek sürdükçe büyüyen yapıya KV cache denir.

Naive sunucular bu alanı, isteğin gerçek uzunluğu için değil, modelin görebileceği en uzun sekans için ve mümkünse bitişik bir buffer olarak ayırır. Kullanıcı 40 token yazıp 80 token cevap alsa bile rezervasyon 8K veya 32K üzerinden yapılmış olur. Üstüne parçalanma eklenir. PagedAttention makalesi, dönemindeki sistemlerin KV belleğinin yüzde 60–80 bandını bu yüzden boşa harcadığını yazıyor. Sonuç: aynı kartta daha az eşzamanlı istek, decode sırasında doymayan GPU.

Neden decode doymuyor? İki faz var ve ikisinin faturası farklı.

  • Prefill. Prompt’un tamamı işlenir, KV cache doldurulur, ilk token örneklenir. Uzun prompt’ta compute-bound’dur.
  • Decode. Her adımda tek yeni token vardır; önceki KV hazırdır. Yine de tüm ağırlıklar (ve büyüyen cache) bellekten okunur. Bu faz memory-bandwidth-bound’dur. Tek istek, H100’ün compute’unu doldurmaz; dolduran şey aynı anda yürüyen çok sayıda decode’dur.

Zihinsel hesap, mimariye göre değişir. Klasik bir GQA model düşünün: 32 katman, 8 KV head, head boyutu 128, bf16 (2 byte). Token başına KV kabaca şöyle:

2 (K+V) × 32 katman × 8 KV head × 128 × 2 byte ≈ 128 KB / token

8K bağlam tek istekte 1 GB mertebesine çıkar. 7B bf16 ağırlık zaten ~14 GB tutar; 24 GB kartta KV havuzu daralır. AWQ veya GPTQ ile ağırlık 5–6 GB bandına inince havuz genişler, fakat sayısal alanlarda 4-bit’in sessiz hata üretebildiğini ayrı ölçmek gerekir. DeepSeek tarzı MLA mimarilerinde token başına KV bundan belirgin küçük; formülü her modele yapıştırmayın. Güvenilecek sayı, vLLM’in açılışta yaptığı profiling forward pass’idir: kalan VRAM’e kaç blok sığdığını o tur hesaplar.

PagedAttention

PagedAttention, KV cache’i bitişik bir dizi diye görmez. Sabit boyutlu bloklara böler. Standart transformer katmanında blok varsayılanı 16 token’dır. Her isteğin mantıksal blokları, fiziksel bloklara bir block table ile eşlenir. Tıpkı sanal sayfanın fiziksel kareye eşlenmesi gibi. İki istek bellekte yan yana olmak zorunda değildir.

Kazanç, “en uzun sekans kadar yer ayır” modelinin kalkması. İsraf, sekans sonundaki tam dolmamış son blokla sınırlı kalır; 16 token’lık blokta bu en fazla 15 token’dır. Aynı VRAM’de daha çok eşzamanlı sekans yaşar. Bildirideki değerlendirme, dönemin FasterTransformer ve Orca seviyesindeki sistemlere göre 2–4 kat throughput artışı raporlar. Bu 2023 ölçümüdür; 2026 kartında aynı katsayıyı vaat etmek doğru olmaz. Mekanizma hâlâ aynı.

Sayfa tablosu ikinci bir kapı açar: fiziksel blok paylaşımı. Beam search, paralel sampling veya aynı prefix’i taşıyan istekler, ortak KV bloklarını kopyalamadan referans sayacıyla kullanır. Prefix caching’in oturduğu yer burası.

Continuous batching

Statik batch’te dört istek aynı anda girer, en uzun cevap bitene kadar dördü de GPU’da tutulur. Biri 30 token’da biter, diğeri 400 token sürer. Kısa istek, uzun isteğin kuyruğuna yapışır. Yeni gelen istek de bu turun bitmesini bekler.

Continuous batching (iteration-level scheduling) bunu adım adım çözer. Her engine step’te biten istek batch’ten çıkar, bekleyen istek içeri girer. vLLM’de forward pass sekansları tek bir “süper sekans”ta birleştirir; her sekans yalnızca kendi token’larına bakar. Sağ tarafa padding basıp kısa isteği uzun isteğin boyuna çekmek gerekmez.

Scheduler iki kuyruk tutar: waiting ve running. Politika FCFS veya öncelik olabilir. V1’de aynı adımda prefill ve decode karışabilir. Decode, yani zaten üretmekte olan istekler, önce schedule edilir. Uzun bir prompt’un prefill’i, kısa sohbetlerin token akışını sonsuza kadar kilitlemesin diye.

Bir istek motorun içinde

Offline çağrı kabaca şudur. Servis bunu HTTP’nin arkasında, sürekli tekrarlar.

from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-7B-Instruct", max_model_len=8192)
params = SamplingParams(temperature=0.2, max_tokens=256)
out = llm.generate(["KV cache neden şişer?"], params)
print(out[0].outputs[0].text)

Akış beş duraktan geçer:

  1. Processor. İstek doğrulanır, tokenize edilir, sampling parametreleriyle birlikte engine’e yazılır. Durum WAITING.
  2. Schedule. Token bütçesi kadar iş seçilir. Decode önce, sonra prefill. KV manager her seçilen istek için allocate_slots çağırır. 17 yeni token, 16’lık blokta 2 blok demektir. Havuz yetmezse düşük öncelikli istek bırakılır ve bloklar iade edilir; V1’de tipik yol yeniden hesaplamadır.
  3. Forward. Worker ağırlıkları ve paged attention kernel’lerini çalıştırır. Üretimde decode adımları, açılışta yakalanan CUDA graph ile replay edilir. Kernel launch maliyeti düşer. --enforce-eager bunu kapatır; debug içindir.
  4. Sample. Son pozisyonun logits’inden greedy, temperature, top-p veya top-k ile token seçilir. Guided decoding açıksa, izin verilmeyen token’ların logits’i eksi sonsuza maskelenir.
  5. Postprocess. Token yazılır, detokenize edilir, durma koşuluna bakılır: EOS, max_tokens, stop token, stop string veya model uzunluğu. Biten isteğin blokları, hâlâ prefix cache’te tutulmuyorsa, serbest kuyruğa döner.

Streaming’de ara token’lar bu döngünün içinde istemciye gider. Kullanıcının gördüğü ilk gecikme (TTFT) büyük ölçüde kuyruk + prefill’dir. Sonraki token aralığı (inter-token latency) decode’un bant genişliği ve batch doluluğudur.

Chunked prefill

32 bin token’lık bir sözleşme prompt’u tek adımda prefill edilirse, o adım compute’u monopolize eder. Arkada bekleyen kısa sohbetlerin ilk token’ı gecikir. Chunked prefill, prompt’u token bütçesine göre parçalara böler. İstek birkaç step boyunca KV’sini doldurur; ancak son parçada ilk çıktı token’ı örneklenir. Bu sırada scheduler decode isteklerine öncelik vermeye devam eder.

Pratik sonuç: RAG ve kısa sohbet aynı instance’ta yaşayabilir. Yine de aynı SLA’yı taşımaları gerekmez. Çok büyük hacimde prefill ile decode’u ayrı GPU havuzlarına ayırmak (disaggregated prefill/decode) bir sonraki mimari adımdır. İlk günden kurulan şey değildir; TTFT ile token başına maliyet birbirine girmeye başlayınca masaya gelir.

Prefix caching

Aynı sistem prompt’u, aynı araç tanımı, aynı uzun sözleşme başı her istekte yeniden prefill edilmesin. vLLM blokları hash’ler. Varsayılan blok 16 token. Her tam blok için önceki bloğun hash’i, bu bloğun token’ları ve gerekiyorsa ek metadata (LoRA kimliği, multimodal hash, cache salt) birleşir. İkinci istek aynı zinciri getirirse KV yeniden hesaplanmaz, fiziksel blok referanslanır.

İki incelik var. Hash tam blokta tutulur; prefix blok sınırına denk gelmiyorsa kalan token’lar yine hesaplanır. Ve sıra önemlidir. Sabit talimat başta, değişen kullanıcı metni sonda durursa isabet artar. Her istekte sistem cümlesinin başına saat veya rastgele bir cümle eklemek cache’i bilinçli olarak öldürür.

Çok kiracılı sistemde prefix’in içine müşteri verisi gömülüyorsa paylaşım istenmez. İlk bloğun hash’ine karışan cache salt, bir kiracının KV bloğunun diğerine çarpmasını keser. Güvenlik burada “model cevap vermesin” diye değil, bellek yeniden kullanımının sınırı diye durur.

JSON, spekülasyon, LoRA, kuantizasyon

Servis ayakta kalınca asıl iş bu dört başlıkta toplanır.

Guided decoding

“Yalnızca JSON dön” bir temennidir. Guided decoding, grameri bir durum makinesine çevirir ve her adımda yalnızca o an izinli token’ları açık bırakır. JSON Schema, regex veya sabit bir seçim listesi bu yolla zorlanır. vLLM bu işi xgrammar gibi arka uçlara bırakır. Alan çıkarma, araç çağrısı ve şemalı API cevaplarında prompt’un üstüne bu katman gerekir. Maskeleme ucuz değildir; grammar derlemesi ilk istekte gecikme ekleyebilir. Şemayı sabitleyip ölçün.

Speculative decoding

Küçük bir draft model birkaç token önerir, asıl model bunları tek forward’da doğrular. Kabul oranı yüksekse decode turu azalır, throughput artar. Draft zayıfsa öneri çöpe gider ve fazladan iş yapılmış olur. “Açalım, hızlanır” diye değil, kendi prompt dağılımınızda kabul oranına bakarak açılır.

Çoklu LoRA

Taban model bir kez yüklenir, alan adaptörleri (fatura, sözleşme, destek dili) aynı süreçte ayrı ayrı uygulanır. Her alan için ayrı 70B kopyası tutmaktan ucuzdur. Adaptör KV hash’e girdiği için prefix cache adaptörler arasında karışmaz. Fine-tune kararı hâlâ eval set ile verilir; serving bunu sadece taşır.

Kuantizasyon ve paralellik

AWQ, GPTQ ve FP8, ağırlığı küçültür, KV havuzuna yer açar. FP8 yeni veri merkezi kartlarında, AWQ/GPTQ ise 24 GB sınıfı kartlarda modeli oturtmak için pratik yoldur. Tutar, IBAN, vergi numarası gibi alanlarda agresif 4-bit sessiz hata üretebilir; serbest metin ile aynı toleransa koyulmaz.

Model tek karta sığmıyorsa tensor parallelism ağırlığı GPU’lara böler. Pipeline parallelism katmanları böler. MoE modellerde expert parallelism devreye girer. İletişim gecikmesi bedavaya gelmez: iki zayıf kart, her zaman bir güçlü karttan hızlı değildir.

OpenAI uyumlu servis

Production yüzeyi offline LLM.generate değil, HTTP sunucusudur.

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --api-key degistirin

Docker ile aynı iş:

docker run --gpus all --ipc=host -p 8000:8000 \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  vllm/vllm-openai:latest \
  --model Qwen/Qwen2.5-7B-Instruct \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90 \
  --api-key degistirin

/v1/chat/completions, /v1/completions ve /v1/models OpenAI sözleşmesine yakındır. Embedding ve vision, seçtiğiniz mimari destekliyorsa aynı süreçten çıkar. Sağlık ucu ayakta mı diye bakılmadan bu porta yük verilmez.

Portu internete çıplak açmayın. --api-key en az katmandır; asıl sınır ağdır. --host 0.0.0.0 konteynerin dinlemesi içindir, güvenlik duvarının yerine geçmez.

.NET tarafından bağlamak

Benim tarafta inference, kuyruk işçisinin arkasında duran bir mikroservis. Sözleşme versiyonlu JSON, health check ve cevap başlığında model sürümü. vLLM’in OpenAI yüzeyi bu sözleşmeyi bozmadan taşınabilir kılar.

var request = new
{
    model = "Qwen/Qwen2.5-7B-Instruct",
    temperature = 0.2,
    max_tokens = 512,
    messages = new object[]
    {
        new { role = "system", content = "Yanıtı yalnızca istenen JSON şemasında ver." },
        new { role = "user", content = userText }
    }
};

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiKey);

using var response = await client.PostAsync(
    "http://gpu-box:8000/v1/chat/completions",
    new StringContent(
        JsonSerializer.Serialize(request),
        Encoding.UTF8,
        "application/json"));

response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();

İstemci zaman aşımını prefill süresine göre ayırın. 256 token’lık sohbet ile 30K token’lık doküman aynı HttpClient.Timeout değerine sığmaz. Üretilecek token tavanını şemaya göre kısın; ucu açık max_tokens, KV havuzunu sessizce yer. Model çıktısı ERP’ye doğrudan yazılmaz. Şema, checksum ve güven skoru arada durur.

Hangi işte hangi motor

İhtiyaç Daha doğru adres
Geliştirici makinesi, tek kullanıcı, hızlı deneme Ollama
CPU, edge, GGUF, düşük eşzamanlılık llama.cpp
Açık model, çok eşzamanlı istek, OpenAI API, prefix cache, çoklu LoRA vLLM
NVIDIA’da motor derlemeyi kabul edip çekirdek seviyesinde son hız TensorRT-LLM
Ağır yapılandırılmış üretim, radix tarzı prefix paylaşımı SGLang de masada
Veri dışarı çıkabilir, GPU işletmek istemiyorum Hosted API

Ollama bir PoC aracı olarak hâlâ doğru. Production’da eşzamanlılık, KV yönetimi ve ölçüm istediğiniz gün vLLM veya TensorRT-LLM hattına geçilir. TensorRT-LLM aynı kartta daha yüksek ham hız verebilir; bedeli model ve GPU başına engine derlemektir. vLLM’in tarafı, Hugging Face checkpoint’ini kısa sürede sunmak ve zamanlayıcıyı (prefix, chunked prefill, LoRA, guided decoding) tek yerde tutmaktır.

İlk günden bakılacak knob’lar

  • max-model-len. Kart “128K destekler” diye 128K açmayın. Her eşzamanlı istek bu tavana kadar KV rezervasyonu yarışına girer. Gerçek prompt dağılımınız 8K ise tavanı 8K tutun.
  • gpu-memory-utilization. Açılış profili bu orana göre blok sayısını biçer. Kartta başka süreç varsa düşün. Yüksek tutup OOM almak, düşük tutup KV’siz kalmaktan daha gürültülüdür; ikisini de logda görün.
  • max-num-seqs ve max-num-batched-tokens. Eşzamanlı istek tavanı ile bir adımdaki token bütçesi. Throughput ile TTFT burada trade-off yapar.
  • tensor-parallel-size. Model sığmıyorsa. Sığıyorsa 1 bırakın.
  • Prefix caching. V1’de varsayılan açıktır. Kiracı verisi prefix’teyse salt kullanın.
  • Kuantizasyon. Ancak eval set’te alan bazlı skor düşmüyorsa.

Açılışta OOM alıyorsanız bu bir runtime sürprizi değildir. Ağırlık + aktivasyon + istediğiniz bağlam, seçtiğiniz utilization’a sığmamıştır. max-model-len düşürmek veya kuantizasyon, “bir daha dene”den önce gelir.

Ölçmeden hızlı denmez

Tek bir “saniyede token” sayısı sistemi anlatmaz. Kısa sohbet, uzun RAG ve şemalı JSON üç ayrı profildir. İzlediğim set:

  • TTFT. İlk token. Kuyruk beklemeyi model süresinden ayırın.
  • Inter-token latency. Decode ritmi. p50 ve p95 birlikte.
  • Toplam throughput. Kart başına token/saniye, eşzamanlı istek sayısıyla birlikte.
  • KV doluluk. Blok havuzu bitmeye yakınsa yeni istek preemption’a düşer; latency kuyruğu şişer.
  • Kalite. Hız için açtığınız kuantizasyon, speculative decoding veya daha küçük model, alan bazlı doğruluğu bozuyor mu.

İyileştirme sırası genelde şöyledir: gereksiz bağlamı kes, prefix’i sabitle, max-model-len ile batch tavanını profile’la, sonra kuantizasyon ve speculative decoding. Daha büyük kart, bu sıra bitmeden alınan bir karardır.

Özet

vLLM bir sohbet arayüzü değil, LLM’i üretimde taşıyan motor. PagedAttention KV belleğini sayfalara bölerek eşzamanlılığı artırır. Continuous batching GPU’yu kısa istekler yüzünden boş bırakmaz. Chunked prefill uzun prompt’un herkesi kilitlemesini, prefix caching aynı sistem prompt’unun tekrar tekrar hesaplanmasını keser. OpenAI uyumlu HTTP ise .NET tarafındaki kuyruk ve API sözleşmesini yerinde bırakır.

Modeli indirip cevap almak başlangıç. p95, KV doluluk ve alan bazlı doğruluk aynı anda tutuluyorsa serving tamamdır. Benzer bir hattı on-prem kuruyorsanız, takıldığınız yer kart mı, şema mı, yoksa kuyruk mu; bir sonraki notu oradan yazarım.

Tüm yazılar →