Данила (Dayfing)
Назад к публикациям
3 023 слов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 образов и заново прогоняйте набор оценки при любой смене сервера, файла модели, квантизации или образа.

Ещё публикации