Всем привет, меня зовут Сергей Прощаев, я Tech Lead и руководитель направления Java/Kotlin‑разработки в FinTech & E‑commerce, преподаю на курсах разработки и архитектуры в ОТУС.

В этой статье расскажу про домашний стенд для локального инференса: почему его часто собирают не от профиля нагрузки и что на самом деле упирается в потолок.


Знакомая картина: вы купили видеокарту подороже, подняли на ней модель, запустили — и получили заметно меньше токенов в секунду, чем обещали в обзорах. При этом карта не греется, загрузка GPU скачет от тридцати до пятидесяти процентов, вентиляторы молчат. 

Железо явно простаивает, а модель еле ползёт.

  • Первая мысль — что‑то настроено криво.

  • Вторая — что надо было брать карту ещё дороже.

Обе неверные, и ниже я разберу почему.

Рис. 1. Узкое место домашнего стенда: данные не успевают дойти до вычислителя
Рис. 1. Узкое место домашнего стенда: данные не успевают дойти до вычислителя

Крючок: 671 миллиард параметров и ноль видеокарт

Я довольно долго списывал эту картину на кривые руки. А потом наткнулся на конкретный опубликованный эксперимент, который заставил меня всё пересчитать.

Инженер Hugging Face Мэтью Кэрриган собрал стенд под полную DeepSeek‑R1 на 671 миллиард параметров в 8-битном квантовании — и не поставил туда ни одной видеокарты. Два процессора AMD EPYC, 768 ГБ DDR5, Linux, llama.cpp.

Обошлось примерно в шесть тысяч долларов. На выходе — 6–8 токенов в секунду при потреблении меньше 400 ватт. Отдельная деталь: правка в BIOS, выставление NUMA‑групп в ноль, удваивала эффективность работы с памятью.

Для сравнения: только веса той же модели в Q8 занимают порядка 700 ГБ, и это без KV‑кэша и служебных буферов. На видеокартах такая конфигурация стоила бы больше сотни тысяч долларов.

Оговорюсь сразу: это единичный эксперимент с конкретной сборкой, а не типовая конфигурация и не бенчмарк.

Но разница в двадцать раз по деньгам — не оптимизация, а другая физика. И именно она объясняет, почему моя видеокарта простаивала.

Объект разбора: из чего на самом деле состоит inference budget

Дальше я разбираю не железо и не движки по отдельности, а одну конструкцию целиком — маршрут запроса от клиента до ответа и то, что на этом маршруте является дефицитным ресурсом.

Четыре наблюдения ниже — то, что я для себя пересобрал после истории с EPYC.

Сразу обозначу границы: речь про стенд для экспериментов и небольших внутренних нагрузок, а не про референсную архитектуру production‑сервиса инференса.

Наблюдение 1. Prefill и decode упираются в разное

Начну с уточнения, которое снимает половину путаницы. Инференс состоит из двух непохожих фаз.

  1. Prefill — обработка входного контекста. Здесь заметную роль играют вычисления и attention, и при длинном промпте фаза вполне может быть compute‑bound.

  2. Decode — генерация токенов по одному. Вот здесь при небольшом батче арифметическая интенсивность многих операций низкая: на каждый прочитанный из памяти байт приходится мало арифметики. Именно эту фазу вы наблюдаете как «мало токенов в секунду при простаивающей карте».

То есть корректная формулировка звучит так:

При авторегрессионной генерации небольшого числа последовательностей узкое место часто смещается в сторону пропускной способности памяти.

Но это не универсальный закон — prefill, большой батч и конкретная архитектура модели способны сделать ограничение вычислительным.

Порядок величин у H100 SXM показывает, откуда берётся перекос: пропускная способность памяти 3,35 ТБ/с, а пиковая производительность тензорных ядер — 989 TFLOPS в FP16 dense, или 1979 TFLOPS с учётом структурной разреженности 2:4.

Эти цифры хорошо иллюстрируют, почему на memory‑bound фазе одной высокой вычислительной производительности недостаточно.

Отсюда практический вывод, и он не универсальный, а привязанный к фазе: для decode при небольшом числе последовательностей объём и пропускная способность памяти обычно важнее пиковых FLOPS, а на prefill и при большом батче определяющей вполне может стать именно вычислительная производительность.

Здесь же становится понятна роль архитектуры MoE. В каждом MoE‑слое DeepSeek‑V3 есть 256 routed‑экспертов, из которых для токена выбираются восемь, плюс один shared‑эксперт, работающий постоянно. Итог — 37 миллиардов активных параметров из 671.

Важно не переусердствовать с выводом. MoE снижает объём вычислений на токен, но не уменьшает требуемый объём памяти: при полном размещении модели держать приходится весь набор параметров, хотя на конкретном токене считается только активная часть.

Поэтому большая модель становится реализуемой на системе с большим объёмом обычной RAM даже без GPU — ценой существенно меньшей скорости. 6–8 токенов в секунду годятся для фоновой обработки и не годятся для интерактивной работы.

Наблюдение 2. Память делят двое, а считают обычно одного

Самая частая ошибка, которую я вижу у коллег, — считать память по весам модели. Формула для прикидки простая: количество параметров, умноженное на число бит на вес, делённое на восемь, даёт объём в гигабайтах.

Это нижняя оценка для самих значений весов: реальные файлы квантования занимают больше из‑за scale‑факторов, метаданных и особенностей упаковки.

Дальше идёт KV‑кэш, который растёт вместе с длиной контекста и числом одновременных последовательностей, плюс память под активации и рабочие буферы, плюс то, что забирает runtime.

Считать весь объём VRAM доступным под веса нельзя, а универсального коэффициента запаса не существует — он зависит от движка, модели, формата квантования и concurrency.

Ситуация, которая мне это объяснила, знакома любому бэкендеру. Мы прогоняли через локальную модель разбор текстовых описаний операций: задача батчевая, ночная, объём предсказуемый.

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

  • KV‑кэш вырос;

  • память закончилась;

  • очередь остановилась.

Классика — тестировали на медиане, а прилетел хвост распределения.

Вывод, который я оттуда унёс: планировать KV‑кэш надо не по среднему контексту, а по выбранному capacity target — p95 или p99 длины контекста при пиковой, а не средней concurrency.

Это эвристика для планирования, а не формула: фактический размер кэша зависит ещё от числа слоёв, количества KV‑голов, размерности головы и типа хранения KV.

Схема ниже показывает, как обе фазы делят одну подсистему памяти (Рис. 2).

Рис. 2. Схема принципиальная: prefill, decode и память как общий ресурс
Рис. 2. Схема принципиальная: prefill, decode и память как общий ресурс

Главная мысль схемы: у памяти два независимых свойства, и путать их нельзя. Capacity отвечает на вопрос «влезет ли», bandwidth — на вопрос «как быстро». Веса и KV‑кэш забирают один и тот же бюджет памяти и нагружают одну подсистему, но ведут себя по‑разному: веса занимают почти постоянную долю бюджета и многократно участвуют в трафике памяти, а кэш растёт вместе с нагрузкой.

Наблюдение 3. Движок выбирается под форму нагрузки

Здесь я сознательно не делаю рейтинг лучших движков — он бесполезен, потому что победитель меняется вместе с задачей.

Для практического выбора я смотрю как минимум на четыре слоя: планировщик и батчинг, формат и ядра квантования, поддержку конкретного hardware backend и работу с KV‑кэшем.

Планировщик — самая заметная развилка. В vLLM механизм PagedAttention обращается с KV‑кэшем как с виртуальными страницами памяти. В SGLang механизм RadixAttention организует переиспользование уже посчитанного префикса через radix‑дерево.

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

Формат квантования определяет, что вообще влезет. GGUF особенно широко используется в llama.cpp и связанных с ним desktop‑сценариях; GPTQ, AWQ, FP8 и NVFP4 чаще встречаются в GPU‑ориентированных стеках; EXL2 и EXL3 живут в линейке ExLlama.

Таблица ниже — фильтр для первого отбора, а не полный список критериев. Она отвечает не на вопрос «кто это умеет», а на вопрос «с чего я бы начал».

Форма нагрузки

С чего начать

Почему именно так

Один пользователь, эксперименты, ноутбук

llama.cpp или Ollama, формат GGUF

Переносимость, не требует NVIDIA‑only стека, быстрый старт

Apple Silicon

Ollama с MLX‑бэкендом

MLX вышел в preview весной 2026 и стал основным путём на Apple Silicon с версии 0.30 (май 2026)

Команда, общий системный промпт, параллельные запросы

vLLM с кэшем префиксов

PagedAttention плюс переиспользование префикса

Переиспользование префикса — центральная часть нагрузки

SGLang

RadixAttention спроектирован вокруг этого сценария

Модель не влезает в VRAM, это MoE, низкий t/s приемлем

llama.cpp на CPU с большим объёмом RAM

Меньше вычислений на токен при том же объёме памяти

Команда без выделенной эксплуатации

LM Studio

Минимум обслуживания, графический интерфейс

Отдельно отмечу новость, которую многие пропустили. Text Generation Inference от Hugging Face переведён в maintenance mode 11 декабря 2025 года, а 21 марта 2026 года репозиторий заархивирован.

Официальная рекомендация — переходить на vLLM или SGLang. Если у вас в закладках лежал гайд по TGI, он устарел.

Теперь про запуск.

История с EPYC выше — это крайний случай, когда GPU нет вообще. Гораздо чаще встречается смешанная конфигурация: тот же llama.cpp использует видеокарту как основной бэкенд и оставляет в системной RAM то, что не поместилось в VRAM.

Мой вариант, который я обычно использую: llama.cpp для первого запуска и переход на vLLM тогда, когда появляется реальная параллельная нагрузка.

Запуск сервера llama.cpp (bash):

llama-server \
  --model ./models/qwen3-30b-a3b-q4_k_m.gguf \
  --ctx-size 16384 \
  --n-gpu-layers all \
  --parallel 2 \
  --host 0.0.0.0 \
  --port 8080

Два параметра здесь стоит прокомментировать. Значение all у --n-gpu-layers разрешает выгрузить на GPU все слои, но сколько реально там окажется, определяют доступная память и остальные параметры запуска; это честнее магического числа вроде 99, хотя работают оба варианта.

А --parallel задаёт число слотов сервера для одновременной обработки последовательностей, причём слот KV‑кэша выделяется на каждую такую последовательность.

Настройка не бесплатная: рост concurrency напрямую увеличивает требования к памяти. Именно здесь смыкаются наблюдение 2 и выбор движка.

Запуск vLLM (bash):

vllm serve <model-id> \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.90 \
  --enable-prefix-caching \
  --port 8000

Я намеренно оставил здесь шаблон вместо конкретного идентификатора: это паттерн поднятия API, а не готовый рецепт. Для реальной модели набор флагов заметно шире — режим квантования, tensor parallelism, требования к версии vLLM и количеству GPU задаются карточкой конкретного чекпоинта, и с ней надо сверяться перед запуском. И сравнивать цифры между запуском llama.cpp и запуском vLLM бессмысленно: это разные стеки, разные модели и разные форматы квантования.

Оба поднимают OpenAI‑совместимый эндпоинт.

Базовый путь через OpenAI SDK обычно сохраняется: меняется base_url и идентификатор модели.

Но совместимость API не равна идентичности поведения — tool calling, structured outputs, поля reasoning, семантика стриминга и часть параметров сэмплирования у разных движков различаются, и это стоит проверять отдельно.

Пример переключения клиента (Python):

from openai import OpenAI

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

resp = client.chat.completions.create(
    model="<model-id>",
    messages=[{"role": "user", "content": "Сократи описание инцидента до трёх пунктов"}],
)
print(resp.choices[0].message.content)

Практический смысл этой развилки простой: движок меняется за вечер, железо — за квартал и за деньги. Поэтому сначала форма нагрузки, потом движок, и только затем закупка.

Наблюдение 4. Квантование не бесплатно, и цену обычно не измеряют

Четырёхбитное квантование сокращает требования к памяти примерно на три четверти. Звучит как бесплатный обед, но обедом это перестаёт быть довольно быстро.

Показательный пример — сверхнизкая битность с выгрузкой части слоёв в системную RAM.

DeepSeek‑R1 в 1,58-битном динамическом квантовании может запускаться на конфигурации с 24 ГБ VRAM: на GPU помещается лишь несколько слоёв, остальное живёт в RAM. Практически скорость падает до единиц токенов в секунду — категория «технически работает», а не «работает пригодно».

Конкретные цифры здесь бессмысленно приводить без указания модели, билда квантования, версии бэкенда, длины контекста, concurrency и числа слоёв на GPU: без этой методологии любой t/s является случайным числом.

Индустрия ответила на это разумно. Unsloth в методе Dynamic 2.0 использует model‑specific схему квантования вместе с калибровочным набором: более чувствительные части модели сохраняются в большей битности, менее чувствительные квантуются сильнее.

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

Как это делают команды, у которых работает

  1. Гибридный роутинг вместо выбора «или‑или». Рутина и приватные данные идут через локальную модель, сложное и редкое — через облачный API. Цифры здесь есть и вполне конкретные: в опубликованной в 2026 году работе RouteNLP маршрутизация запросов по нескольким уровням моделей дала от 40 до 85 процентов экономии на бенчмарке из шести задач, а в восьминедельном пилоте на потоке около пяти тысяч запросов в день — 58 процентов снижения затрат при сохранении приемлемости ответов и падении p99-задержки с 1847 до 387 миллисекунд. Переносить эти числа на свой случай напрямую нельзя: экономия зависит от доли простых запросов, разницы в цене моделей и качества самого маршрутизатора. Но механика универсальная — дорогая модель перестаёт обрабатывать дешёвые запросы.

  2. Считать TCO, а не объём токенов. Универсального порога окупаемости по количеству токенов не существует: считать нужно стоимость API при вашем соотношении входных и выходных токенов, утилизацию GPU, амортизацию железа, электричество, охлаждение, эксплуатацию и цену задержки. Пример для ориентира: 50 сотрудников по тысяче токенов, 22 рабочих дня — это 1,1 миллиона токенов в месяц. На облачной модели среднего класса при типичных тарифах речь идёт о единицах тысяч рублей в месяц, против миллионов рублей за железо плюс электричество и отдельный человек на обслуживание. При таком профиле стенд не окупается никогда, и это нормальный результат расчёта.

  3. Оптимизировать промпты раньше, чем железо. У нас был heartbeat‑агент, дёргавший модель раз в несколько минут: в наивной реализации он отправлял на каждый вызов весь накопленный state. После сокращения контекста до необходимого минимума размер запроса упал более чем на порядок. Ни рубля вложений в железо.

  4. Включать кэш префиксов и проверять попадания. Если у значимой доли запросов совпадает длинный префикс, prefix caching заметно уменьшает стоимость prefill. Но я видел конфигурации, где кэш формально включён, а попаданий нет: в начало промпта подставлялась текущая дата с точностью до секунды. Префикс переставал совпадать, и механизм превращался в декорацию. Правило простое: всё динамическое — время, идентификаторы запроса, счётчики — должно стоять после кэшируемой части, а не перед ней.

Порядок принятия решения, к которому я в итоге пришёл, показан ниже (Рис. 3).

Рис. 3. План действий: порядок выбора конфигурации стенда
Рис. 3. План действий: порядок выбора конфигурации стенда

Главная мысль схемы: решение про железо принимается последним, а TCO считается только после того, как известен профиль нагрузки — без concurrency, длины контекста и требований к задержке эта цифра не существует.

И начинается всё не с объёма токенов, а с требований к данным: сотня запросов в месяц с чувствительным содержимым может потребовать приватного инференса, тогда как миллионы безобидных запросов прекрасно живут в облаке.

Где это не работает

Стенд не оправдан при малых объёмах и отсутствии требований к приватности — арифметика выше показывает это без вариантов. Он не оправдан, если некому его эксплуатировать: инференс‑сервер это такая же прод‑система, со своими падениями, обновлениями и мониторингом. Он плохо подходит под жёсткий SLA на отклик, если вы пошли по пути «много RAM, мало GPU».

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

У нас это вылилось в отдельный чек‑лист релиза, потому что «обновили модель и стало хуже» — диагноз, который без замеров не ставится.

И отдельно: чужие цифры производительности стоит воспринимать как ориентир, а не как обещание. Заложите вечер на замеры своей конфигурации — скучно, но дешевле, чем принять чужой бенчмарк за свой.

Практический вывод: что делать со всем этим

  • Разделять prefill и decode при разговоре о скорости. Мерить TTFT, пропускную способность prefill и скорость decode отдельно, фиксировать concurrency и длину контекста, смотреть на p95, а не на среднее.

  • Начинать выбор с требований к данным, затем описывать профиль нагрузки и только потом считать TCO. Универсального порога окупаемости по токенам не существует.

  • Планировать память от KV‑кэша на p95 контекста при пиковой concurrency, а не от размера файла с весами.

  • Проверить, MoE ли модель. Если да, вариант с большим объёмом обычной RAM перестаёт быть экзотикой — но скорость будет низкой.

  • Выбирать движок под форму нагрузки, а не по рейтингу из обзора. Проверить, что prefix caching действительно даёт попадания.

  • Замерить качество на своих задачах до и после квантования. Без этого деградацию через месяц будут искать в коде.

А главный вывод, ради которого я всё это разбирал, звучит так: локальный инференс перестал быть историей про то, у кого дороже видеокарта. Он стал историей про то, кто правильно посчитал память и трафик. Это хорошая новость — считать умеет любой инженер, а покупать топовое железо может не каждый.

Источники

Если вы уже столкнулись с тем, что локальная LLM работает медленнее ожидаемого, а выбор железа и настроек превращается в перебор вариантов, важно сначала разобраться в архитектуре и ограничениях AI‑систем.

На бесплатных уроках разберём, как запускать LLM, интегрировать ИИ и выбирать подходящую архитектуру:

  • 22 сентября в 18:00. «Ландшафт современного NLP: от эмбеддингов и классических ML‑методов до современных LLM». Записаться

  • 1 октября в 20:00. «Kagent + Ollama: ИИ‑агент для работы с Kubernetes». Записаться

  • 15 октября в 20:00. «Security by Design: архитектура для защиты ИИ‑систем». Записаться

Комментарии (1)


  1. Moog_Prodigy
    18.09.2026 17:35

    Нейрослоп! Можно не читать.