Налаштування балансування навантаження між GPU-інстансами

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.
Показано 1 з 1Усі 1566 послуг
Налаштування балансування навантаження між GPU-інстансами
Середній
від 1 дня до 3 днів
Часті запитання

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1281
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1196
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    901
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1119
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    586
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    853

Налаштування балансування навантаження для GPU-інстанцій

Балансування навантаження між GPU-інстанціями з LLM має нюанси, порівняно із звичайними web-серверами: stateful KV-кеш, тривалі запити (streaming), різна вартість запитів (від одного до тисяч токенів).

Алгоритми балансування для LLM

Round-robin: простий, ігнорує поточне завантаження. Неоптимальний: один довгий запит перевантажує сервер, доки інші простоюють.

Least connections: направляє до сервера з найменшою кількістю активних з'єднань. Найкраще round-robin, але не враховує довжину запитів.

Least pending tokens: направляє до сервера з найменшою кількістю токенів у черзі генерації. Найбільш ефективний для LLM. Реалізується через custom балансувальник.

Nginx upstream з health checks

upstream vllm_cluster {
    # Least connections — базовый вариант
    least_conn;

    server 10.0.1.10:8000 max_fails=3 fail_timeout=30s weight=1;
    server 10.0.1.11:8000 max_fails=3 fail_timeout=30s weight=1;
    server 10.0.1.12:8000 max_fails=3 fail_timeout=30s weight=1;
    server 10.0.1.13:8000 max_fails=3 fail_timeout=30s weight=1;

    keepalive 100;              # persistent connections к backend
    keepalive_requests 1000;
    keepalive_timeout 60s;
}

server {
    listen 443 ssl http2;
    server_name llm-api.internal;

    location /v1/ {
        proxy_pass http://vllm_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # Timeout для длинных streaming ответов
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
        proxy_connect_timeout 5s;

        # Streaming: отключаем буферизацию
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;

        # Circuit breaker
        proxy_next_upstream error timeout http_500 http_502 http_503;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 10s;
    }

    # Active health check (nginx plus) или через отдельный endpoint
    location /health {
        proxy_pass http://vllm_cluster/health;
    }
}

Кастомний балансувальник з pending requests

from fastapi import FastAPI, Request
import httpx
import asyncio

class LLMLeastPendingBalancer:
    def __init__(self, backends: list[str]):
        self.backends = {url: {"pending": 0, "healthy": True} for url in backends}
        self.client = httpx.AsyncClient(timeout=300)

    async def get_backend(self) -> str:
        """Выбираем backend с наименьшим числом pending токенов."""
        healthy = {url: info for url, info in self.backends.items() if info["healthy"]}
        if not healthy:
            raise RuntimeError("No healthy backends")

        # Получаем актуальные метрики
        metrics = await self._fetch_metrics(list(healthy.keys()))

        # Выбираем backend с минимальным queue
        best = min(metrics.items(), key=lambda x: x[1].get("vllm_num_requests_waiting", 0))
        return best[0]

    async def _fetch_metrics(self, backends: list[str]) -> dict:
        tasks = [self._get_backend_queue(url) for url in backends]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return {url: result for url, result in zip(backends, results)
                if not isinstance(result, Exception)}

    async def _get_backend_queue(self, url: str) -> dict:
        response = await self.client.get(f"{url}/metrics")
        # Парсим Prometheus метрики
        for line in response.text.split('\n'):
            if line.startswith('vllm:num_requests_waiting'):
                return {"vllm_num_requests_waiting": float(line.split()[-1])}
        return {"vllm_num_requests_waiting": 0}

    async def forward(self, request: Request) -> httpx.Response:
        backend = await self.get_backend()
        url = f"{backend}{request.url.path}"

        self.backends[backend]["pending"] += 1
        try:
            return await self.client.request(
                method=request.method,
                url=url,
                content=await request.body(),
                headers=dict(request.headers)
            )
        finally:
            self.backends[backend]["pending"] -= 1

app = FastAPI()
balancer = LLMLeastPendingBalancer(["http://gpu1:8000", "http://gpu2:8000", "http://gpu3:8000"])

@app.api_route("/v1/{path:path}", methods=["GET", "POST"])
async def proxy(path: str, request: Request):
    return await balancer.forward(request)

Sticky sessions для context-heavy запитів

Якщо LLM використовує KV-кеш prefix reuse (загальний system prompt), корисно надсилати запити з однаковим prefix на той самий сервер:

def get_backend_by_prefix(prompt: str, backends: list[str]) -> str:
    """Consistent hashing по prefix для максимального cache hit."""
    # Хэш от первых 256 символов (system prompt)
    prefix_hash = hashlib.md5(prompt[:256].encode()).hexdigest()
    # Consistent hashing — одинаковый prefix → один backend
    idx = int(prefix_hash, 16) % len(backends)
    return backends[idx]

Моніторинг розподілу навантаження

Ключові метрики: розподіл RPS по бекендах (має бути рівномірним), queue depth по кожному бекенду, error rate на кожному бекенді. Алерт: один бекенд приймає > 80% трафіку за наявності інших здорових.