LocalAI ile Çoklu Model Yük Dengeleme
Üretim ortamında birden fazla LLM modeli aynı anda çalıştırmak, beklenmedik sorunların kapısını açar. Bir model isteklere boğulurken diğeri boş oturur, bazı istekler zaman aşımına uğrar, bazıları hiç yanıt alamaz. LocalAI’ın çoklu model yük dengeleme özelliği tam bu noktada devreye giriyor. Yıllar içinde farklı AI altyapıları kurdum ve en çok baş ağrıtan senaryo her zaman “hangi model, hangi isteği alacak?” sorusuydu. Bu yazıda LocalAI ile gerçek bir üretim ortamında çoklu model yük dengelemesini nasıl kurduğumu, hangi hatalarla karşılaştığımı ve nasıl çözdüğümü anlatacağım.
LocalAI’ın Çoklu Model Mimarisi Nasıl Çalışır?
LocalAI, özünde bir REST API sunucusudur ve OpenAI API’siyle uyumlu bir arayüz sunar. Tek bir process olarak çalışmasına rağmen, arka planda birden fazla model yükleyebilir ve bu modeller arasında istek dağıtımı yapabilir. Ancak burada önemli bir ayrım var: LocalAI’ın kendi içindeki model yönetimi ile harici yük dengeleme katmanı arasındaki fark.
LocalAI, her model için ayrı bir “backend” process başlatır. llama.cpp, whisper, stable-diffusion gibi farklı backend’ler aynı anda çalışabilir. Sistem belleği izin verdiği sürece teorik olarak sınırsız model yüklenebilir, ancak pratikte GPU belleği sizi kısıtlar.
Temel mimari şöyle özetlenebilir:
- Model Havuzu: Birden fazla model aynı anda bellekte tutulur
- İstek Yönlendirme: Gelen API istekleri model adına göre doğru backend’e yönlendirilir
- Worker Thread Yönetimi: Her model kendi thread havuzuyla çalışır
- Otomatik Model Yükleme/Boşaltma: Bellek baskısı altında kullanılmayan modeller bellekten atılabilir
Ortamı Hazırlamak
Ben bu kurulumu Ubuntu 22.04 üzerinde, 2x NVIDIA A10G GPU’ya sahip bir sunucuda yaptım. Önce LocalAI’ı Docker ile ayağa kaldırıyorum çünkü bağımlılık yönetimi açısından çok daha sağlıklı:
# GPU destekli LocalAI başlatma
docker run -d
--name localai-primary
--gpus all
-p 8080:8080
-v /opt/localai/models:/build/models:cached
-v /opt/localai/config:/build/config
-e MODELS_PATH=/build/models
-e THREADS=8
-e CONTEXT_SIZE=4096
-e GALLERIES='[{"name":"model-gallery","url":"github:go-skynet/model-gallery/index.yaml"}]'
--restart unless-stopped
quay.io/go-skynet/local-ai:latest-aio-gpu
Birden fazla LocalAI instance’ı çalıştıracaksanız, her birinin farklı portlarda olması gerekiyor. Ben genellikle 8080, 8081, 8082 şeklinde dağıtırım:
# İkinci instance - farklı GPU ve port
docker run -d
--name localai-secondary
--gpus '"device=1"'
-p 8081:8080
-v /opt/localai/models:/build/models:cached
-v /opt/localai/config:/build/config
-e MODELS_PATH=/build/models
-e THREADS=8
--restart unless-stopped
quay.io/go-skynet/local-ai:latest-aio-gpu
Model Konfigürasyonu
Her model için ayrı bir YAML dosyası oluşturmanız gerekiyor. Bu dosyalar /opt/localai/config/ dizininde bulunuyor. LocalAI’ın bu konfigürasyon sistemi başta garip gelebilir, ama öğrendikten sonra çok esnek bir yapı sunduğunu anlıyorsunuz.
# /opt/localai/config/mistral-7b.yaml
name: mistral-7b
parameters:
model: mistral-7b-instruct-v0.2.Q4_K_M.gguf
temperature: 0.7
top_p: 0.9
max_tokens: 2048
threads: 4
gpu_layers: 35
context_size: 4096
f16: true
mmap: true
mmlock: false
low_vram: false
roles:
user: "[INST]"
assistant: "[/INST]"
system: "<<SYS>>"
template:
chat: |
{{.Input}}
# /opt/localai/config/llama3-8b.yaml
name: llama3-8b
parameters:
model: Meta-Llama-3-8B-Instruct.Q5_K_M.gguf
temperature: 0.8
top_p: 0.95
max_tokens: 4096
threads: 4
gpu_layers: 40
context_size: 8192
f16: true
mmap: true
roles:
user: "<|start_header_id|>user<|end_header_id|>"
assistant: "<|start_header_id|>assistant<|end_header_id|>"
system: "<|start_header_id|>system<|end_header_id|>"
Nginx ile Temel Yük Dengeleme
En basit yük dengeleme senaryosunda Nginx’i upstream proxy olarak kullanabilirsiniz. Bu yaklaşım round-robin dağıtımı yapar, yani istekler sırayla her instance’a gönderilir:
# /etc/nginx/sites-available/localai-lb
upstream localai_backend {
least_conn; # En az bağlantıya sahip sunucuya yönlendir
server 127.0.0.1:8080 weight=2 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8081 weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 443 ssl;
server_name ai.sirketiniz.com;
ssl_certificate /etc/ssl/certs/ai.crt;
ssl_certificate_key /etc/ssl/private/ai.key;
location /v1/ {
proxy_pass http://localai_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
# Streaming yanıtlar için
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}
location /health {
proxy_pass http://localai_backend/readyz;
access_log off;
}
}
least_conn direktifi burada kritik. Round-robin yerine bunu kullanmanızı şiddetle öneririm çünkü LLM istekleri çok farklı sürelerde tamamlanabilir. Kısa bir soru 2 saniyede bitebilirken, uzun bir doküman özetleme isteği 45 saniye sürebilir. Round-robin’de bir instance onlarca uzun istekle boğulurken diğeri boş kalabilir.
HAProxy ile Gelişmiş Yük Dengeleme
Nginx’in ötesine geçmek isteyenler için HAProxy çok daha granüler kontrol sağlıyor. Sağlık kontrollerini özelleştirebilir, model bazlı yönlendirme yapabilir ve çok daha detaylı metrikler alabilirsiniz:
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 4096
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 10s
timeout client 300s
timeout server 300s
timeout tunnel 3600s
frontend localai_front
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/ai.pem
# Model bazlı ACL tanımları
acl is_llama3 path_reg -i /v1/chat/completions.*llama3
acl is_mistral path_reg -i /v1/chat/completions.*mistral
acl is_embedding path_beg /v1/embeddings
# Yönlendirme kuralları
use_backend embedding_servers if is_embedding
use_backend llama3_servers if is_llama3
use_backend mistral_servers if is_mistral
default_backend general_ai_servers
backend general_ai_servers
balance leastconn
option httpchk GET /readyz
http-check expect status 200
server ai1 127.0.0.1:8080 check inter 10s rise 2 fall 3 weight 100
server ai2 127.0.0.1:8081 check inter 10s rise 2 fall 3 weight 100
server ai3 127.0.0.1:8082 check inter 10s rise 2 fall 3 weight 50 backup
backend embedding_servers
balance roundrobin
server ai1 127.0.0.1:8080 check inter 5s
server ai2 127.0.0.1:8081 check inter 5s
backend llama3_servers
balance leastconn
server ai1 127.0.0.1:8080 check inter 10s
backend mistral_servers
balance leastconn
server ai2 127.0.0.1:8081 check inter 10s
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats auth admin:guclu_sifre_koy
Bu konfigürasyondaki backup parametresi çok önemli. Üçüncü sunucuyu yalnızca diğer ikisi çöktüğünde devreye sokuyoruz, normal trafiği almıyor. Bu sayede daha zayıf bir sunucuyu acil durum yedek olarak tutabiliyorsunuz.
Python ile Akıllı Yük Dengeleme
Bazı senaryolar için daha akıllı bir yönlendirme mantığına ihtiyaç duyuyorsunuz. İstek boyutuna, modelin o anki yüküne veya kullanıcı önceliğine göre yönlendirme yapmak isteyebilirsiniz. Ben bu iş için FastAPI ile küçük bir proxy yazdım:
# smart_lb.py
import asyncio
import httpx
import json
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
import time
from collections import defaultdict
app = FastAPI()
BACKENDS = [
{"url": "http://localhost:8080", "weight": 2, "active_requests": 0, "healthy": True},
{"url": "http://localhost:8081", "weight": 1, "active_requests": 0, "healthy": True},
]
request_counts = defaultdict(int)
async def check_health():
"""Arka planda sürekli sağlık kontrolü yapar"""
while True:
for backend in BACKENDS:
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{backend['url']}/readyz")
backend["healthy"] = resp.status_code == 200
except Exception:
backend["healthy"] = False
await asyncio.sleep(10)
def select_backend(token_count: int = 0):
"""Aktif istek sayısı ve token miktarına göre en uygun backend'i seçer"""
healthy_backends = [b for b in BACKENDS if b["healthy"]]
if not healthy_backends:
raise HTTPException(status_code=503, detail="Tüm AI sunucuları çevrimdışı")
# Weighted least connections algoritması
best = min(
healthy_backends,
key=lambda b: b["active_requests"] / b["weight"]
)
return best
@app.post("/v1/chat/completions")
async def proxy_chat(request: Request):
body = await request.json()
# Token tahminleme (basit yaklaşım)
messages = body.get("messages", [])
estimated_tokens = sum(len(m.get("content", "")) // 4 for m in messages)
backend = select_backend(estimated_tokens)
backend["active_requests"] += 1
try:
is_streaming = body.get("stream", False)
async with httpx.AsyncClient(timeout=300.0) as client:
if is_streaming:
async def stream_generator():
async with client.stream(
"POST",
f"{backend['url']}/v1/chat/completions",
json=body,
headers={"Content-Type": "application/json"}
) as response:
async for chunk in response.aiter_bytes():
yield chunk
return StreamingResponse(
stream_generator(),
media_type="text/event-stream"
)
else:
response = await client.post(
f"{backend['url']}/v1/chat/completions",
json=body
)
return response.json()
finally:
backend["active_requests"] -= 1
@app.on_event("startup")
async def startup():
asyncio.create_task(check_health())
Bu proxy’yi systemd service olarak çalıştırabilirsiniz. Gerçek üretimde bunu Gunicorn veya Uvicorn ile birden fazla worker’la çalıştırın.
İzleme ve Metrik Toplama
Yük dengeleme kurduğunuzda işin yarısı bitti demektir. Asıl mesele neler olduğunu görebilmek. Prometheus ve Grafana ikilisiyle LocalAI metriklerini toplamak için şu konfigürasyonu kullanıyorum:
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'localai-instances'
static_configs:
- targets:
- 'localhost:8080'
- 'localhost:8081'
metrics_path: '/metrics'
scrape_timeout: 10s
- job_name: 'haproxy'
static_configs:
- targets: ['localhost:8404']
metrics_path: '/stats;csv'
LocalAI’ın /metrics endpoint’i Prometheus formatında veri sunuyor. Hangi modelin kaç istek aldığını, ortalama yanıt sürelerini ve hata oranlarını buradan takip edebilirsiniz.
Gerçek Dünya Senaryosu: Aynı Anda Farklı Modeller
Bir müşteri projesinde şu senaryoyla karşılaştım: Kodlama soruları için CodeLlama, genel sohbet için Mistral, Türkçe içerik için ince ayarlı bir model kullanmamız gerekiyordu. Nginx yeterli olmadı çünkü istek body’sini inceleme kapasitesi yok.
Çözüm olarak şu basit router scripti işe yaradı:
#!/bin/bash
# model_router_test.sh - Yük dengelemenin doğru çalışıp çalışmadığını test eder
ENDPOINTS=("http://localhost:8080" "http://localhost:8081")
TEST_PROMPT='{"model":"mistral-7b","messages":[{"role":"user","content":"Merhaba, nasılsın?"}],"max_tokens":50}'
echo "=== LocalAI Yük Dengeleme Testi ==="
echo ""
for endpoint in "${ENDPOINTS[@]}"; do
echo "Endpoint test ediliyor: $endpoint"
# Sağlık kontrolü
health=$(curl -s -o /dev/null -w "%{http_code}" "$endpoint/readyz")
echo " Sağlık durumu: HTTP $health"
# Yanıt süresi testi
start_time=$(date +%s%N)
response=$(curl -s -X POST
-H "Content-Type: application/json"
-d "$TEST_PROMPT"
"$endpoint/v1/chat/completions"
--max-time 60)
end_time=$(date +%s%N)
duration=$(( (end_time - start_time) / 1000000 ))
if echo "$response" | grep -q '"content"'; then
echo " Yanıt durumu: BASARILI"
echo " Yanıt süresi: ${duration}ms"
else
echo " Yanıt durumu: HATA"
echo " Ham yanıt: $response"
fi
echo ""
done
echo "Test tamamlandi."
Sık Karşılaşılan Sorunlar ve Çözümleri
GPU Bellek Sızıntısı: Uzun süre çalıştıktan sonra bazı modeller GPU belleğini serbest bırakmıyor. Bunu çözmek için periyodik container yeniden başlatması ekledim:
# /etc/cron.d/localai-restart
# Her gece 03:00'te kontrollü yeniden başlatma
0 3 * * * root docker restart localai-secondary && sleep 120 && docker restart localai-primary
Model Yükleme Süresi: İlk istek geldiğinde model henüz bellekte olmayabilir. LocalAI’ın preload özelliğini kullanmak bu sorunu çözüyor. Konfigürasyon dosyanıza şunu ekleyin:
# model config'e eklenir
name: mistral-7b
# diğer ayarlar...
preload: true # Sunucu başlarken modeli önceden yükle
Timeout Ayarları: LLM yanıtları beklenmedik kadar uzun sürebilir. Nginx, HAProxy ve uygulamanızdaki timeout değerlerinin tutarlı olması gerekiyor. Nginx’te proxy_read_timeout 300s ayarını mutlaka yapın, varsayılan 60 saniye çoğu LLM isteği için yetersiz.
Streaming Yanıtlarla Proxy Sorunu: HAProxy’nin bazı eski sürümleri Server-Sent Events ile iyi çalışmıyor. tunnel timeout’u 3600s‘e çıkarmak ve option http-server-close direktifini kaldırmak genellikle sorunu çözüyor.
Kapasite Planlaması
Her GPU’nun ne kadar yük kaldırabileceğini bilmek yük dengeleme konfigürasyonunu doğrudan etkiliyor. Basit bir ölçüm scripti:
#!/bin/bash
# capacity_test.sh - Eş zamanlı istek kapasitesini ölçer
ENDPOINT="http://localhost:8080/v1/chat/completions"
CONCURRENT_REQUESTS=(1 2 4 8 16)
test_payload='{"model":"mistral-7b","messages":[{"role":"user","content":"Türkiye hakkında 3 cümle yaz."}],"max_tokens":100}'
for concurrent in "${CONCURRENT_REQUESTS[@]}"; do
echo "Test: $concurrent eş zamanlı istek"
start_time=$(date +%s)
for i in $(seq 1 $concurrent); do
curl -s -X POST
-H "Content-Type: application/json"
-d "$test_payload"
"$ENDPOINT"
--max-time 120
-o /dev/null &
done
wait
end_time=$(date +%s)
duration=$((end_time - start_time))
throughput=$(echo "scale=2; $concurrent / $duration" | bc)
echo " Toplam süre: ${duration}s"
echo " İşleme hızı: ${throughput} istek/saniye"
echo ""
done
Bu testin çıktısına bakarak hangi eş zamanlılık seviyesinde throughput düşmeye başladığını görüp HAProxy weight değerlerinizi ona göre ayarlayabilirsiniz.
Sonuç
LocalAI ile çoklu model yük dengeleme, tek bir doğru yöntemi olmayan, altyapınıza ve iş gereksinimlerinize göre şekillendirmeniz gereken bir kurulum. Nginx basit round-robin için yeterli, HAProxy model bazlı yönlendirme istiyorsanız daha iyi bir seçim, kendi Python proxy’nizi yazmak ise en esnek ama en çok bakım gerektiren yaklaşım.
Pratikte benim tercihim şu: Kademeli başlayın. Önce tek instance çalıştırın, sisteminizin gerçek yükünü ölçün, sonra ikinci instance ekleyin ve Nginx ile yük dağıtın. Gerçekten karmaşık bir yönlendirme mantığına ihtiyaç duyduğunuzda HAProxy’ye geçin. Eğer model seçimini istek içeriğine göre yapmak istiyorsanız o zaman özel bir proxy katmanına yatırım yapın.
En önemli nokta şu: GPU belleği değerli bir kaynak. Kaç model yükleyebileceğinizi değil, kaç modeli aynı anda verimli şekilde çalıştırabileceğinizi hesaplayın. 4 model yükleyip hepsinin yavaş çalışması yerine 2 model yükleyip hızlı ve kararlı çalışması çok daha değerli.
