00 LLM Serving Zorlukları
Üretimde LLM çalıştırmak neden bu kadar karmaşık? Bellek yönetimi ve batching verimsizliğinin anatomisi.
Bir LLM'i tek kullanıcı için çalıştırmak zaten zordur; ama aynı modeli yüzlerce eş zamanlı kullanıcıya servis etmek bambaşka bir mühendislik problemidir. İki temel engel öne çıkar: KV cache bellek parçalanması ve batching verimsizliği.
KV Cache Memory Sorunu
Her transformer katmanındaki attention mekanizması, daha önce üretilen token'ların Key ve Value matrislerini hafızada tutmak zorundadır — buna KV cache denir. Sequence uzadıkça bu cache büyür; farklı uzunluktaki sequence'lar için ayrı ayrı bellek bloğu tahsis edilmesi gerekir. Geleneksel sistemlerde bu bellek statik olarak önceden ayrılır; yüksek fragmentasyon ve düşük kullanım oranı kaçınılmazdır.
Problem 40 GB GPU, 7B model ağırlıkları → ~14 GB Kalan 26 GB KV cache için kullanılabilir Statik Her sequence için max_len × layer × head × d_head ayrılır Sonuç Çoğu blok boş, GPU dolmuş görünür — utilization %30-40
Batching Verimsizliği
Static batching'de tüm sequence'ların aynı anda tamamlanması beklenir. Kısa bir yanıt veren kullanıcı, en uzun sequence bitene kadar bekler. GPU o süre boyunca kısmen boş çalışır — padding waste olarak adlandırılan bu kayıp, throughput'u ciddi ölçüde düşürür.
| Sorun | Nedeni | Etkisi | vLLM Çözümü |
|---|---|---|---|
| KV Fragmentasyonu | Statik bellek tahsisi | %60-70 VRAM atıl | PagedAttention |
| Batching Gecikme | Static batch senkronizasyonu | Throughput düşük | Continuous batching |
| TTFT Yüksek | Sıra bekleme | Kötü UX | Preemption + prioritization |
| Memory OOM | Tahmin edilemeyen sequence uzunluğu | Sunucu çökmesi | Dinamik block allocation |
vLLM, orijinal Orca paper'ındaki continuous batching fikrini PagedAttention ile birleştirerek 24× daha yüksek throughput elde eder. Bu sayının arka planını anlamak için önce bu iki kavramı kavramak şarttır.
01 PagedAttention Mimarisi
Sanal bellek ilkesini KV cache'e uyarla — virtual KV cache ve block table ile fragmentasyonu sıfıra indir.
PagedAttention, işletim sistemlerindeki virtual memory paging ilkesini KV cache yönetimine uygular. Fiziksel GPU belleği sabit boyutlu bloklara (block size = 16 token) bölünür. Her sequence için bir block table tutulur; bu tablo sanal blok numaralarını fiziksel bloklara eşler.
Fiziksel GPU VRAM → N adet eşit boyutlu blok (ör. 16 token/blok) Sanal Her sequence kendi sanal blok listesine sahip Tablo block_table[seq_id] = [phys_blk_3, phys_blk_7, phys_blk_12, ...] Tahsis Yeni token üretildikçe boş fiziksel blok talep edilir Serbest Sequence tamamlandığında bloklar hemen geri verilir
Copy-on-Write ile Beam Search
Beam search veya parallel sampling gibi senaryolarda aynı prefix farklı sequence'lar arasında paylaşılabilir. PagedAttention, aynı fiziksel bloğa birden fazla sequence'ın referans vermesine olanak tanır. Bir sequence o bloktaki içeriği değiştirmeye kalkarsa copy-on-write tetiklenir, sadece o an değiştirilen blok kopyalanır.
| Özellik | Geleneksel | PagedAttention |
|---|---|---|
| Bellek tahsisi | max_seq_len önceden ayrılır | Token üretildikçe dinamik |
| Fragmentasyon | Yüksek (%60+) | Neredeyse sıfır (<4%) |
| Prefix paylaşımı | Yok | Copy-on-write ile var |
| Batch boyutu | Bellek sınırlı (küçük) | Büyük batch mümkün |
| OOM riski | Tahmin edilemez | Belirleyici, kontrollü |
Block size seçimi kritiktir. Küçük bloklar (ör. 8 token) daha fazla esneklik sağlar ama block table overhead'i artar. Büyük bloklar (ör. 32 token) overhead'i düşürür ama son blokta iç fragmentasyon oluşur. vLLM varsayılanı 16'dır.
02 Continuous Batching
Static batching'in GPU boşa harcama sorununu her iteration adımında çözüme kavuşturan yaklaşım.
Static batching'de bir batch oluşturulur, tüm sequence'lar tamamlanana kadar beklenir, sonra yeni batch oluşturulur. Bu yaklaşımda bir sequence erken biterse GPU kapasitesi atıl kalır.
Continuous batching'de her token üretim adımında (iteration level) scheduler devreye girer: tamamlanan sequence'lar batch'ten çıkarılır, bekleme kuyruğundaki yeni sequence'lar eklenir. GPU hiçbir zaman boş kalmaz.
Iter 1 Batch = [A(10), B(50), C(30)] token kaldı Iter 2 A tamamlandı → Batch = [B(49), C(29), D(yeni)] Iter 3 Batch = [B(48), C(28), D(devam)] ... Sonuç GPU utilization %90+ (static'te %40-60)
Preemption ve Priority
Bellek yetersiz kaldığında vLLM preemption uygular: düşük öncelikli sequence'ların KV cache'i diske (swap) veya başka GPU'ya taşınır. Bu mekanizma OOM yerine kontrollü performans düşüşü sağlar.
03 vLLM Kurulumu ve Temel Kullanım
vLLM'i CUDA ortamına kur, ilk modeli yükle ve offline/online inference çalıştır.
# CUDA 12.1+ gerektirir
pip install vllm
# Belirli CUDA versiyonu için
pip install vllm --extra-index-url https://download.pytorch.org/whl/cu121
# Docker ile (önerilen production yöntemi)
docker pull vllm/vllm-openai:latest
Offline Inference (LLM sınıfı)
from vllm import LLM, SamplingParams
# Modeli yükle
llm = LLM(
model="meta-llama/Meta-Llama-3-8B-Instruct",
tensor_parallel_size=1, # tek GPU
gpu_memory_utilization=0.90,
max_model_len=4096,
dtype="bfloat16",
)
# Sampling parametreleri
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.95,
max_tokens=512,
stop=["<|eot_id|>"],
)
# Batch inference — tüm prompts tek seferde işlenir
prompts = [
"Transformer mimarisini açıkla:",
"Python'da async/await nedir?",
"Docker container neden kullanılır?",
]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
prompt = output.prompt
generated_text = output.outputs[0].text
print(f"Prompt: {prompt!r}")
print(f"Output: {generated_text!r}")
print("-" * 60)
Chat Template ile Kullanım
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Meta-Llama-3-8B-Instruct")
# Chat formatında mesajlar
conversation = [
{"role": "system", "content": "Sen yardımcı bir asistansın."},
{"role": "user", "content": "vLLM neden bu kadar hızlı?"},
]
# Tokenizer'ın chat template'ini kullan
tokenizer = llm.get_tokenizer()
prompt = tokenizer.apply_chat_template(
conversation,
tokenize=False,
add_generation_prompt=True,
)
outputs = llm.generate([prompt], SamplingParams(max_tokens=256))
print(outputs[0].outputs[0].text)
04 vLLM OpenAI-Compatible API
vLLM'i OpenAI API formatında bir HTTP sunucusu olarak çalıştır — mevcut OpenAI istemcileri sıfır değişiklikle çalışır.
# Sunucuyu başlat
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--served-model-name llama-3-8b
from openai import OpenAI
# vLLM sunucusuna yönlendir
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="token-abc123", # herhangi bir değer
)
# Chat completion
response = client.chat.completions.create(
model="llama-3-8b",
messages=[
{"role": "system", "content": "Kısa ve net yanıt ver."},
{"role": "user", "content": "PagedAttention nedir?"},
],
max_tokens=256,
temperature=0.7,
stream=False,
)
print(response.choices[0].message.content)
import sys
stream = client.chat.completions.create(
model="llama-3-8b",
messages=[{"role": "user", "content": "Bana bir hikaye anlat."}],
max_tokens=512,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
sys.stdout.write(delta.content)
sys.stdout.flush()
print() # newline
Kullanılabilir Endpoint'ler
| Endpoint | Metot | Açıklama |
|---|---|---|
| /v1/chat/completions | POST | Chat formatı (GPT-4 uyumlu) |
| /v1/completions | POST | Legacy text completion |
| /v1/embeddings | POST | Embedding modelleri için |
| /v1/models | GET | Yüklü modelleri listele |
| /health | GET | Sunucu sağlık kontrolü |
| /metrics | GET | Prometheus metrikleri |
05 Tensor Parallelism
Büyük modelleri birden fazla GPU'ya yay — tensor parallelism ile 70B modelleri çalıştır.
Tensor parallelism, model matrislerini farklı GPU'lara böler. Attention head'leri ve MLP katmanları sütun/satır bazında parçalanır. Her GPU hesabın bir bölümünü yapar, sonuçlar all-reduce ile birleştirilir.
# 4 GPU için tensor parallelism
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--tensor-parallel-size 4 \
--pipeline-parallel-size 1 \
--gpu-memory-utilization 0.95 \
--max-model-len 8192 \
--dtype bfloat16
# 8 GPU: tensor × pipeline parallelism
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--tensor-parallel-size 4 \
--pipeline-parallel-size 2 \
--max-model-len 4096
from vllm import LLM
# 4 GPU'ya tensor parallelism
llm = LLM(
model="meta-llama/Meta-Llama-3-70B-Instruct",
tensor_parallel_size=4,
gpu_memory_utilization=0.95,
dtype="bfloat16",
enforce_eager=False, # CUDA graph kullan (daha hızlı)
)
# GPU kullanımını kontrol et
import torch
for i in range(torch.cuda.device_count()):
mem = torch.cuda.memory_allocated(i) / 1024**3
print(f"GPU {i}: {mem:.1f} GB kullanımda")
06 Speculative Decoding
Küçük taslak model ile büyük hedef modelin birlikte çalışması — latency'yi 2-3x düşür.
Speculative decoding'de küçük (draft) bir model hızlıca birkaç token üretir; büyük (target) model bu token'ları paralel olarak doğrular. Hedef model kabul ederse token'lar ücretsiz gelmiş olur, reddettiğinde kendi token'ını üretir.
# Eagle draft model ile speculative decoding
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--speculative-model eagle \
--num-speculative-tokens 5 \
--speculative-draft-tensor-parallel-size 1
# Küçük model ile (harici draft)
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Meta-Llama-3-70B-Instruct \
--speculative-model meta-llama/Meta-Llama-3-8B-Instruct \
--num-speculative-tokens 5 \
--tensor-parallel-size 4
from vllm import LLM, SamplingParams
# Medusa: hedef modele eklenen ek head'ler
llm = LLM(
model="FasterDecoding/medusa-vicuna-7b-v1.3",
speculative_model="[medusa]",
num_speculative_tokens=3,
tensor_parallel_size=1,
)
outputs = llm.generate(
["Python'da decorator nasıl kullanılır?"],
SamplingParams(max_tokens=256, temperature=0.0),
)
print(outputs[0].outputs[0].text)
| Yöntem | Mekanizma | Speedup | Doğruluk Kaybı |
|---|---|---|---|
| Eagle | Autoregressive draft head | 2-3× | Yok (lossless) |
| Medusa | Paralel speculative heads | 1.5-2× | Yok (lossless) |
| Draft model | Ayrı küçük model | 2-4× | Yok (lossless) |
| n-gram | Prompt'tan n-gram tahmin | 1.3-2× | Yok (lossless) |
07 TensorRT-LLM
NVIDIA'nın optimize edilmiş LLM inference kütüphanesi — kernel fusion, in-flight batching ve quantization.
TensorRT-LLM (TRT-LLM), NVIDIA'nın CUDA çekirdeklerini model mimarisine özgü fuse eden, in-flight batching (continuous batching benzeri), multi-GPU desteği ve agresif quantization sunan bir inference framework'üdür. vLLM'den farklı olarak derleme adımı gerektirir; ancak NVIDIA donanımında genellikle daha yüksek throughput sunar.
# Docker önerilen yöntem
docker pull nvcr.io/nvidia/tritonserver:24.01-trtllm-python-py3
# pip install (CUDA 12.x)
pip install tensorrt-llm -U --extra-index-url https://pypi.nvidia.com
# Model derleme (Llama-3 örneği)
python examples/llama/convert_checkpoint.py \
--model_dir /models/llama-3-8b-hf \
--output_dir /models/llama-3-8b-trt-ckpt \
--dtype bfloat16 \
--tp_size 1
trtllm-build \
--checkpoint_dir /models/llama-3-8b-trt-ckpt \
--output_dir /models/llama-3-8b-trt-engine \
--gemm_plugin bfloat16 \
--max_batch_size 32 \
--max_input_len 2048 \
--max_seq_len 4096
import tensorrt_llm
from tensorrt_llm.runtime import ModelRunnerCpp
# Engine'i yükle
runner = ModelRunnerCpp.from_dir(
engine_dir="/models/llama-3-8b-trt-engine",
rank=tensorrt_llm.mpi_rank(),
)
# Batch inference
import torch
input_ids = [
torch.tensor([1, 29871, 13, 450], dtype=torch.int32),
torch.tensor([1, 29871, 13], dtype=torch.int32),
]
outputs = runner.generate(
batch_input_ids=input_ids,
max_new_tokens=256,
temperature=0.7,
end_id=2,
pad_id=2,
)
print(outputs)
vLLM kurulumu daha kolay ve framework-agnostic'tir. TRT-LLM, NVIDIA GPU'larda daha yüksek throughput sağlar ama derleme süreci ve NVIDIA ekosistemi gerektirir. Production'da çoğu ekip vLLM ile başlar, ardından gerekirse TRT-LLM'e geçiş yapar.
08 Quantization: AWQ, GPTQ, INT4/INT8
Model ağırlıklarını sıkıştır — VRAM gereksinimini yarıya indir, throughput'u artır, kaliteyi koru.
# AWQ (Activation-Aware Weight Quantization) — önerilen INT4
pip install autoawq
# Hugging Face'den hazır AWQ model
python -m vllm.entrypoints.openai.api_server \
--model TheBloke/Llama-3-8B-Instruct-AWQ \
--quantization awq \
--dtype half
# GPTQ model
python -m vllm.entrypoints.openai.api_server \
--model TheBloke/Llama-3-8B-Instruct-GPTQ \
--quantization gptq \
--dtype half
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_path = "meta-llama/Meta-Llama-3-8B-Instruct"
quant_path = "llama-3-8b-awq"
# Quantization konfigürasyonu
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4, # INT4
"version": "GEMM",
}
# Model ve tokenizer yükle
model = AutoAWQForCausalLM.from_pretrained(model_path)
tokenizer = AutoTokenizer.from_pretrained(model_path)
# Kalibrasyon verisi ile quantize et
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
print(f"AWQ model kaydedildi: {quant_path}")
| Yöntem | Bit | Bellek (7B) | Kalite Kaybı | Hız |
|---|---|---|---|---|
| FP16 (baseline) | 16 | 14 GB | — | 1× |
| GPTQ INT4 | 4 | 3.5 GB | Hafif | 2-3× |
| AWQ INT4 | 4 | 3.5 GB | Çok hafif | 2-3× |
| INT8 (W8A8) | 8 | 7 GB | Minimal | 1.5-2× |
| FP8 (Hopper+) | 8 | 7 GB | Neredeyse yok | 2× |
09 Throughput Benchmark
tokens/sec, TTFT ve latency percentile — LLM serving performansını doğru ölç.
# vLLM'in dahili benchmark aracı
python benchmarks/benchmark_throughput.py \
--model meta-llama/Meta-Llama-3-8B-Instruct \
--backend vllm \
--num-prompts 1000 \
--input-len 512 \
--output-len 128 \
--tensor-parallel-size 1
# Sonraki çıktıya örnek:
# Throughput: 1423.2 tokens/s
# Request Throughput: 11.1 req/s
import asyncio
import time
import aiohttp
import statistics
async def send_request(session, url, prompt, req_id):
start = time.perf_counter()
first_token_time = None
tokens = 0
async with session.post(url, json={
"model": "llama-3-8b",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 256,
"stream": True,
}) as resp:
async for line in resp.content:
line = line.decode().strip()
if line.startswith("data: ") and line != "data: [DONE]":
if first_token_time is None:
first_token_time = time.perf_counter()
tokens += 1
total_time = time.perf_counter() - start
ttft = first_token_time - start if first_token_time else total_time
return {
"req_id": req_id,
"ttft_ms": ttft * 1000,
"total_ms": total_time * 1000,
"tokens": tokens,
"tps": tokens / total_time,
}
async def benchmark(concurrency=10, total_requests=100):
url = "http://localhost:8000/v1/chat/completions"
prompt = "Makine öğrenmesinde overfitting nedir ve nasıl önlenir?"
async with aiohttp.ClientSession() as session:
semaphore = asyncio.Semaphore(concurrency)
async def bounded_req(i):
async with semaphore:
return await send_request(session, url, prompt, i)
start = time.perf_counter()
results = await asyncio.gather(*[bounded_req(i) for i in range(total_requests)])
elapsed = time.perf_counter() - start
ttfts = [r["ttft_ms"] for r in results]
tpss = [r["tps"] for r in results]
print(f"Toplam süre: {elapsed:.1f}s")
print(f"Request/s: {total_requests/elapsed:.1f}")
print(f"TTFT p50: {statistics.median(ttfts):.0f} ms")
print(f"TTFT p95: {sorted(ttfts)[int(0.95*len(ttfts))]:.0f} ms")
print(f"Throughput p50: {statistics.median(tpss):.0f} tok/s")
asyncio.run(benchmark(concurrency=20, total_requests=200))
| Metrik | Açıklama | İyi Değer (8B, A100) |
|---|---|---|
| TTFT (p50) | İlk token süresi, medyan | < 100 ms |
| TTFT (p99) | İlk token süresi, %99 percentile | < 500 ms |
| Throughput | Saniyede üretilen token | > 1000 tok/s |
| Request/s | Saniyede tamamlanan istek | > 10 req/s |
| GPU Util | GPU kullanım oranı | > 85% |
10 Pratik: Llama-3 vLLM ile Deploy
Llama-3-8B-Instruct'ı vLLM ile production'a al, throughput benchmark yap ve streaming output entegre et.
# 1. Model indir (HuggingFace Hub)
huggingface-cli login
huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct \
--local-dir /models/llama-3-8b
# 2. vLLM sunucusu başlat
python -m vllm.entrypoints.openai.api_server \
--model /models/llama-3-8b \
--served-model-name llama-3-8b \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--enable-prefix-caching \
--disable-log-requests \
--tensor-parallel-size 1 &
# 3. Sağlık kontrolü
curl http://localhost:8000/health
# 4. Hızlı throughput testi
python benchmarks/benchmark_throughput.py \
--model llama-3-8b \
--backend openai \
--endpoint http://localhost:8000/v1 \
--num-prompts 200 \
--request-rate 10
import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
from pydantic import BaseModel
app = FastAPI(title="LLM Proxy")
client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="x")
class ChatRequest(BaseModel):
message: str
system: str = "Sen yardımcı bir asistansın."
max_tokens: int = 512
@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
async def event_generator():
stream = await client.chat.completions.create(
model="llama-3-8b",
messages=[
{"role": "system", "content": req.system},
{"role": "user", "content": req.message},
],
max_tokens=req.max_tokens,
stream=True,
)
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
yield f"data: {delta.content}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
# Çalıştır: uvicorn serve:app --host 0.0.0.0 --port 8080
Llama-3-8B-Instruct + vLLM + tek A100 80GB GPU kombinasyonu tipik olarak 1200-1800 token/s throughput, <80ms TTFT (p50) sağlar. AWQ INT4 quantization ile bu değerler 2000+ token/s'ye ulaşabilir, TTFT ise benzer kalır.