vLLM: высокопроизводительный инференс LLM для production

opensourceitaivllmllmproduction
← Back to Blog

Введение: Когда локальная LLM нужна не для прототипа, а для тысяч пользователей

Ollama идеальна для разработки и прототипирования. llama.cpp — для встраивания в приложения. Но что если вам нужно обслуживать 1000+ запросов в секунду с локальной LLM? Что если latency должна быть предсказуемой, а throughput — максимальным?

Для этого создан vLLM — open-source движок инференса, разработанный в UC Berkeley. Он использует PagedAttention, speculative decoding и продвинутый менеджмент памяти для достижения throughput, который в 24x выше, чем у базовых реализаций.

В этой статье — полное руководство по vLLM: от установки до production-развёртывания, с бенчмарками, настройками и сравнением с альтернативами.

Что такое vLLM и почему он быстрее всех

vLLM — это движок для быстрого инференса и развёртывания LLM, который объединяет несколько ключевых оптимизаций:

  1. PagedAttention — аналог виртуальной памяти из ОС, но для KV-кэшей генерации. Разделяет память эффективно, без фрагментации.
  2. Continuous batching — новые запросы добавляются во время генерации других, а не ждут завершения.
  3. Optimized CUDA kernels — кастомные ядра для операций внимания.
  4. Speculative decoding — использование маленькой модели для предложения токенов, которые проверяются большой.
  5. Streaming decoding — генерация по несколько токенов за итерацию.
# Установка
pip install vllm

# Запуск сервера с Llama 3.1 8B
python -m vllm.entrypoints.api_server --model meta-llama/Llama-3.1-8B-Instruct

# Готово. API на порту 8000.

В чём фишка: vLLM не просто «ещё один сервер для LLM». Он пересматривает саму архитектуру управления памятью при генерации, что даёт кратный прирост throughput без потери качества генерации.

Архитектура: как работает PagedAttention

Проблема традиционного подхода

При генерации текста LLM хранит KV-кэш (key-value cache) для каждого токена. Традиционные системы:

  • Выделяют максимально возможный размер контекста для каждого запроса
  • Не разделяют память между запросами
  • Теряют до 60% памяти на фрагментацию

Решение: PagedAttention

vLLM применяет идеи из операционных систем:

ОС vLLM
Виртуальная память KV-кэш токенов
Страницы (pages) Blocks KV-кэша (16 токенов)
Page Table Маппинг блоков для каждого запроса
Shared memory Shared prefixes для prompt batching
Обычная система:
Request 1: [block1][block2][block3][block4][____][____]  ← 50% потеря
Request 2: [block5][block6][block7][block8][____][____]  ← 50% потеря
Request 3: [block9][block10][block11][block12][____][____] ← 50% потеря

vLLM с PagedAttention:
Request 1: [block1][block2][block3][block4][block13]
Request 2: [block5][block6][block7][block8][block14]
Request 3: [block9][block10][block11][block12][block15]
         ← 0% потерь, идеальная упаковка

Это даёт:

  • До 24x больше throughput по сравнению с базовыми реализациями
  • До 5x меньше памяти для KV-кэша
  • Предсказуемую latency

Установка

Требования

Компонент Минимум Рекомендовано
GPU 1x NVIDIA (8GB VRAM для 7B) 1x A100/H100 для 70B
CUDA 11.8 12.x
Python 3.9 3.11
RAM 16 GB 64 GB
Disk 10 GB 100 GB (для кэша моделей)

Установка через pip

# Базовая установка
pip install vllm

# С конкретной версией PyTorch
pip install vllm --extra-index-url https://download.pytorch.org/whl/cu121

# Установка с Docker (рекомендуется для production)
docker pull vllm/vllm-openai:latest

Проверка установки

python -c "import vllm; print(vllm.__version__)"

# Проверка GPU
python -c "from vllm import LLM; print('vLLM ready')"

Docker-образ

docker run -d --gpus all \
  -p 8000:8000 \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --name vllm-server \
  vllm/vllm-openai:latest \
  --model meta-llama/Llama-3.1-8B-Instruct

Запуск: от одной модели до кластера

Базовый запуск

# Запуск с HuggingFace моделью
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct

# С кастомным weight-путём
python -m vllm.entrypoints.api_server \
  --model /data/models/llama-3.1-8b \
  --host 0.0.0.0 \
  --port 8000

Ключевые параметры запуска

Параметр По умолчанию Описание
--model (required) Имя модели HF или путь
--tensor-parallel-size 1 Количество GPU (tensor parallel)
--gpu-memory-utilization 0.9 Доля VRAM для модели (0-1)
--max-model-len None Максимальная длина контекста
--max-num-seqs 256 Максимум параллельных запросов
--quantization None Метод квантования (awq, gptq, bitsandbytes)
--dtype auto Тип данных (float16, bfloat16, float32)
--swap-space 4 Размер CPU swap для памяти (GB)
--enable-chunked-prefill False Chunked prefill для длинных промптов
--speculative-model None Модель для speculative decoding

Пример: оптимизированный запуск для 8B на A100

python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --tensor-parallel-size 1 \
  --gpu-memory-utilization 0.9 \
  --max-model-len 8192 \
  --max-num-seqs 256 \
  --dtype bfloat16 \
  --enable-chunked-prefill

Пример: 70B на 4x A100

python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4 \
  --gpu-memory-utilization 0.95 \
  --max-model-len 4096 \
  --max-num-seqs 128 \
  --dtype bfloat16

API: OpenAI-совместимый интерфейс

vLLM предоставляет API, полностью совместимый с OpenAI Chat Completions.

Chat Completions

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Llama-3.1-8B-Instruct",
    "messages": [
      {"role": "system", "content": "Ты технический эксперт."},
      {"role": "user", "content": "Объясни, что такое PagedAttention"}
    ],
    "temperature": 0.7,
    "max_tokens": 512
  }'

Stream-режим

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Llama-3.1-8B-Instruct",
    "messages": [
      {"role": "user", "content": "Напиши статью о квантовых вычислениях"}
    ],
    "stream": true,
    "max_tokens": 1024
  }'

Completions API (старый формат)

curl http://localhost:8000/v1/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Llama-3.1-8B-Instruct",
    "prompt": "Преимущества микросервисной архитектуры:",
    "max_tokens": 256,
    "temperature": 0.5
  }'

List Models

curl http://localhost:8000/v1/models
{
  "object": "list",
  "data": [
    {
      "id": "meta-llama/Llama-3.1-8B-Instruct",
      "object": "model",
      "created": 1714432703,
      "owned_by": "meta"
    }
  ]
}

Embeddings (через API)

curl http://localhost:8000/v1/embeddings \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Llama-3.1-8B-Instruct",
    "input": ["Первый текст", "Второй текст"]
  }'

Python SDK: интеграция с приложениями

Использование с OpenAI SDK

from openai import OpenAI

# Указываем на vLLM сервер
client = OpenAI(
    base_url="http://localhost:8000/v1/",
    api_key="token-abc123"  # любое значение
)

# Базовый запрос
response = client.chat.completions.create(
    model="meta-llama/Llama-3.1-8B-Instruct",
    messages=[
        {"role": "user", "content": "Напиши функцию quicksort на Rust"}
    ],
    temperature=0.2,
    max_tokens=512
)

print(response.choices[0].message.content)

Stream-обработка

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1/", api_key="token-abc123")

stream = client.chat.completions.create(
    model="meta-llama/Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "Объясни трансформеры"}],
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

Прямое использование vLLM Python API

from vllm import LLM, SamplingParams

# Инициализация LLM
llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct")

# Параметры генерации
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512,
    stop=["### END"]
)

# Генерация
prompts = [
    "Что такое PagedAttention?",
    "Как работает continuous batching?",
    "Сравните vLLM с Ollama"
]

outputs = llm.generate(prompts, sampling_params)

# Вывод результатов
for prompt, output in zip(prompts, outputs):
    print("=" * 50)
    print(f"Prompt: {prompt}")
    print(f"Output: {output.outputs[0].text}")
    print()

Продвинное использование: кастомные параметры

from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-3.1-70B-Instruct",
    tensor_parallel_size=4,
    gpu_memory_utilization=0.95,
    max_model_len=8192,
    dtype="bfloat16"
)

sampling_params = SamplingParams(
    temperature=0.3,
    top_p=0.85,
    top_k=20,
    max_tokens=1024,
    min_tokens=10,
    stop=["USER:", "ASSISTANT:"],
    presence_penalty=0.1,
    frequency_penalty=0.1,
    repetition_penalty=1.05,
    use_beam_search=False,
    n=1  # количество генераций на запрос
)

# Batch-обработка с разными параметрами
batch = [
    {"prompt": "Код для бинарного поиска на Python", "params": {"temperature": 0.1, "max_tokens": 256}},
    {"prompt": "Креативная история про ИИ", "params": {"temperature": 0.9, "max_tokens": 512}},
]

for item in batch:
    outputs = llm.generate(item["prompt"], SamplingParams(**item["params"]))
    print(outputs[0].outputs[0].text)

Speculative Decoding: ускорение генерации

Speculative decoding использует маленькую «assistant» модель для предложения токенов, которые затем проверяются большой «target» модельой.

# Speculative decoding с LM-ассистентом
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --speculative-model meta-llama/Llama-3.1-8B-Instruct \
  --tensor-parallel-size 4

# Speculative decoding с Medusa-ассистентом
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --speculative-model TheBloke/Llama-2-70B-Medusa \
  --tensor-parallel-size 4

# Speculative decoding с n-gram
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --ngram-prompt-max-len 128

Ускорение

Конфигурация Ускорение
70B + 8B speculative 1.5-2x
70B + Medusa 1.8-2.5x
70B + n-gram 1.2-1.5x

Квантование в vLLM

vLLM поддерживает несколько методов квантования для уменьшения размера модели и ускорения инференса.

AWQ (Activation-aware Weight Quantization)

# Запуск AWQ-модели
python -m vllm.entrypoints.api_server \
  --model TheBloke/Llama-2-7B-AWQ \
  --quantization awq

GPTQ

# Запуск GPTQ-модели
python -m vllm.entrypoints.api_server \
  --model TheBloke/Llama-2-7B-GPTQ \
  --quantization gptq

Bitsandbytes (INT8 / FP8)

# INT8 квантование
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --quantization bitsandbytes \
  --load-bytes 8

# FP8 квантование (H100/A100)
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --quantization fp8

Сравнение квантования

Метод Размер (8B) Качество Скорость Требование
F16 (baseline) 16 GB 100% 1x
INT4 (AWQ/GPTQ) 5 GB 97-99% 1.5-2x Квантованная модель
INT8 (bitsandbytes) 8 GB 99-100% 1.2x bitsandbytes
FP8 8 GB 98-99% 1.3-1.5x Hopper/Ampere GPU

Бенчмарки и производительность

Сравнение throughput: vLLM vs другие движки

Тест: Llama 3.1 8B, batch size = 128, A100-80GB

Движок Throughput (req/s) Latency (p99, ms)
vLLM 3,200 450
Ollama 800 1200
llama.cpp server 650 1500
HuggingFace TGI 2,100 600
Text Generation Inference 2,800 500

Сравнение: vLLM vs другие для 70B моделей

Тест: Llama 3.1 70B, 4x A100, batch size = 64

Движок Throughput (req/s) VRAM usage
vLLM 1,800 65 GB
TGI 1,200 72 GB
Ollama 200 70 GB

Мониторинг метрик

from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.async_llm_engine import AsyncLLMEngine

# vLLM предоставляет встроенные метрики
# Prometheus-совместимые метрики доступны на /metrics

# Основные метрики:
# - vllm:cpu_cache_usage_perc
# - vllm:gpu_cache_usage_perc
# - vllm:prompt_tokens_total
# - vllm:generation_tokens_total
# - vllm:time_to_first_token_seconds
# - vllm:time_per_output_token_seconds
# - vllm:e2e_request_latency_seconds
# - vllm:request_success

Production-развёртывание

Docker Compose

version: '3.8'

services:
  vllm:
    image: vllm/vllm-openai:latest
    container_name: vllm-server
    ports:
      - "8000:8000"
    environment:
      - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
    volumes:
      - model-cache:/root/.cache/huggingface
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    command: >
      --model meta-llama/Llama-3.1-8B-Instruct
      --tensor-parallel-size 1
      --gpu-memory-utilization 0.9
      --max-model-len 8192
      --max-num-seqs 256
    restart: unless-stopped

  # Reverse proxy для балансировки
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - vllm
    restart: unless-stopped

volumes:
  model-cache:

Nginx конфигурация для балансировки

upstream vllm_servers {
    server vllm1:8000;
    server vllm2:8000;
    server vllm3:8000;
}

server {
    listen 80;

    location / {
        proxy_pass http://vllm_servers;
        proxy_set_header Host $host;
        proxy_read_timeout 300s;
        proxy_buffering off;  # Для stream
    }
}

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-server
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        args:
        - --model
        - meta-llama/Llama-3.1-70B-Instruct
        - --tensor-parallel-size
        - "4"
        - --gpu-memory-utilization
        - "0.95"
        resources:
          limits:
            nvidia.com/gpu: 4
        ports:
        - containerPort: 8000
apiVersion: v1
kind: Service
metadata:
  name: vllm-service
spec:
  selector:
    app: vllm-server
  ports:
  - port: 8000
    targetPort: 8000
  type: LoadBalancer

Health Checks

# Проверка здоровья сервера
curl http://localhost:8000/health

# Возвращает:
# {"status": "ok"}

# Проверка доступных моделей
curl http://localhost:8000/v1/models

vLLM как бэкенд для RAG-систем

С LangChain

from langchain_community.llms import VLLM
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import SentenceTransformerEmbeddings
from langchain.chains import RetrievalQA

# vLLM как LLM
llm = VLLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    trust_remote_code=True,  # обязательно для vLLM
    max_tokens=512,
    temperature=0.1
)

# Векторная БД
embeddings = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
vectorstore = Chroma(persist_directory="./rag_db", embedding_function=embeddings)

# RAG
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 4, "score_threshold": 0.65}
)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=retriever,
    chain_type="stuff"
)

answer = qa_chain.invoke("Какие требования к GPU для 70B моделей?")
print(answer["result"])

С FastAPI

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from vllm import LLM, SamplingParams
from typing import List, Optional

app = FastAPI(title="vLLM API Gateway")

# Инициализация
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.9
)

class ChatRequest(BaseModel):
    messages: List[dict]
    temperature: float = 0.7
    max_tokens: int = 512
    stream: bool = False

class ChatResponse(BaseModel):
    content: str
    tokens_used: int

@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    try:
        # Формируем промпт
        prompt = "\n".join(
            f"{m['role']}: {m['content']}" for m in request.messages
        )

        sampling_params = SamplingParams(
            temperature=request.temperature,
            max_tokens=request.max_tokens
        )

        output = llm.generate(prompt, sampling_params)
        content = output.outputs[0].text

        return ChatResponse(
            content=content,
            tokens_used=len(output.outputs[0].token_ids)
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/stats")
async def stats():
    """Метрики сервера"""
    return {
        "gpu_cache_usage": llm.get_cache_block_size(),
        "model": llm.llm_engine.model_config.model
    }

Много-GPU развёртывание

Tensor Parallelism

Для больших моделей (30B+) vLLM использует tensor parallelism — разделение весов модели между GPU.

# 70B модель на 4 GPU
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --tensor-parallel-size 4

# 405B модель на 8 GPU (нужен A100-80GB или H100)
python -m vllm.entrypoints.api_server \
  --model meta-llama/Meta-Llama-3.1-405B-Instruct \
  --tensor-parallel-size 8 \
  --dtype bfloat16

Pipeline Parallelism

# Комбинация tensor + pipeline parallelism
python -m vllm.entrypoints.api_server \
  --model meta-llama/Meta-Llama-3.1-405B-Instruct \
  --tensor-parallel-size 4 \
  --pipeline-parallel-size 2

Распределённый режим (Ray)

# Запуск на нескольких узлах через Ray
export VLLM_HOST_IP=10.0.0.1
export OMP_NUM_THREADS=8

python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-70B-Instruct \
  --distributed-init-method tcp://10.0.0.1:25000 \
  --tensor-parallel-size 4

Тонкая настройка

Оптимизация для низкой latency

# Для чат-ботов с низкой latency
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --max-model-len 2048 \
  --max-num-seqs 512 \
  --enable-chunked-prefill

Оптимизация для высокого throughput

# Для batch-обработки и API
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --max-model-len 8192 \
  --max-num-seqs 1024 \
  --enable-chunked-prefill

Работа с длинным контекстом

# Поддержка 32K контекста
python -m vllm.entrypoints.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.8

Cold Start оптимизация

from vllm import LLM

# Кэширование модели в памяти
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    enforce_eager=False,  # Кэширует CUDA-графы для ускорения
    max_num_batched_tokens=8192,
    max_num_seqs=256
)

# enforce_eager=False: первый запрос медленный (граф), последующие — быстрые
# enforce_eager=True: все запросы одинаковой скорости (без оптимизации графов)

vLLM vs Ollama vs llama.cpp: сравнение

Критерий vLLM Ollama llama.cpp
Установка pip / Docker Одна команда Компиляция
GPU поддержка NVIDIA, AMD (ROCm) NVIDIA, AMD, Apple Metal NVIDIA, AMD, Apple Metal, CPU
Throughput ⭐⭐⭐ Максимальный Средний Средний
Latency Низкая Низкая Очень низкая (CPU)
Мульти-GPU Tensor + Pipeline Нет Нет
Speculative decoding Да Нет Нет
Квантование AWQ, GPTQ, FP8, INT8 GGUF (Q4-Q8) GGUF (Q0-Q8)
API OpenAI-совместимый OpenAI-совместимый OpenAI-совместимый
Production Kubernetes, Docker Docker, systemd Сервер
Лучший для High-throughput production Разработка, прототип Встраивание, edge

Правило выбора:

  • Production API с 100+ RPS? → vLLM
  • Локальная разработка и тесты? → Ollama
  • Встраивание в приложение или CPU-only? → llama.cpp

Типичные проблемы и решения

Проблема 1: «CUDA out of memory»

# Решение: уменьшите gpu_memory_utilization
python -m vllm.entrypoints.api_server \
  --gpu-memory-utilization 0.8

# Или уменьшите max_model_len
--max-model-len 4096

# Или увеличьте swap space
--swap-space 16

Проблема 2: «Slow cold start»

# Решение: используйте enforce_eager=False (по умолчанию)
llm = LLM(
    model="...",
    enforce_eager=False  # Кэширует CUDA графы
)

# Или используйте prefill caching
--enable-chunked-prefill

Проблема 3: «Модель не загружается с HF»

# Решение: используйте TheBloke версии (квантованные)
--model TheBloke/Llama-3.1-8B-Instruct-AWQ

# Или укажите HuggingFace token
export HUGGING_FACE_HUB_TOKEN=hf_xxxxxx

Проблема 4: «API отвечает 503»

# Сервер перегружен. Увеличьте max_num_seqs:
--max-num-seqs 512

# Или добавьте больше GPU через tensor_parallel_size
--tensor-parallel-size 2

Проблема 5: «FP8 не поддерживается»

# FP8 требует GPU архитектуры Hopper (H100) или Ampere (A100)
# Для других GPU используйте:
--quantization awq    # INT4
--quantization gptq   # INT4
--dtype bfloat16      # Если GPU поддерживает BF16

Экосистема vLLM

Инструменты

Инструмент Описание Ссылка
vLLM TGI Bridge Интеграция с HuggingFace TGI github.com/vllm-project
Open WebUI WebUI с поддержкой vLLM github.com/open-webui
LangChain Интеграция с RAG-фреймворками docs.langchain.com
LlamaIndex Интеграция с индексацией docs.llamaindex.ai
Ray Serve Масштабирование через Ray docs.ray.io
Triton Inference Server Деплой через NVIDIA Triton github.com/triton-inference-server

Мониторинг

# Prometheus метрики из vLLM
# endpoint: http://localhost:8000/metrics

# Основные метрики:
# vllm:gpu_cache_usage_perc — использование GPU памяти
# vllm:time_per_output_token_seconds — время на токен
# vllm:e2e_request_latency_seconds — общая latency
# vllm:request_success — успешные запросы

Заключение: vLLM — стандарт для production LLM serving

vLLM решает главную проблему production-инференса: как обслужить тысячи запросов с предсказуемой latency и максимальным throughput.

Ключевые выводы:

  1. PagedAttention — game changer. Пересмотр архитектуры управления памятью даёт кратный прирост.
  2. OpenAI-совместимый API. Любой клиент работает без изменений.
  3. Speculative decoding — для скорости. 1.5-2x