ИИ-агенты и prompt engineering Локальные модели: llama.cpp, Ollama, vLLM, LM Studio, квантизация, требования к железу
0%

Локальные модели: llama.cpp, Ollama, vLLM, LM Studio, квантизация, требования к железу

Локальные модели: 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% времени, себестоимость взлетает в двадцать раз, и локальный инференс становится дороже облачного на порядок.

Экономика переворачивается только в трёх случаях: очень высокая постоянная нагрузка (карта не простаивает), железо уже куплено и амортизировано, или ваша задача решается моделью, которую никто не хостит.

Теперь настоящие причины.

  1. Данные не могут покинуть периметр. Медицина, банки, гостайна, персональные данные под локальным регулированием, внутренние кодовые базы под NDA. Это причина номер один в корпоративной практике, и она не про деньги — она про то, что альтернативы «не делать вообще» и «делать локально».
  2. Контроль версии и воспроизводимость. API-модель молча обновляется, и ваши промпты, вылизанные под её характер, начинают вести себя иначе. Локальный GGUF-файл с известным хэшем даст один и тот же ответ через год. Для регулируемой отчётности и для научных экспериментов это критично.
  3. Латентность и офлайн. Локальная 3B-модель отвечает за 20–40 мс TTFT против 300–800 мс до облака. Для автодополнения кода, голосового интерфейса, реалтайм-модерации разница решает. Плюс работа без сети — на борту, на заводе, в поле.
  4. Свобода делать запрещённое API. Доступ к логитам, кастомные сэмплеры, грамматики, принудительная генерация, дообучение под свою доменную задачу (об этом — в следующей статье про LoRA), эксперименты с внутренними активациями. Ни один провайдер не даст вам записывать hidden states.

Если ни один из четырёх пунктов не про вас — вероятно, локальный инференс это интересное хобби, а не инженерное решение. Признайте это честно и вернитесь к API.

Арифметика памяти: три слагаемых, которые надо уметь считать в уме

Модель влезает в карту или не влезает. Это определяется тремя слагаемыми, и только первое очевидно.

Бюджет VRAM: веса, KV-кэш и служебная память

Слагаемое 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 битах: ниже начинает выигрывать модель поменьше.

Три места, где квантизация обманывает замеры

Это самая недооценённая часть темы, поэтому подробно.

  1. Перплексия не ловит то, что ломается. Работа Accuracy is Not All You Need, arXiv:2407.09141 показывает: квантованная модель может иметь ту же среднюю точность на бенчмарке, но при этом «переворачивать» ответы на десятках процентов конкретных примеров — одни задачи ломаются, другие случайно чинятся, среднее сохраняется. Если вы меряете качество только агрегатом, вы не видите деградацию. Меряйте flip rate: долю примеров, где ответ изменился относительно FP16.
  2. Хорошо обученные модели страдают сильнее. How Good Are Low-bit Quantized LLaMA3 Models? arXiv:2404.14047 обнаружила, что Llama 3 деградирует от квантизации заметно сильнее Llama 2. Интуиция: модель, обученную на 15 трлн токенов, «прижали» к весам плотнее — в каждом бите больше информации, и терять их дороже. Чем свежее и лучше обучена модель, тем осторожнее с низкими битами.
  3. Деградация неравномерна по задачам. Сильнее всего проседают: длинный контекст (ошибки накапливаются по всей глубине), математика и код (одна неверная цифра рушит ответ), не-английские языки (редкие токены), следование сложным инструкциям и вызов инструментов (JSON начинает разъезжаться). Обычный чат на английском проседает меньше всего — и именно на нём все делают демо.

Вывод для практики: прогоняйте свой собственный eval-набор (см. Оценку и бенчмарки) на FP16 и на кандидате-кванте, сравнивайте не только среднее, но и долю расхождений. Это два часа работы, которые спасают от «модель в проде стала тупить, а почему — непонятно».

Дерево решений: что запускать

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, поддержка структурированного вывода и вызова инструментов.

Две вещи делают его больше чем игрушкой:

  1. Движок MLX на Apple Silicon. MLX — фреймворк Apple, спроектированный под unified memory. На маках он часто быстрее llama.cpp+Metal, особенно на prefill. LM Studio переключает движок в один клик; альтернатива — mlx_lm.server из mlx-examples руками.
  2. 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 планирует на каждой итерации: закончившийся запрос немедленно уступает слот новому.

Жизненный цикл одного запроса внутри движка:

Запуск и ключевые флаги

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, фолбэки, лимиты и учёт затрат поверх и локальных, и облачных бэкендов.

Типичные ошибки

  1. Не посчитали KV-кэш. Модель влезла, а на 30K токенов сервер упал. Считайте до покупки железа, а не после.
  2. Оставили контекст Ollama по умолчанию и решили, что модель глупая.
  3. --max-model-len в максимум модели. Объявили 128K «на всякий случай» — vLLM зарезервировал под это KV и не смог обслуживать конкурентность.
  4. Меряли скорость на коротком промпте. pp и tg отличаются в 40 раз; «150 tok/s» без указания, что именно меряли, не значит ничего.
  5. Взяли Q2/Q3, чтобы влезла модель побольше. Ниже 4 бит выигрыш от размера съедается деградацией. Лучше 8B@Q5, чем 32B@Q2.
  6. Сравнивали качество только по перплексии и не заметили flip rate на своих задачах.
  7. Поставили vLLM для одного пользователя. Вся его магия — в батчинге, которого нет. Взяли бы llama.cpp — получили бы больше скорости и меньше боли.
  8. Забыли про шаблон промпта. Каждое семейство моделей имеет свой chat template; неправильный формат ролей ухудшает качество тихо, без ошибок. Используйте /v1/chat/completions, а не /v1/completions, и пусть движок применит шаблон из GGUF-метаданных сам.
  9. Не зафиксировали seed и сэмплер в экспериментах. temperature=0 не гарантирует полного детерминизма на GPU (порядок редукций с плавающей точкой плавает от размера батча), но без фиксации сэмплера вы вообще ничего не сравните.
  10. Игнорировали лицензию модели. 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» звучит как «и то, и другое»: дешёвая массовка локально, сложное — во фронтир.

Источники

Что дальше

Мы научились запускать чужие веса у себя. Следующий шаг — сделать их своими: Дообучение: LoRA, QLoRA, инструкционный тюнинг и когда оно вообще нужно. Там разберём, почему адаптеры ранга 16 заменяют полное дообучение, как QLoRA умещает тюнинг 70B в одну карту и — главное — в каких случаях дообучение вообще не нужно, а нужен промпт получше или RAG.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов