Локальные модели: llama.cpp, Ollama, vLLM, LM Studio, квантизация, требования к железу
До этого момента модель в наших пайплайнах была чужим HTTP-эндпоинтом: вы платите за токены и не думаете, что происходит внутри. Локальный инференс убирает эту абстракцию — и вместе с ней исчезает магия. Вы вдруг обязаны знать, сколько байт весит один слой, сколько памяти съедает 32-й тысячный токен контекста и почему одна и та же карта выдаёт 140 токенов в секунду в одиночном чате и 2000 — под нагрузкой в сорок параллельных запросов.
Хорошая новость: вся эта физика описывается двумя формулами и одним неравенством. Плохая — большинство статей про «запусти LLM дома» их не приводит, из-за чего люди покупают не то железо и удивляются результату.
Зачем вообще локально: четыре честные причины и одна ложная
Ложная причина — «так дешевле». Она верна гораздо реже, чем принято думать. Посчитаем на конкретных числах. Аренда A100 80 ГБ на Runpod/Vast — порядка 1,5–2 $/час. vLLM на такой карте выдаёт для 8B-модели в FP16 примерно 2000–3000 выходных токенов в секунду при высокой конкурентности. Это ~9 млн токенов в час, то есть около 0,2 $ за миллион токенов. Хостинговые провайдеры (Together, Fireworks, Groq, DeepInfra) продают ту же Llama 8B за 0,05–0,20 $ за миллион. Вы не выиграли ничего — и это при 100% утилизации. Если ваш GPU занят 5% времени, себестоимость взлетает в двадцать раз, и локальный инференс становится дороже облачного на порядок.
Экономика переворачивается только в трёх случаях: очень высокая постоянная нагрузка (карта не простаивает), железо уже куплено и амортизировано, или ваша задача решается моделью, которую никто не хостит.
Теперь настоящие причины.
- Данные не могут покинуть периметр. Медицина, банки, гостайна, персональные данные под локальным регулированием, внутренние кодовые базы под NDA. Это причина номер один в корпоративной практике, и она не про деньги — она про то, что альтернативы «не делать вообще» и «делать локально».
- Контроль версии и воспроизводимость. API-модель молча обновляется, и ваши промпты, вылизанные под её характер, начинают вести себя иначе. Локальный GGUF-файл с известным хэшем даст один и тот же ответ через год. Для регулируемой отчётности и для научных экспериментов это критично.
- Латентность и офлайн. Локальная 3B-модель отвечает за 20–40 мс TTFT против 300–800 мс до облака. Для автодополнения кода, голосового интерфейса, реалтайм-модерации разница решает. Плюс работа без сети — на борту, на заводе, в поле.
- Свобода делать запрещённое API. Доступ к логитам, кастомные сэмплеры, грамматики, принудительная генерация, дообучение под свою доменную задачу (об этом — в следующей статье про LoRA), эксперименты с внутренними активациями. Ни один провайдер не даст вам записывать hidden states.
Если ни один из четырёх пунктов не про вас — вероятно, локальный инференс это интересное хобби, а не инженерное решение. Признайте это честно и вернитесь к API.
Арифметика памяти: три слагаемых, которые надо уметь считать в уме
Модель влезает в карту или не влезает. Это определяется тремя слагаемыми, и только первое очевидно.
Слагаемое 1: веса
размер_весов_ГБ ≈ параметры_млрд × бит_на_вес / 8
Для 8B в FP16 это 8 × 16 / 8 = 16 ГБ. В 4 битах — примерно 4,5–5 ГБ (чуть больше номинала, потому что эмбеддинги и выходной слой обычно оставляют в большей точности). Быстрая эвристика: в 4 битах модель весит примерно половину числа своих миллиардов параметров в гигабайтах. 8B → ~4,7 ГБ. 70B → ~42 ГБ. 405B → ~230 ГБ.
Слагаемое 2: KV-кэш — то, что все забывают
При генерации каждого нового токена модель обязана видеть ключи и значения всех предыдущих. Пересчитывать их каждый раз — квадратично дорого, поэтому они хранятся. Размер:
KV_байт = 2 × слои × kv_голов × размер_головы × байт_на_число × токены × батч
Двойка — это отдельно K и отдельно V. Для Llama-3.1-8B (32 слоя, 8 KV-голов благодаря GQA, размер головы 128, FP16):
2 × 32 × 8 × 128 × 2 = 131 072 байта = 128 КиБ на один токен
Отсюда 8K контекста = 1 ГБ, 128K контекста = 16 ГБ, то есть кэш для полного контекста весит в три с лишним раза больше, чем сама 4-битная модель. Для Llama-3.3-70B (80 слоёв, 8 KV-голов) — 320 КиБ на токен, 128K = 40 ГБ.
Практический вывод, который экономит людям недели недоумения: вы почти никогда не упираетесь в веса, вы упираетесь в KV-кэш. Именно поэтому «модель загрузилась, а на длинном запросе всё падает по OOM». И именно поэтому Grouped-Query Attention (мало KV-голов на много Q-голов) — не оптимизация ради галочки, а то, что вообще сделало длинный контекст возможным на потребительском железе.
Лечится тремя способами: уменьшить объявленный контекст (--ctx-size / --max-model-len), квантовать сам кэш (--cache-type-k q8_0, --kv-cache-dtype fp8 — обычно даёт двукратную экономию почти без потерь) и включить flash attention, который убирает материализацию матрицы внимания.
Слагаемое 3: служебная память
Контекст CUDA, буферы активаций, промежуточные тензоры при prefill, фрагментация аллокатора. Закладывайте 1–2 ГБ на карту, а для vLLM просто оставляйте --gpu-memory-utilization 0.90 и не воюйте с последними процентами.
Второе неравенство: почему скорость упирается в память, а не в вычисления
Самое контринтуитивное в локальном инференсе: во время генерации ваш GPU простаивает. Он не считает — он ждёт, пока веса приедут из памяти в регистры.
Каждый новый токен требует прочитать все веса модели ровно один раз. Арифметических операций при этом мало (один токен — это вектор, а не матрица). Значит потолок скорости:
токенов_в_секунду ≤ пропускная_способность_памяти / размер_модели_в_байтах
Реальность — 60–80% от этого потолка. Проверим на живых числах:
| Железо | Пропускная способность | 8B Q4_K_M (4,9 ГБ) | 70B Q4_K_M (42,5 ГБ) |
|---|---|---|---|
| DDR5 dual-channel (CPU) | ~80 ГБ/с | ~10–13 tok/s | ~1,3 tok/s (и это если влезет) |
| RTX 3090 / 4090 (24 ГБ) | 936 / 1008 ГБ/с | ~120–150 tok/s | не влезает |
| RTX 5090 (32 ГБ) | 1792 ГБ/с | ~220–260 tok/s | не влезает |
| Apple M4 Max (128 ГБ unified) | ~546 ГБ/с | ~70–90 tok/s | ~9–11 tok/s |
| Apple M2/M3 Ultra (192 ГБ) | ~800 ГБ/с | ~110–130 tok/s | ~13–16 tok/s |
| A100 80 ГБ | 2039 ГБ/с | ~280–330 tok/s | ~35–40 tok/s |
| H100 SXM 80 ГБ | 3350 ГБ/с | ~450–520 tok/s | ~55–65 tok/s |
Из этой таблицы следуют все практические выводы разом.
- Квантизация ускоряет генерацию не потому, что 4-битная арифметика быстрее, а потому что через шину едет вчетверо меньше байт. Q4 примерно вдвое быстрее Q8 и вчетверо быстрее FP16 на одном и том же железе. Это чистая память.
- Prefill (обработка промпта) живёт по другим законам. Там все токены считаются параллельно, GPU упирается в вычисления, и квантизация помогает мало, а иногда даже вредит из-за накладных расходов на деквантизацию. Если у вас длинные промпты и короткие ответы — оптимизировать надо не биты, а prompt caching (см. ИИ в продакшене).
- Батчинг — бесплатный обед. Веса читаются один раз на всю пачку запросов. Прочитали 4,9 ГБ — обслужили не один токен, а сорок. Отсюда пропасть между 140 tok/s в одиночном чате и 2500 tok/s под нагрузкой. Из этого же следует: если у вас один пользователь, vLLM вам не нужен, его главное преимущество не работает.
- Мак с unified memory — не «медленный GPU», а «очень быстрый CPU». 128 ГБ единой памяти позволяют запустить 70B там, где никакая потребительская NVIDIA не справится, ценой скорости.
Квантизация: как работает и что именно ломает
Базовая идея проста до неприличия: веса внутри небольшого блока похожи по величине, значит можно хранить один общий масштаб во float и сами веса — маленькими целыми. Блок из 32 весов в 4 битах плюс FP16-масштаб даёт 144 бита на 32 веса, то есть 4,5 бита на вес вместо 16.
K-кванты в llama.cpp идут дальше: суперблок из 256 весов, восемь подблоков по 32, масштабы подблоков сами квантуются до 6 бит, плюс один общий множитель. Так экономятся биты на метаданных, и Q4_K при том же размере ощутимо точнее наивного Q4_0.
Где всё ломается — выбросы. В матрицах трансформера регулярно встречаются каналы с аномально большими активациями (это описано в LLM.int8(), arXiv:2208.07339). Один такой вес раздувает масштаб блока, и все его соседи схлопываются в ноль. Отсюда — три семейства «умных» методов:
- GPTQ (arXiv:2210.17323) — послойная квантизация с компенсацией: округлив один вес, метод подправляет остальные, используя приближение гессиана ошибки. Требует калибровочный набор текстов.
- AWQ (arXiv:2306.00978) — activation-aware: находит ~1% «важных» каналов по статистике активаций и масштабирует их так, чтобы они не теряли точность. Быстрее GPTQ на калибровке, обычно точнее на 4 битах.
- imatrix в llama.cpp — та же идея в GGUF-мире: прогнать корпус, собрать матрицу важности и распределить биты неравномерно. Обязателен для IQ-квантов (2–3 бита), где без него результат несъедобен.
Отдельная ветка — FP8 (E4M3) на Hopper/Ada и MXFP4/NVFP4 на Blackwell. Это не программный трюк, а аппаратный формат: тензорные ядра умножают в нём нативно, поэтому ускоряется не только decode, но и prefill. FP8 при этом почти безвреден по качеству. Именно в MXFP4 OpenAI выпустила gpt-oss — модель, спроектированную под 4-битный формат с самого начала.
Практическая таблица: что выбирать
| Формат | Бит/вес | Размер 8B | Рост перплексии | Когда брать |
|---|---|---|---|---|
| FP16/BF16 | 16 | 16,1 ГБ | базовая | эталон для замеров, обучение |
| FP8 | 8 | 8,5 ГБ | ~0,1% | H100/L40S в vLLM — почти даром |
| Q8_0 | 8,5 | 8,5 ГБ | ~0,1% | когда VRAM некуда девать |
| Q6_K | 6,6 | 6,6 ГБ | ~0,2% | лучший «безопасный» вариант |
| Q5_K_M | 5,7 | 5,7 ГБ | ~0,5% | компромисс, если Q4 подводит |
| Q4_K_M | 4,8 | 4,9 ГБ | ~1–2% | дефолт для 99% случаев |
| IQ4_XS | 4,3 | 4,4 ГБ | ~2% | когда 4,9 ГБ не влезает, а 4,4 влезает |
| Q3_K_M | 3,9 | 4,0 ГБ | ~5% | только ради влезания в память |
| IQ2_XS | 2,4 | 2,6 ГБ | ~15–25% | почти всегда плохая идея |
Цифры перплексии — порядок величины по замерам в llama.cpp quantize README; точные значения зависят от модели и корпуса.
Главное правило выбора, и оно нетривиально
Большая модель в 4 битах почти всегда лучше маленькой в 16. 70B@Q4 (42 ГБ) бьёт 13B@FP16 (26 ГБ) и по качеству, и по «памяти на единицу пользы». Это подтверждено систематически — см. A Comprehensive Evaluation of Quantized Instruction-Tuned LLMs, arXiv:2409.11055. Порог примерно на 4 битах: ниже начинает выигрывать модель поменьше.
Три места, где квантизация обманывает замеры
Это самая недооценённая часть темы, поэтому подробно.
- Перплексия не ловит то, что ломается. Работа Accuracy is Not All You Need, arXiv:2407.09141 показывает: квантованная модель может иметь ту же среднюю точность на бенчмарке, но при этом «переворачивать» ответы на десятках процентов конкретных примеров — одни задачи ломаются, другие случайно чинятся, среднее сохраняется. Если вы меряете качество только агрегатом, вы не видите деградацию. Меряйте flip rate: долю примеров, где ответ изменился относительно FP16.
- Хорошо обученные модели страдают сильнее. How Good Are Low-bit Quantized LLaMA3 Models? arXiv:2404.14047 обнаружила, что Llama 3 деградирует от квантизации заметно сильнее Llama 2. Интуиция: модель, обученную на 15 трлн токенов, «прижали» к весам плотнее — в каждом бите больше информации, и терять их дороже. Чем свежее и лучше обучена модель, тем осторожнее с низкими битами.
- Деградация неравномерна по задачам. Сильнее всего проседают: длинный контекст (ошибки накапливаются по всей глубине), математика и код (одна неверная цифра рушит ответ), не-английские языки (редкие токены), следование сложным инструкциям и вызов инструментов (JSON начинает разъезжаться). Обычный чат на английском проседает меньше всего — и именно на нём все делают демо.
Вывод для практики: прогоняйте свой собственный eval-набор (см. Оценку и бенчмарки) на FP16 и на кандидате-кванте, сравнивайте не только среднее, но и долю расхождений. Это два часа работы, которые спасают от «модель в проде стала тупить, а почему — непонятно».
Дерево решений: что запускать
пользователей?} B -->|Один: я сам| C{Какая платформа?} B -->|Десятки-сотни| D{Модель влезает
в VRAM целиком?} C -->|Mac| E[LM Studio + MLX
или llama.cpp Metal] C -->|Windows/Linux, хочу быстро| F[Ollama
одна команда, всё работает] C -->|Хочу контроль над флагами| G[llama.cpp / llama-server] D -->|Да, с запасом на KV| H[vLLM или SGLang
continuous batching] D -->|Нет, не хватает 10-20%| I{Можно квантовать
до AWQ/FP8?} D -->|Нет, не хватает вдвое| J[Tensor parallel на 2+ GPU
или модель поменьше] I -->|Да| H I -->|Нет| K[llama.cpp с выгрузкой слоёв
в RAM: работает, но медленно] H --> L{Модель — MoE
с большим числом экспертов?} L -->|Да| M[Проверьте --override-tensor
эксперты в RAM, attention в VRAM] L -->|Нет| N[Готово] style H fill:#6aa86a,fill-opacity:0.2 style F fill:#4f8ac9,fill-opacity:0.2 style K fill:#c25b5b,fill-opacity:0.2
llama.cpp: фундамент, на котором стоит половина экосистемы
ggml-org/llama.cpp — это C/C++ движок без зависимостей, с бэкендами под CUDA, Metal, ROCm, Vulkan, SYCL и просто CPU. Ollama, LM Studio, Jan, llamafile и десяток других проектов — это обёртки над ним (Ollama сейчас частично переехал на собственный движок, но GGUF и наследие остались).
Ключевая особенность, ради которой его выбирают: частичная выгрузка. Вы можете положить 30 из 40 слоёв в VRAM, а остальные считать на CPU. Модель, которая «не влезает», всё равно работает — медленно, но работает. Ни vLLM, ни TensorRT-LLM так не умеют.
# Сборка с CUDA
cmake -B build -DGGML_CUDA=ON && cmake --build build --config Release -j
# Скачать модель прямо с HuggingFace и поднять OpenAI-совместимый сервер
./build/bin/llama-server \
-hf bartowski/Meta-Llama-3.1-8B-Instruct-GGUF:Q4_K_M \
--host 0.0.0.0 --port 8080 \
-ngl 99 \ # все слои на GPU (99 = "сколько влезет")
-c 16384 \ # размер контекста; помните про KV-кэш
-fa on \ # flash attention: меньше памяти, быстрее длинный контекст
-ctk q8_0 -ctv q8_0 \ # квантизация KV-кэша: вдвое меньше памяти
-np 4 \ # 4 параллельных слота (контекст делится между ними!)
--mlock # запретить своп весов на диск
Тонкость про -np: указанный -c 16384 при четырёх слотах означает 4096 токенов на слот, а не 16K каждому. Классический источник «модель забыла начало разговора».
Замеры — встроенные
Не верьте ощущениям, у llama.cpp есть свой бенчмарк:
./build/bin/llama-bench -m models/llama-3.1-8b-q4_k_m.gguf -p 512 -n 128 -ngl 99
# pp512 — скорость prefill (обработка промпта), tok/s
# tg128 — скорость генерации, tok/s
Разница между pp и tg на порядок — это и есть та самая асимметрия compute-bound / memory-bound. На RTX 4090 для 8B Q4_K_M ждите примерно pp512 ≈ 5000–7000 tok/s и tg128 ≈ 130–150 tok/s.
Два продвинутых приёма
Спекулятивное декодирование. Маленькая черновая модель генерирует несколько токенов вперёд, большая проверяет их одним проходом. Если угадала — вы получили 3 токена по цене одного. На коде и структурированном тексте (где много предсказуемого) даёт ускорение в 1,5–2,5 раза, на творческом тексте — почти ничего.
llama-server -m Llama-3.3-70B-Q4_K_M.gguf \
-md Llama-3.2-1B-Instruct-Q4_K_M.gguf \ # черновая модель
--draft-max 16 --draft-min 4 -ngl 99 -ngld 99
Выгрузка экспертов MoE. У MoE-модели на каждый токен активна лишь часть параметров, но в память надо положить все. Хитрость: attention-слои маленькие и нужны всегда — им VRAM; тензоры экспертов огромные и читаются выборочно — их можно оставить в RAM.
# Attention на GPU, FFN-эксперты в системной памяти
llama-server -m qwen3-moe-Q4_K_M.gguf -ngl 99 \
--override-tensor "blk\..*_exps\.=CPU" -c 32768
Это позволяет крутить MoE-модели на 100+ млрд параметров на одной потребительской карте с приемлемой скоростью. Тот же принцип лежит в основе KTransformers.
Ollama: удобство ценой контроля
Ollama — это «Docker для моделей»: ollama run llama3.1 скачивает, настраивает и запускает. Демон держит модель в памяти, выгружает по таймауту, предоставляет свой REST API и OpenAI-совместимый эндпоинт на localhost:11434/v1. Для прототипов, локальной разработки и десктопных приложений это правильный выбор — вы экономите день возни с флагами.
Главная ловушка, на которой спотыкаются все: контекст по умолчанию обрезан (исторически 2048, в свежих версиях 4096 токенов). Вы отправляете модели документ на 20 тысяч токенов, Ollama молча выбрасывает начало, и вы делаете вывод «локальные модели тупые». Проверьте это первым делом.
# Глобально
OLLAMA_CONTEXT_LENGTH=32768 ollama serve
# Или для конкретной модели через Modelfile
cat > Modelfile <<'EOF'
FROM llama3.1:8b-instruct-q4_K_M
PARAMETER num_ctx 32768
PARAMETER temperature 0.2
PARAMETER top_p 0.9
PARAMETER repeat_penalty 1.05
SYSTEM """Ты ассистент по внутренней документации. Отвечай только по контексту."""
EOF
ollama create doc-assistant -f Modelfile
Полезные переменные окружения для сервера:
| Переменная | Что делает | Разумное значение |
|---|---|---|
OLLAMA_CONTEXT_LENGTH |
контекст по умолчанию | 8192–32768 |
OLLAMA_KEEP_ALIVE |
сколько держать модель в VRAM | 30m или -1 (вечно) |
OLLAMA_NUM_PARALLEL |
параллельных запросов на модель | 1–4 |
OLLAMA_MAX_LOADED_MODELS |
моделей в памяти одновременно | 1, если VRAM мало |
OLLAMA_FLASH_ATTENTION |
flash attention | 1 |
OLLAMA_KV_CACHE_TYPE |
квантизация KV-кэша | q8_0 |
Вызов из Python через обычный OpenAI SDK — Ollama притворяется OpenAI:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama") # ключ игнорируется
resp = client.chat.completions.create(
model="doc-assistant",
messages=[{"role": "user", "content": "Что такое KV-кэш?"}],
temperature=0.2,
# llama.cpp под капотом умеет грамматики; через OpenAI-слой доступен JSON-режим
response_format={"type": "json_object"},
)
print(resp.choices[0].message.content)
Когда Ollama мешает: нет continuous batching уровня vLLM (пропускная способность под нагрузкой в разы ниже), меньше контроля над флагами llama.cpp, свои имена тегов моделей вместо явных GGUF-файлов, и квантизация по умолчанию (llama3.1:8b = Q4_K_M) не всегда та, которую вы хотели. Для одного-двух пользователей — идеально. Для продакшн-сервиса — нет.
LM Studio: GUI, и это не оскорбление
LM Studio — десктопное приложение под macOS/Windows/Linux: каталог моделей с HuggingFace, выбор кванта с индикатором «влезет / влезет частично / не влезет», встроенный чат, локальный сервер на localhost:1234/v1, поддержка структурированного вывода и вызова инструментов.
Две вещи делают его больше чем игрушкой:
- Движок MLX на Apple Silicon. MLX — фреймворк Apple, спроектированный под unified memory. На маках он часто быстрее llama.cpp+Metal, особенно на prefill. LM Studio переключает движок в один клик; альтернатива —
mlx_lm.serverиз mlx-examples руками. - CLI
lms— так что GUI не обязателен в проде:
lms server start --port 1234
lms load llama-3.1-8b-instruct --context-length 32768 --gpu max
lms ps # что загружено сейчас
lms unload --all
Ниша LM Studio: быстрый подбор модели и кванта под ваше конкретное железо (индикатор влезания экономит массу времени), демо для нетехнических коллег, и разработка на маке. Лицензия менялась — проверьте текущие условия для коммерческого использования на сайте.
vLLM: когда пользователей много
vLLM — это другой класс инструмента. Он не про «запустить на ноутбуке», он про максимальную пропускную способность на серверном GPU. Две идеи внутри.
PagedAttention (arXiv:2309.06180, SOSP 2023). Классические рантаймы резервируют непрерывный кусок памяти под максимальный контекст каждого запроса — и теряют 60–80% VRAM на неиспользованный хвост. vLLM применяет к KV-кэшу идею виртуальной памяти ОС: кэш нарезается на блоки фиксированного размера (обычно 16 токенов), блоки лежат где угодно, таблица отображения связывает их в логическую последовательность. Фрагментация падает до единиц процентов, а общие префиксы (системный промпт!) физически шарятся между запросами.
Continuous batching. Статический батчинг ждёт, пока все запросы в пачке закончатся — и короткий ответ простаивает, дожидаясь длинного. vLLM планирует на каждой итерации: закончившийся запрос немедленно уступает слот новому.
Жизненный цикл одного запроса внутри движка:
длинный промпт по кускам Prefill --> Decode: KV заполнен Decode --> Decode: +1 токен за итерацию } RUNNING --> PREEMPTED: KV-блоки кончились,
вытеснен более приоритетным PREEMPTED --> WAITING: вернуть в очередь
(recompute или swap в RAM) RUNNING --> FINISHED: eos / max_tokens / стоп-строка RUNNING --> ABORTED: клиент отключился FINISHED --> [*]: блоки освобождены ABORTED --> [*] note right of PREEMPTED Растёт число preemption в метриках → вы перегрузили сервер. Снижайте max_num_seqs или max_model_len. end note
Запуск и ключевые флаги
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--host 0.0.0.0 --port 8000 \
--max-model-len 32768 \ # НЕ ставьте максимум модели без нужды: съест KV
--gpu-memory-utilization 0.90 \ # доля VRAM под движок; выше 0.95 — риск OOM
--max-num-seqs 64 \ # потолок конкурентности
--enable-prefix-caching \ # переиспользование общих префиксов (в V1 по умолчанию)
--kv-cache-dtype fp8 \ # вдвое больше запросов в ту же память
--quantization awq \ # если веса в AWQ
--tensor-parallel-size 2 \ # шардинг по 2 GPU (нужен быстрый интерконнект!)
--served-model-name local-8b
Про --tensor-parallel-size: он делит каждый слой между картами, и на каждом слое требуется all-reduce. Без NVLink на PCIe это может отъесть больше, чем даёт. Для «модель не влезает» на потребительском железе часто честнее --pipeline-parallel-size (слои целиком по картам, обмен только на границах) или просто квант поменьше.
Правильные замеры
# Реалистичная нагрузка на ShareGPT-подобных запросах
vllm bench serve \
--backend openai-chat --model local-8b \
--base-url http://localhost:8000 \
--dataset-name sharegpt --num-prompts 500 \
--request-rate 20 # запросов в секунду; inf = стресс-тест
Смотреть надо на четыре метрики, и путать их нельзя:
- TTFT (time to first token) — насколько «отзывчиво» для пользователя. Определяется prefill и очередью.
- TPOT/ITL (time per output token) — скорость печати. Определяется пропускной способностью памяти и размером батча.
- Throughput (выходных токенов в секунду по всему серверу) — экономика. Растёт с батчем.
- P99 latency — то, по чему вас будут ругать. Растёт вместе с throughput; это фундаментальный компромисс, а не баг.
Ключевое наблюдение: throughput и latency тянут в разные стороны. Увеличив max_num_seqs, вы поднимете общую пропускную способность и ухудшите TPOT каждому. Решайте, что оптимизируете, до того как крутить флаги.
Сравнение рантаймов
| llama.cpp | Ollama | LM Studio | vLLM | |
|---|---|---|---|---|
| Форматы | GGUF | GGUF | GGUF, MLX | HF safetensors, AWQ, GPTQ, FP8 |
| Работает без GPU | да, хорошо | да | да | формально да, практически нет |
| Частичная выгрузка в RAM | да, гибко | да | да | нет |
| Continuous batching | базовый | базовый | базовый | лучший в классе |
| Throughput под нагрузкой | ~1× | ~1× | ~1× | 5–20× |
| Порог входа | высокий | минимальный | минимальный | средний |
| Apple Silicon | Metal | да | MLX, лучший вариант | нет |
| Типичная ниша | встраивание, edge, эксперименты | десктоп, прототипы | подбор модели, мак | продакшн-сервис |
Стоит знать и о соседях: SGLang (репозиторий) с RadixAttention — агрессивное переиспользование префиксов, часто быстрее vLLM на агентных нагрузках с общими системными промптами; TensorRT-LLM — максимум на NVIDIA ценой компиляции движка под конкретную карту; TGI от HuggingFace; ExLlamaV2/V3 с форматом EXL — лидер по скорости одиночной генерации на потребительских картах.
Требования к железу: что покупать под задачу
| Бюджет / железо | VRAM | Что реально крутится |
|---|---|---|
| Ноутбук без дискретной карты | — | 1–4B в Q4 на CPU, 5–15 tok/s. Классификация, автодополнение |
| RTX 3060 12 ГБ | 12 | 8B Q4 с контекстом 16K, 7–14B Q4 впритык |
| RTX 4090 / 5090 | 24 / 32 | 8B в FP16, 14B Q6, 32B Q4, MoE с выгрузкой экспертов |
| 2× RTX 3090 (NVLink) | 48 | 70B Q4 на ~15–18 tok/s — золотой стандарт домашней лаборатории |
| Mac Studio M3 Ultra 256 ГБ | 256 unified | 70B без квантизации, 235B MoE в Q4. Медленно, но помещается |
| L40S / A100 80 ГБ | 48 / 80 | продакшн 8–32B c vLLM, десятки одновременных пользователей |
| H100/H200 | 80 / 141 | 70B в FP8 с высокой конкурентностью, 405B на узле из 8 карт |
Несколько практических замечаний, которые редко пишут.
- Два GPU по 24 ГБ ≠ один на 48 ГБ. Появляется межкарточный обмен, и на PCIe без NVLink tensor parallel может дать прирост меньше ожидаемого. Но 70B, которая иначе не запускается, стоит этих потерь.
- Б/у RTX 3090 — лучшее соотношение ГБ/рубль на рынке для домашней лаборатории: 24 ГБ и 936 ГБ/с при цене вдвое ниже 4090, а разница в скорости всего ~10% (обе упираются в память, а не в вычисления).
- RAM должна быть не меньше VRAM, иначе загрузка модели с диска превратится в мучение, а частичная выгрузка не заработает.
- NVMe вместо HDD — обязательно. 40-гигабайтная модель грузится с SATA-диска минуту, с NVMe — 10 секунд.
- Питание и охлаждение. Две 3090 — это 700 Вт под нагрузкой плюс процессор. Считайте БП и корпусный airflow заранее; троттлинг съедает 20–30% скорости незаметно.
Продакшн-обвязка
Локальный сервер в проде — это не только vllm serve. Минимальный набор:
# docker-compose.yml
services:
vllm:
image: vllm/vllm-openai:latest
command: >
--model /models/Llama-3.1-8B-Instruct
--served-model-name local-8b
--max-model-len 32768
--gpu-memory-utilization 0.90
--enable-prefix-caching
--kv-cache-dtype fp8
volumes: ["/srv/models:/models:ro"]
ports: ["8000:8000"]
deploy:
resources:
reservations:
devices: [{driver: nvidia, count: 1, capabilities: [gpu]}]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
start_period: 300s # загрузка весов долгая, не дайте оркестратору убить контейнер
restart: unless-stopped
Наблюдаемость. vLLM отдаёт Prometheus-метрики на /metrics. Три показателя, которые надо вывести на дашборд в первую очередь:
vllm:gpu_cache_usage_perc— заполнение KV-кэша. Стабильно >90% означает, что вы на грани вытеснений.vllm:num_preemptions_total— если растёт, сервер перегружен: снижайтеmax_num_seqsилиmax_model_len.vllm:num_requests_waiting— длина очереди, прямой предиктор роста TTFT.
Гибридная схема — то, что чаще всего оказывается правильным ответом. Локальная модель обрабатывает массовые дешёвые задачи (классификация, извлечение сущностей, реранжирование в RAG, суммаризация), а сложные запросы уходят во фронтир-модель по API. Роутинг делается через прокси с единым OpenAI-совместимым интерфейсом:
# Простейший роутер: локально по умолчанию, эскалация по сигналу неуверенности
from openai import OpenAI
local = OpenAI(base_url="http://localhost:8000/v1", api_key="x")
cloud = OpenAI() # ключ из окружения
def classify(text: str, escalate_if_unsure: bool = True) -> dict:
"""Классификация тикета: сначала локальная 8B, при низкой уверенности — облако."""
r = local.chat.completions.create(
model="local-8b",
messages=[{"role": "user", "content": PROMPT.format(text=text)}],
temperature=0.0,
logprobs=True, top_logprobs=2, # локальные движки отдают логпробы — API часто нет
max_tokens=8,
)
choice = r.choices[0]
# уверенность = вероятность первого токена метки
top = choice.logprobs.content[0].top_logprobs[0]
confidence = 2.718 ** top.logprob
if escalate_if_unsure and confidence < 0.75:
r = cloud.chat.completions.create(
model="claude-sonnet-4-5", # или другая фронтир-модель
messages=[{"role": "user", "content": PROMPT.format(text=text)}],
temperature=0.0, max_tokens=8,
)
return {"label": r.choices[0].message.content.strip(), "route": "cloud"}
return {"label": choice.message.content.strip(), "route": "local", "conf": confidence}
Обратите внимание на деталь: локальный движок отдаёт логпробы, и это даёт вам дешёвый и честный сигнал уверенности для роутинга — то, чего у большинства API-провайдеров попросту нет. Это один из недооценённых аргументов в пользу локального инференса.
Для более серьёзной маршрутизации возьмите готовый прокси — LiteLLM даёт единый API, фолбэки, лимиты и учёт затрат поверх и локальных, и облачных бэкендов.
Типичные ошибки
- Не посчитали KV-кэш. Модель влезла, а на 30K токенов сервер упал. Считайте до покупки железа, а не после.
- Оставили контекст Ollama по умолчанию и решили, что модель глупая.
--max-model-lenв максимум модели. Объявили 128K «на всякий случай» — vLLM зарезервировал под это KV и не смог обслуживать конкурентность.- Меряли скорость на коротком промпте. pp и tg отличаются в 40 раз; «150 tok/s» без указания, что именно меряли, не значит ничего.
- Взяли Q2/Q3, чтобы влезла модель побольше. Ниже 4 бит выигрыш от размера съедается деградацией. Лучше 8B@Q5, чем 32B@Q2.
- Сравнивали качество только по перплексии и не заметили flip rate на своих задачах.
- Поставили vLLM для одного пользователя. Вся его магия — в батчинге, которого нет. Взяли бы llama.cpp — получили бы больше скорости и меньше боли.
- Забыли про шаблон промпта. Каждое семейство моделей имеет свой chat template; неправильный формат ролей ухудшает качество тихо, без ошибок. Используйте
/v1/chat/completions, а не/v1/completions, и пусть движок применит шаблон из GGUF-метаданных сам. - Не зафиксировали seed и сэмплер в экспериментах.
temperature=0не гарантирует полного детерминизма на GPU (порядок редукций с плавающей точкой плавает от размера батча), но без фиксации сэмплера вы вообще ничего не сравните. - Игнорировали лицензию модели. Llama-лицензия, Gemma-лицензия и Apache 2.0 — разные вещи; часть моделей запрещает коммерческое использование или требует атрибуции.
Мини-итог
Локальный инференс — это инженерия памяти, а не магия. Три вещи, которые надо унести:
- Считайте память в три слагаемых: веса (параметры × бит / 8), KV-кэш (
2 × слои × kv_голов × dim × байт × токены) и служебные 1–2 ГБ. Упираются обычно во второе. - Скорость генерации = пропускная способность памяти / размер модели. Отсюда следует всё: и почему квантизация ускоряет, и почему батчинг бесплатен, и почему мак с unified memory играет в свою лигу.
- Q4_K_M — дефолт, большая модель в 4 битах лучше маленькой в 16, но ниже 4 бит начинается территория, где надо мерить на своих задачах — и мерить долю изменившихся ответов, а не среднюю метрику.
Рантайм выбирается по числу пользователей: один — llama.cpp/Ollama/LM Studio, много — vLLM или SGLang. А самый частый правильный ответ на вопрос «локально или API» звучит как «и то, и другое»: дешёвая массовка локально, сложное — во фронтир.
Источники
- ggml-org/llama.cpp — движок, документация по флагам,
llama-bench, форматы K-квантов - Документация vLLM и PagedAttention, arXiv:2309.06180
- Ollama — FAQ с переменными окружения и настройкой контекста
- LM Studio и MLX — Apple Silicon
- GPTQ, arXiv:2210.17323, AWQ, arXiv:2306.00978, LLM.int8(), arXiv:2208.07339, SmoothQuant, arXiv:2211.10438
- Accuracy is Not All You Need, arXiv:2407.09141 — почему средняя метрика скрывает деградацию
- How Good Are Low-bit Quantized LLaMA3 Models? arXiv:2404.14047
- A Comprehensive Evaluation of Quantized Instruction-Tuned LLMs, arXiv:2409.11055
- SGLang / RadixAttention, LiteLLM
Что дальше
Мы научились запускать чужие веса у себя. Следующий шаг — сделать их своими: Дообучение: LoRA, QLoRA, инструкционный тюнинг и когда оно вообще нужно. Там разберём, почему адаптеры ранга 16 заменяют полное дообучение, как QLoRA умещает тюнинг 70B в одну карту и — главное — в каких случаях дообучение вообще не нужно, а нужен промпт получше или RAG.