Данило (Dayfing)
Назад до публікацій
3 020 слів15 хв

Ollama, llama.cpp чи vLLM: який локальний LLM-сервер обрати

Для одного розробника на ноутбуці запускайте Ollama: вона встановлюється за кілька хвилин, завантажує моделі за назвою і піднімає їх на вимогу. Для невеликої команди, Mac, сервера без GPU або споживчої відеокарти беріть llama-server з llama.cpp: він обслуговує GGUF-моделі й дає явний контроль над паралельними слотами, API-ключами та метриками Prometheus. Для production-трафіку на GPU рівня дата-центру запускайте vLLM, у якого continuous batching і PagedAttention тримають високу пропускну здатність із ростом конкурентності. Усі три сервери надають OpenAI-сумісний API, тож можна почати з одного й перейти на інший, змінивши base URL та назву моделі.

Для чого оптимізований кожен сервер

Ollama оптимізована під час до першої відповіді. Вона працює як десктопний застосунок або фоновий сервіс, завантажує моделі за назвою, піднімає модель під час першого запиту та вивантажує її після періоду простою, за замовчуванням через п'ять хвилин, згідно з FAQ Ollama. Кілька змінних середовища OLLAMA_* замінюють тонке налаштування планувальника, і саме в цій простоті весь сенс.

llama.cpp оптимізований під переносимість. Це рушій інференсу на C/C++ поверх бібліотеки ggml з бекендами Metal для Apple Silicon, CUDA, HIP для AMD, Vulkan, SYCL та оптимізованими шляхами для CPU з AVX, AVX2, AVX-512, AMX і ARM NEON. Якщо модель не вміщується у VRAM, він може тримати частину шарів на GPU, а решту в системній пам'яті. Його HTTP-сервер llama-server зводиться до одного бінарника, GGUF-файлу й набору прапорців. У швидкому старті README проєкту тепер показано єдину команду llama serve, а документація llama-server і Docker-образи й далі постачають виконуваний файл llama-server, який використано в цій статті.

vLLM оптимізований під пропускну здатність на прискорювачах. У його README перелічено PagedAttention для керування KV-кешем, continuous batching, chunked prefill, prefix caching, обслуговування кількох LoRA, тензорний і конвеєрний паралелізм та довгий список форматів квантизації. Він підтримує GPU NVIDIA, AMD та Intel, процесори x86, ARM і PowerPC, а інші прискорювачі під'єднуються через апаратні плагіни.

Коротко: Ollama і llama.cpp пасують скромному залізу та кільком одночасним користувачам, а vLLM виправдовує складніше налаштування, коли багато запитів надходить одночасно. Апаратний бік вибору, зокрема обсяг пам'яті для ваг і KV-кешу, розібрано в посібнику із заліза для локальних LLM.

Один OpenAI-клієнт, три base URL

Усі три сервери реалізують /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/models і /v1/responses. Сторінка сумісності Ollama з OpenAI уточнює, що Responses підтримується лише без збереження стану. llama-server додатково пропонує Anthropic-сумісний /v1/messages, а vLLM поряд із маршрутами OpenAI перелічує Anthropic Messages, транскрипцію аудіо та pooling API.

Тримайте вибір сервера в конфігурації, щоб код клієнта не змінювався:

import os
from openai import OpenAI

# Ollama:       http://127.0.0.1:11434/v1
# llama-server: http://127.0.0.1:8080/v1
# vLLM:         http://127.0.0.1:8000/v1
client = OpenAI(
    base_url=os.environ["LLM_BASE_URL"],
    api_key=os.environ.get("LLM_API_KEY", "not-used"),
)

reply = client.chat.completions.create(
    model=os.environ["LLM_MODEL"],
    messages=[{"role": "user", "content": "Explain HTTP 429 in two sentences."}],
    temperature=0.2,
)
print(reply.choices[0].message.content)

OpenAI SDK вимагає рядок ключа, тому передавайте заглушку, якщо сервер ключа не має. Ollama локально ігнорує ключ, а llama-server і vLLM перевіряють його, лише якщо їх запущено з ключем.

Сервери розходяться в полі model. Ollama очікує тег зі своєї бібліотеки, наприклад qwen3:8b. vLLM очікує назву репозиторію Hugging Face або значення --served-model-name. llama-server з однією моделлю обслуговує те, що завантажив, прапорець --alias задає назву, яку повертає /v1/models, а в режимі router назва обирає модель.

Сумісність закінчується на межах специфікації OpenAI. vLLM приймає додаткові параметри на кшталт top_k через extra_body і за замовчуванням застосовує generation_config.json моделі, що може змінити типові значення семплювання, якщо не передати --generation-config vllm. Шаблони чату й токенізатори також можуть відрізнятися в GGUF-конвертації та вихідному чекпоінті, тож перед перемиканням проженіть той самий набір оцінювання на обох серверах, як описано в посібнику з оцінювання AI-агентів.

Формати моделей і звідки беруться ваги

Три сервери читають різні файли, і це часто вирішує питання раніше, ніж продуктивність.

Ollama завантажує моделі зі своєї бібліотеки за назвою, запускає GGUF-репозиторії з Hugging Face командою ollama run hf.co/{user}/{repo}:{quant} та імпортує локальні GGUF-файли або каталоги Safetensors через Modelfile, у якому рядок FROM вказує на ваги. Під час імпорту вона не квантизує GGUF-файли, тому спершу квантизуйте їх інструментами llama.cpp.

llama.cpp читає GGUF. Чекпоінт Hugging Face конвертують скриптом convert_hf_to_gguf.py і квантизують або завантажують готовий GGUF через -hf user/repo:quant. README перелічує цілочисельну квантизацію від 1,5 до 8 біт. Моделям із зором потрібен ще файл проєктора, який -hf завантажує автоматично.

vLLM читає репозиторії моделей Hugging Face з вагами Safetensors, а також квантизовані чекпоінти GPTQ, AWQ, FP8, INT8, INT4 і compressed-tensors. Документація називає підтримку GGUF дуже експериментальною, і ця підтримка переїхала до зовнішнього плагіна vllm-gguf-plugin.

# Ollama: a library model, or a GGUF repository on Hugging Face
ollama pull qwen3:8b
ollama run hf.co/bartowski/Llama-3.2-3B-Instruct-GGUF:Q8_0

# llama.cpp: download a GGUF and serve it
llama-server -hf ggml-org/Qwen3.5-0.8B-GGUF --port 8080

# vLLM: serve a Hugging Face repository
vllm serve Qwen/Qwen3-0.6B --host 127.0.0.1 --port 8000

Конкурентність, батчинг і пам'ять KV-кешу

Конкурентність коштує пам'яті. Кожна активна послідовність зберігає ключі й значення уваги для кожного токена свого контексту, тому пам'ять зростає разом із кількістю одночасних послідовностей, помноженою на їхню довжину, і все це понад ваги. Для стандартного трансформера корисна така оцінка:

KV bytes per token = 2 × layers × kv_heads × head_dim × bytes_per_value
Qwen3-8B, 16-bit cache: 2 × 36 × 8 × 128 × 2 = 147,456 bytes = 144 KiB
One 32,768-token sequence: 144 KiB × 32,768 = 4.5 GiB

Кількість шарів, кількість KV-голів і розмірність голови беруться з config.json моделі. Чотирьом таким послідовностям потрібно 18 GiB кешу ще до врахування ваг. Моделям зі sliding-window або гібридними шарами уваги потрібно менше, тож для них формула дає верхню межу.

Ollama

OLLAMA_NUM_PARALLEL задає, скільки запитів кожна завантажена модель обробляє одночасно, і FAQ вказує типове значення 1. OLLAMA_MAX_LOADED_MODELS за замовчуванням дорівнює кількості GPU, помноженій на три, або трьом під час інференсу на CPU. OLLAMA_MAX_QUEUE за замовчуванням допускає 512 запитів у черзі, після чого сервер відповідає помилкою 503. FAQ також попереджає, що потрібна пам'ять зростає як OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH. Типову довжину контексту різні сторінки документації описують по-різному, і вона залежить від доступної VRAM, тому задавайте її явно й перевіряйте в стовпці CONTEXT виводу ollama ps.

OLLAMA_HOST=127.0.0.1:11434 \
OLLAMA_NUM_PARALLEL=4 \
OLLAMA_CONTEXT_LENGTH=16384 \
OLLAMA_KEEP_ALIVE=30m \
ollama serve

llama-server

llama-server ділить роботу на слоти. Кожен слот тримає один діалог, -np задає кількість слотів, а типове значення -1 означає автоматичний вибір. Continuous batching увімкнено за замовчуванням, тож нові запити приєднуються до поточного батчу між кроками декодування. Якщо кількість слотів обрано автоматично, усі слоти ділять один спільний KV-буфер. Якщо -np задано вручну, спільний буфер за замовчуванням вимкнено і -c ділиться між слотами: -c 32768 -np 4 дає кожному діалогу 8192 токени. Перевірте результат через GET /slots, який показує n_ctx кожного слота.

Вивантаження на GPU задається прапорцем -ngl, що приймає число, auto (за замовчуванням) або all. Якщо модель не вміщується, шари, які лишилися на CPU, упираються в пропускну здатність системної пам'яті, тож частково вивантажена модель працює, але зазвичай генерує токени повільніше.

llama-server -m /models/qwen3-8b-q4_k_m.gguf \
  --host 127.0.0.1 --port 8080 \
  -c 32768 -np 4 -ngl all \
  --api-key-file /etc/llama/api-keys.txt \
  --metrics

vLLM

Continuous batching у vLLM приймає нові послідовності на кожній ітерації декодування, поки є вільні блоки KV-кешу, а PagedAttention виділяє цей кеш блоками фіксованого розміру в міру зростання послідовностей і не резервує максимальну довжину наперед. Під час старту vLLM займає частку пам'яті GPU, задану --gpu-memory-utilization, і перетворює залишок після ваг і робочої пам'яті активацій на блоки KV-кешу. Коли під навантаженням блоки закінчуються, він витісняє частину запитів і перераховує їх, щойно місце звільниться. Це видно за метрикою vllm:num_preemptions і за хвостовою затримкою.

Основні важелі: --max-model-len для найдовшого допустимого контексту, --max-num-seqs для максимальної кількості послідовностей за ітерацію, --max-num-batched-tokens, --gpu-memory-utilization і --tensor-parallel-size для розподілу однієї моделі між кількома GPU. Знизити --max-model-len до того, що справді потрібно застосунку, зазвичай найдешевший спосіб умістити більше одночасних запитів.

export VLLM_API_KEY="$(openssl rand -hex 32)"
vllm serve Qwen/Qwen3-8B \
  --host 127.0.0.1 --port 8000 \
  --max-model-len 32768 \
  --max-num-seqs 64 \
  --gpu-memory-utilization 0.90

Структурований вивід і виклик інструментів

Усі три сервери вміють обмежувати вивід JSON-схемою через поле OpenAI response_format, тому такий запит переноситься між ними без змін:

schema = {
    "type": "object",
    "properties": {
        "severity": {"type": "string", "enum": ["low", "medium", "high"]},
        "summary": {"type": "string"},
    },
    "required": ["severity", "summary"],
    "additionalProperties": False,
}

reply = client.chat.completions.create(
    model=os.environ["LLM_MODEL"],
    messages=[{"role": "user", "content": "Triage this log line: disk /var is 97% full"}],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "triage", "schema": schema},
    },
)

Нативний API Ollama також приймає "json" або схему в полі format, а документація радить повторювати схему в промпті. llama-server перетворює JSON-схеми на свій формат граматик GBNF і приймає готову граматику для інших форм виводу. vLLM підтримує response_format та об'єкт structured_outputs в extra_body з ключами json, regex, choice, grammar і structural_tag. Старі параметри guided_* видалено у v0.12.0, як зазначає сторінка vLLM про structured outputs. Обмежене декодування гарантує синтаксис, але не істинність, а відповідь, обрізана через max_tokens, однаково буде невалідним JSON, тому перевіряйте кожен результат за схемою у своєму коді.

Виклик інструментів працює в усіх трьох серверах, але налаштовується по-різному:

  • Ollama приймає tools в /api/chat і /v1/chat/completions, включно з паралельними викликами, для моделей, чиї шаблони підтримують інструменти.
  • llama-server підтримує інструменти в стилі OpenAI через Jinja-шаблони чату, які увімкнено за замовчуванням. Нотатки llama.cpp про function calling перелічують нативні обробники для кількох сімейств моделей і універсальний резервний обробник, який витрачає більше токенів. Паралельні виклики вимкнено, доки запит не передасть "parallel_tool_calls": true.
  • vLLM потребує --enable-auto-tool-choice і --tool-call-parser, що відповідає сімейству моделі, наприклад hermes для Qwen2.5 або llama3_json для Llama 3.1 і 3.2. Сторінка vLLM про tool calling пояснює, що іменовані функції та tool_choice="required" використовують structured outputs, а режим auto не гарантує, що аргументи коректно розберуться.
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --host 127.0.0.1 --port 8000 \
  --enable-auto-tool-choice --tool-call-parser hermes

Вважайте аргументи інструментів недовіреним вводом. Їх обирає модель, а текст усередині знайденого документа може вплинути на цей вибір, як пояснює посібник з prompt injection і безпеки MCP.

Запуск у Docker з доступом до GPU

На Linux з GPU NVIDIA спершу встановіть NVIDIA Container Toolkit, щоб працював --gpus. Команди нижче повторюють задокументовані образи й прапорці кожного проєкту з однією зміною: кожен порт публікується лише на 127.0.0.1. Документація Docker про публікацію портів зазначає, що порти без адреси хоста публікуються на всіх адресах хоста, а нотатки Docker про файрвол додають, що трафік опублікованих портів перенаправляється раніше, ніж його побачать правила ufw.

# Ollama with NVIDIA GPUs
docker run -d --name ollama --gpus=all \
  -v ollama:/root/.ollama \
  -p 127.0.0.1:11434:11434 \
  ollama/ollama

# llama-server, CUDA build
docker run -d --name llama --gpus all \
  -v /srv/models:/models:ro \
  -p 127.0.0.1:8080:8080 \
  ghcr.io/ggml-org/llama.cpp:server-cuda \
  -m /models/qwen3-8b-q4_k_m.gguf --host 0.0.0.0 --port 8080 -ngl all

# vLLM with NVIDIA GPUs
docker run -d --name vllm --runtime nvidia --gpus all --ipc=host \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --env "HF_TOKEN=$HF_TOKEN" \
  -p 127.0.0.1:8000:8000 \
  vllm/vllm-openai:latest \
  --model Qwen/Qwen3-8B

Усередині контейнера сервер має слухати 0.0.0.0, інакше опублікований порт до нього не дістанеться. Приватним його робить 127.0.0.1 на боці хоста. У задокументованій команді vLLM є --ipc=host, бо PyTorch обмінюється даними між процесами через спільну пам'ять, особливо під час тензорно-паралельного інференсу. Для GPU AMD використовуйте ollama/ollama:rocm з --device /dev/kfd --device /dev/dri, образи llama.cpp server-rocm або server-vulkan чи vllm/vllm-openai-rocm. У production фіксуйте теги або digest образів замість latest.

Безпека: локальна прив'язка й автентифікація на проксі

Ставтеся до порту інференсу як до порту бази даних. Будь-хто, хто до нього дістанеться, може витрачати ваш час GPU, читати все, що повертає модель, а в деяких конфігураціях викликати адміністративні маршрути.

Почніть з адреси прив'язки. Ollama за замовчуванням слухає 127.0.0.1:11434, а llama-server слухає 127.0.0.1:8080. vLLM поводиться інакше: якщо --host не задано, його launcher слухає всі інтерфейси, тому поза контейнером завжди передавайте --host 127.0.0.1.

Далі перевірте, що насправді захищає ключ кожного сервера:

  • Локальний API Ollama не має налаштування API-ключа. Будь-хто, хто дістається порту, може ним користуватися, тож відкривайте доступ лише через проксі з автентифікацією.
  • llama-server перевіряє --api-key або --api-key-file на своєму API, а /health навмисно лишається публічним. У README зазначено, що CORS за замовчуванням віддзеркалює будь-який Origin, і для локальних мереж рекомендовано --cors-origins.
  • --api-key у vLLM покриває лише кілька префіксів шляхів, зокрема /v1. Посібник vLLM з безпеки перелічує незахищені маршрути, наприклад /invocations, який веде до тих самих функцій інференсу, а також /pause чи /abort_requests, і радить зворотний проксі зі списком дозволених ендпоінтів.

Невеликий фронтенд на Nginx закриває обидві потреби. Він завершує TLS, перевіряє bearer-токен, пропускає лише /v1/ і повертає 404 на все інше.

map $http_authorization $llm_client {
    default "";
    "Bearer REPLACE_WITH_A_LONG_RANDOM_TOKEN" "team";
}

server {
    listen 443 ssl;
    server_name llm.example.internal;
    ssl_certificate     /etc/nginx/tls/llm.crt;
    ssl_certificate_key /etc/nginx/tls/llm.key;
    client_max_body_size 10m;

    location /v1/ {
        if ($llm_client = "") { return 401; }
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_read_timeout 600s;
    }

    location / {
        return 404;
    }
}

proxy_buffering off дає змогу потоковим токенам доходити до клієнта в міру генерації. Якщо upstream-сервер має власний ключ, задайте той самий токен, щоб запит в обхід проксі однаково не пройшов. Для Ollama спрямуйте proxy_pass на порт 11434 і додайте proxy_set_header Host localhost:11434, як у прикладі з FAQ. Додайте limit_req, якщо один GPU ділять кілька користувачів. На ноутбуці розробника пам'ятайте, що OLLAMA_ORIGINS=* дозволяє будь-якій відкритій вебсторінці звертатися до локального сервера через ваш браузер.

Моніторинг і перевірки працездатності

Почніть з готовності. /health у llama-server повертає 503, поки модель завантажується, і 200, коли вона готова, а vLLM також надає /health. Використовуйте їх для health check контейнерів і проб балансувальника, але не як доказ того, що якість генерації в порядку.

vLLM за замовчуванням віддає метрики Prometheus на /metrics. Сторінка vLLM про метрики перелічує vllm:num_requests_running, vllm:num_requests_waiting, vllm:kv_cache_usage_perc, гістограми часу до першого токена, затримки між токенами й повної затримки запиту, а також лічильник витіснень. llama-server віддає /metrics лише після запуску з --metrics, зокрема llamacpp:requests_processing, llamacpp:requests_deferred, llamacpp:prompt_tokens_seconds і llamacpp:predicted_tokens_seconds, а GET /slots показує стан кожного слота.

scrape_configs:
  - job_name: vllm
    static_configs:
      - targets: ["127.0.0.1:8000"]
  - job_name: llama-server
    static_configs:
      - targets: ["127.0.0.1:8080"]

Задокументований API Ollama не має ендпоінта Prometheus. Використовуйте GET /api/ps, щоб бачити завантажені моделі, їхнє споживання VRAM, довжину контексту й час вивантаження, і читайте поля з таймінгами в кожній відповіді. Тривалості вказано в наносекундах, тому швидкість генерації дорівнює eval_count / eval_duration × 10^9 токенів за секунду:

curl -s http://127.0.0.1:11434/api/generate \
  -d '{"model": "qwen3:8b", "prompt": "Say ready.", "stream": false}' |
  jq '{tokens: .eval_count, tokens_per_second: (.eval_count / .eval_duration * 1e9)}'

Налаштовуйте алерти на сигнали того, що користувачі чекають: черга waiting або deferred, яка не спадає до нуля, використання KV-кешу близько 1, зростання лічильника витіснень і збільшення часу до першого токена на p95. Метрики сервера описують ємність. Вони не скажуть, який промпт, виклик інструмента чи крок пошуку сповільнив запит, тому пов'язуйте їх із трасуванням запитів, як описано в посібнику зі спостережуваності AI-агентів.

Порівняння в одній таблиці

Ollama llama.cpp llama-server vLLM
Оптимізований для Швидкого встановлення й керування моделями Переносимості на CPU, Apple Silicon і споживчі GPU Пропускної здатності на GPU дата-центрів
Формати моделей Моделі з бібліотеки, GGUF, імпорт Safetensors GGUF Safetensors з Hugging Face, GPTQ, AWQ, FP8 та інші; GGUF експериментально
Адреса за замовчуванням 127.0.0.1:11434 127.0.0.1:8080 Усі інтерфейси, порт 8000
Вбудований API-ключ Немає --api-key, --api-key-file --api-key або VLLM_API_KEY, обмежені префікси шляхів
Конкурентність OLLAMA_NUM_PARALLEL на модель, типово 1 Слоти (-np) з continuous batching Continuous batching з PagedAttention
Структурований вивід format, response_format response_format, JSON-схема, GBNF response_format, structured_outputs
Виклик інструментів tools у нативних і OpenAI-маршрутах Jinja-шаблони, нативні або універсальні обробники --enable-auto-tool-choice з парсером
Метрики /api/ps і таймінги відповідей /metrics з --metrics, /slots /metrics за замовчуванням
Найкраще підходить Одному розробнику, прототипам Невеликим командам, скромному або змішаному залізу Багатьом одночасним користувачам, production SLO

Шлях вибору для розробки, команди й production

Пройдіть запитання по черзі й зупиніться на першій однозначній відповіді.

  1. Моделі пробує одна людина на ноутбуці чи робочій станції? Використовуйте Ollama. Рухайтеся далі, коли знадобляться метрики, автентифікація або контроль на рівні окремого запиту.
  2. Ви працюєте на Mac, сервері без GPU або споживчій відеокарті, у VRAM якої модель ледь уміщується чи не вміщується зовсім? Використовуйте llama-server. Квантизація GGUF і часткове вивантаження дають йому змогу працювати там, де vLLM не зможе завантажити модель.
  3. Це невелика команда з кількома одночасними користувачами на одній машині? Використовуйте llama-server з -np, що дорівнює піковій конкурентності, API-ключем, --metrics і проксі. Ollama теж підійде, якщо всі користуються однією-двома моделями, а автентифікацію виконує проксі.
  4. Очікується стабільний конкурентний трафік, цільові затримки, кілька GPU або формати квантизації для GPU на кшталт FP8 чи AWQ? Використовуйте vLLM, прив'язаний до localhost за шлюзом, із зафіксованими образами та збором метрик Prometheus від першого дня.

Багато команд зрештою використовують два сервери: Ollama або llama-server на машинах розробників, vLLM у staging і production та той самий код OpenAI-клієнта всюди. Це працює лише тоді, коли набір оцінювання проганяється на production-артефакті, бо GGUF-квантизація і FP8-чекпоінт однієї моделі на практиці поводяться як різні моделі. Посібник з архітектури production AI-агента описує шлюз, тайм-аути, повтори й резервні варіанти, які мають стояти перед будь-яким із цих серверів.

Чеклист розгортання

  • Оберіть сервер за шляхом вибору й зафіксуйте точний файл моделі, квантизацію та шаблон чату, які обслуговуватимете.
  • Розрахуйте пам'ять KV-кешу для пікової конкурентності й максимального контексту, потім налаштуйте OLLAMA_CONTEXT_LENGTH, -c і -np або --max-model-len і --max-num-seqs.
  • Прив'язуйте сервери до 127.0.0.1, явно передавайте --host 127.0.0.1 у vLLM і публікуйте порти Docker як 127.0.0.1:port:port.
  • Винесіть TLS, автентифікацію, список дозволених ендпоінтів і обмеження частоти запитів у зворотний проксі. Ніколи не відкривайте порт інференсу без автентифікації.
  • Задайте ключ у llama-server або vLLM як другий рівень захисту й не залишайте його в історії shell і шарах образу.
  • Збирайте /metrics з llama-server і vLLM та опитуйте /api/ps Ollama через приватну мережу.
  • Проведіть навантажувальний тест на власних промптах за очікуваної конкурентності й запишіть час до першого токена, токени за секунду, довжину черги та пам'ять.
  • Перевіряйте структурований вивід за схемою в коді й вважайте аргументи інструментів недовіреним вводом.
  • Фіксуйте версії та digest образів і повторно проганяйте набір оцінювання після будь-якої зміни сервера, файлу моделі, квантизації чи образу.

Інші публікації