Всем привет, меня зовут Сергей Прощаев, я Tech Lead и руководитель направления Java/Kotlin‑разработки в FinTech & E‑commerce, преподаю на курсах разработки и архитектуры в ОТУС.
В этой статье расскажу про домашний стенд для локального инференса: почему его часто собирают не от профиля нагрузки и что на самом деле упирается в потолок.
Знакомая картина: вы купили видеокарту подороже, подняли на ней модель, запустили — и получили заметно меньше токенов в секунду, чем обещали в обзорах. При этом карта не греется, загрузка GPU скачет от тридцати до пятидесяти процентов, вентиляторы молчат.
Железо явно простаивает, а модель еле ползёт.
Первая мысль — что‑то настроено криво.
Вторая — что надо было брать карту ещё дороже.
Обе неверные, и ниже я разберу почему.

Крючок: 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 упираются в разное
Начну с уточнения, которое снимает половину путаницы. Инференс состоит из двух непохожих фаз.
Prefill — обработка входного контекста. Здесь заметную роль играют вычисления и attention, и при длинном промпте фаза вполне может быть compute‑bound.
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).

Главная мысль схемы: у памяти два независимых свойства, и путать их нельзя. 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 схему квантования вместе с калибровочным набором: более чувствительные части модели сохраняются в большей битности, менее чувствительные квантуются сильнее.
Самая частая беда при этом не в скорости, а в качестве, и выглядит она одинаково: команда берёт самый маленький файл, который влез в память, деградацию никто не измеряет, а через месяц ухудшение ответов ищут в коде приложения.
Как это делают команды, у которых работает
Гибридный роутинг вместо выбора «или‑или». Рутина и приватные данные идут через локальную модель, сложное и редкое — через облачный API. Цифры здесь есть и вполне конкретные: в опубликованной в 2026 году работе RouteNLP маршрутизация запросов по нескольким уровням моделей дала от 40 до 85 процентов экономии на бенчмарке из шести задач, а в восьминедельном пилоте на потоке около пяти тысяч запросов в день — 58 процентов снижения затрат при сохранении приемлемости ответов и падении p99-задержки с 1847 до 387 миллисекунд. Переносить эти числа на свой случай напрямую нельзя: экономия зависит от доли простых запросов, разницы в цене моделей и качества самого маршрутизатора. Но механика универсальная — дорогая модель перестаёт обрабатывать дешёвые запросы.
Считать TCO, а не объём токенов. Универсального порога окупаемости по количеству токенов не существует: считать нужно стоимость API при вашем соотношении входных и выходных токенов, утилизацию GPU, амортизацию железа, электричество, охлаждение, эксплуатацию и цену задержки. Пример для ориентира: 50 сотрудников по тысяче токенов, 22 рабочих дня — это 1,1 миллиона токенов в месяц. На облачной модели среднего класса при типичных тарифах речь идёт о единицах тысяч рублей в месяц, против миллионов рублей за железо плюс электричество и отдельный человек на обслуживание. При таком профиле стенд не окупается никогда, и это нормальный результат расчёта.
Оптимизировать промпты раньше, чем железо. У нас был heartbeat‑агент, дёргавший модель раз в несколько минут: в наивной реализации он отправлял на каждый вызов весь накопленный state. После сокращения контекста до необходимого минимума размер запроса упал более чем на порядок. Ни рубля вложений в железо.
Включать кэш префиксов и проверять попадания. Если у значимой доли запросов совпадает длинный префикс, prefix caching заметно уменьшает стоимость prefill. Но я видел конфигурации, где кэш формально включён, а попаданий нет: в начало промпта подставлялась текущая дата с точностью до секунды. Префикс переставал совпадать, и механизм превращался в декорацию. Правило простое: всё динамическое — время, идентификаторы запроса, счётчики — должно стоять после кэшируемой части, а не перед ней.
Порядок принятия решения, к которому я в итоге пришёл, показан ниже (Рис. 3).

Главная мысль схемы: решение про железо принимается последним, а TCO считается только после того, как известен профиль нагрузки — без concurrency, длины контекста и требований к задержке эта цифра не существует.
И начинается всё не с объёма токенов, а с требований к данным: сотня запросов в месяц с чувствительным содержимым может потребовать приватного инференса, тогда как миллионы безобидных запросов прекрасно живут в облаке.
Где это не работает
Стенд не оправдан при малых объёмах и отсутствии требований к приватности — арифметика выше показывает это без вариантов. Он не оправдан, если некому его эксплуатировать: инференс‑сервер это такая же прод‑система, со своими падениями, обновлениями и мониторингом. Он плохо подходит под жёсткий SLA на отклик, если вы пошли по пути «много RAM, мало GPU».
Есть и менее очевидное ограничение — организационное. Стенд снимает счёт за API, но добавляет постоянную работу: обновления движка, миграции на новые веса, регрессии после смены квантования.
У нас это вылилось в отдельный чек‑лист релиза, потому что «обновили модель и стало хуже» — диагноз, который без замеров не ставится.
И отдельно: чужие цифры производительности стоит воспринимать как ориентир, а не как обещание. Заложите вечер на замеры своей конфигурации — скучно, но дешевле, чем принять чужой бенчмарк за свой.
Практический вывод: что делать со всем этим
Разделять prefill и decode при разговоре о скорости. Мерить TTFT, пропускную способность prefill и скорость decode отдельно, фиксировать concurrency и длину контекста, смотреть на p95, а не на среднее.
Начинать выбор с требований к данным, затем описывать профиль нагрузки и только потом считать TCO. Универсального порога окупаемости по токенам не существует.
Планировать память от KV‑кэша на p95 контекста при пиковой concurrency, а не от размера файла с весами.
Проверить, MoE ли модель. Если да, вариант с большим объёмом обычной RAM перестаёт быть экзотикой — но скорость будет низкой.
Выбирать движок под форму нагрузки, а не по рейтингу из обзора. Проверить, что prefix caching действительно даёт попадания.
Замерить качество на своих задачах до и после квантования. Без этого деградацию через месяц будут искать в коде.
А главный вывод, ради которого я всё это разбирал, звучит так: локальный инференс перестал быть историей про то, у кого дороже видеокарта. Он стал историей про то, кто правильно посчитал память и трафик. Это хорошая новость — считать умеет любой инженер, а покупать топовое железо может не каждый.
Источники
NVIDIA H100: спецификации SXM, пропускная способность памяти и производительность тензорных ядер
DeepSeek‑V3 Technical Report (arXiv): архитектура DeepSeekMoE, shared и routed эксперты
llama.cpp: документация по multi‑GPU и параметрам
--n-gpu-layers,--parallelHugging Face: официальная страница TGI о переводе в maintenance mode и рекомендации по миграции
vLLM Docs: automatic prefix caching и переиспользование блоков KV‑кэша
RouteNLP: маршрутизация запросов по уровням моделей, экономия и задержки
Notebookcheck: опубликованный эксперимент с DeepSeek‑R1 671B на двух EPYC без GPU
Cloud.ru на Хабре: оптимизация LLM‑инференса, соотношение вычислений и пропускной способности памяти
vc.ru / provod.AI: расчёты окупаемости своей GPU и гибридный роутинг

Если вы уже столкнулись с тем, что локальная LLM работает медленнее ожидаемого, а выбор железа и настроек превращается в перебор вариантов, важно сначала разобраться в архитектуре и ограничениях AI‑систем.
На бесплатных уроках разберём, как запускать LLM, интегрировать ИИ и выбирать подходящую архитектуру:
22 сентября в 18:00. «Ландшафт современного NLP: от эмбеддингов и классических ML‑методов до современных LLM». Записаться
1 октября в 20:00. «Kagent + Ollama: ИИ‑агент для работы с Kubernetes». Записаться
15 октября в 20:00. «Security by Design: архитектура для защиты ИИ‑систем». Записаться
Moog_Prodigy
Нейрослоп! Можно не читать.