MAATRIX / Блог / Сколько запросов в секунду вытянет локальная модель: методика замера на своём железе

Сколько запросов в секунду вытянет локальная модель: методика замера на своём железе

MAATRIX

Модель отвечает быстро, когда вы один задаёте ей вопросы в чате. Проблема начинается, когда её включают в продакшн: десять, тридцать, сто пользователей одновременно — и внезапно сервис захлёбывается, хотя на демо всё летало. Разница между «модель быстрая» и «модель выдержит нагрузку» — это разница между двумя разными метриками, и путать их — самая частая ошибка при планировании железа под локальный инференс. Дальше — рабочая методика, как измерить реальную пропускную способность именно на вашей связке модели, движка и GPU, не полагаясь на чужие бенчмарки и маркетинговые цифры производителей карт.

Зачем мерить самому, а не доверять характеристикам

Паспортные цифры GPU — пиковая производительность в терафлопсах, заявленная пропускная способность памяти — описывают теоретический потолок железа в вакууме. До него не дотягивает ни одна реальная нагрузка: часть ресурсов уходит на планировщик движка инференса, на KV-кэш, на накладные расходы фреймворка, на прогрев CUDA-графов, на то, как конкретная версия драйвера обращается с конкретной архитектурой модели. Два одинаковых по паспорту сервера с разными версиями vLLM или разными сборками llama.cpp могут показывать заметно разную производительность на одной и той же модели.

Чужой бенчмарк из блога или репозитория тоже не переносится один в один: там другая модель (пусть даже с тем же названием — важна ревизия и квантизация), другая длина контекста, другой профиль нагрузки, часто другая версия движка. Единственный способ узнать, сколько запросов в секунду выдержит именно ваша система, — прогнать нагрузочный тест на этой самой системе, с этой самой моделью, в конфигурации, близкой к боевой. Это не займёт больше пары часов, а решение «докупать GPU или квантовать модель» после такого замера принимается на фактах, а не на догадках.

Latency одного запроса и throughput при параллельной нагрузке — не одна и та же метрика

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

Latency — это время, которое проходит от отправки запроса до получения полного ответа (или до первого токена — метрика TTFT, time to first token — и до устойчивого темпа выдачи следующих токенов, inter-token latency). Она измеряется на одном-единственном запросе, без конкуренции за ресурсы.

Throughput — это суммарное число токенов или запросов в секунду, которое система обрабатывает при параллельной нагрузке от нескольких клиентов одновременно. Она растёт вместе с числом параллельных запросов — но не бесконечно.

Ключевой нюанс: рост throughput почти всегда идёт за счёт latency отдельного запроса. Пока GPU обслуживает одного клиента, все вычислительные блоки и вся пропускная способность памяти отданы ему одному — отсюда минимальная задержка. Как только приходит второй, третий, десятый параллельный запрос, движок инференса (vLLM, TGI, SGLang и другие серверы с непрерывным батчингом) объединяет их в общий батч на каждом шаге декодирования. Суммарный throughput по всем клиентам растёт, а время ответа каждому конкретному клиенту увеличивается — GPU распределяет один и тот же проход по весам между несколькими независимыми последовательностями токенов.

Отсюда вывод: нельзя мерить «скорость модели» одним числом. Нужна пара цифр вида «при N параллельных запросах — X токенов/с суммарно и Y мс на пользователя», причём для разных N. Ответ на вопрос «сколько запросов в секунду вытянет модель» — это кривая, а не одно число.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Что зафиксировать перед тем как мерить

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

  • Версия и конфигурация движка инференса — llama.cpp, vLLM, SGLang, TGI и так далее, с точной версией. У каждого свой планировщик батчей и своя эффективность.
  • Файл модели и квантизация — не просто «модель X», а конкретный чекпоинт и формат (GGUF Q4_K_M, GPTQ, AWQ, FP8 и т.д.). Про сам эффект квантизации — ниже.
  • Число слоёв на GPU и объём выделенного VRAM — если часть слоёв выгружена на CPU, картина будет совсем другой (см. статью про выбор CPU или GPU для локальной LLM).
  • Профиль запросов — короткий чат-диалог на 200-500 токенов контекста или длинный RAG-запрос на несколько тысяч. Это принципиально разная нагрузка.
  • max_tokens на генерацию — фиксированная длина ответа в тесте, иначе результаты не сравнить между прогонами.
  • Состояние GPU перед тестом — на карте не должно параллельно крутиться ничего постороннего; проверьте nvidia-smi, что VRAM свободна.

Держите эти параметры записанными рядом с результатами — при обновлении версии движка или модели вы захотите повторить тест и сравнить.

Методика замера шаг за шагом

Шаг 1. Базовая latency на одном запросе. Отправьте один запрос без конкуренции и зафиксируйте TTFT и скорость генерации токенов в одиночном режиме. Это ваша точка отсчёта — «пол» задержки, лучше которого при параллельной нагрузке уже не будет.

Для движков с OpenAI-совместимым API это можно замерить обычным curl с замером времени:

curl -s -w "\ntotal: %{time_total}s\n" \
  -X POST http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"your-model","messages":[{"role":"user","content":"Опиши в трёх предложениях..."}],"max_tokens":128,"stream":false}'

Для llama.cpp есть встроенный llama-bench, который сам прогоняет серию замеров prefill и генерации на заданных длинах контекста и печатает токены в секунду для каждой комбинации — быстрый способ проверить одиночный запрос без поднятия HTTP-сервера.

Шаг 2. Throughput при параллельной нагрузке. Дальше нужен инструмент, который умеет слать N одновременных запросов и агрегировать результат: универсальные HTTP-утилиты (hey, wrk, k6, locust) либо специализированный скрипт бенчмарка серверного режима, который идёт в комплекте с движком — например, у vLLM он умеет генерировать синтетические или взятые из датасета запросы с заданным числом параллельных клиентов и на выходе даёт агрегированный throughput, TTFT и inter-token latency по перцентилям.

Прогоните серию тестов с разным числом параллельных клиентов: 1, 2, 4, 8, 16, 32... и для каждого уровня зафиксируйте суммарный throughput (токенов/с и запросов/с) и медианную/p95 latency. По этим точкам постройте кривую «throughput от параллелизма» — она почти всегда выглядит так: сначала рост почти линейный, затем замедление, затем плато или деградация из-за переполнения очереди. Точка, где рост throughput перестаёт быть пропорционален росту числа клиентов, — это точка насыщения системы, и она отвечает на вопрос «сколько запросов в секунду вытянет модель» в вашей конфигурации.

Шаг 3. Закрытый и открытый цикл нагрузки — не путайте их. В закрытом цикле (closed-loop) N виртуальных клиентов ждут ответа на предыдущий запрос, прежде чем отправить следующий, — так удобно находить максимальный устойчивый throughput при разном уровне параллелизма, это и есть шаг 2. В открытом цикле (open-loop) запросы приходят с фиксированной интенсивностью независимо от того, ответила ли система на предыдущие, — это ближе к реальному трафику продакшна и хорошо показывает, что происходит при перегрузке: очередь растёт, а latency разгоняется лавинообразно, а не плавно. Перед боевой нагрузкой с непредсказуемыми всплесками стоит прогнать оба режима — открытый цикл честнее показывает, как система ведёт себя не под контролируемой, а под реальной внешней нагрузкой.

Шаг 4. Мониторинг во время теста. Пока тест идёт, параллельно снимайте показания GPU:

nvidia-smi dmon -s pucvmet -d 1

Эта команда каждую секунду печатает загрузку вычислительных блоков, использование памяти, температуру и мощность. Эти данные понадобятся на шаге поиска узкого места — без них throughput-число само по себе не объясняет, почему система упёрлась именно в этот потолок.

Как длина контекста и профиль промпта меняют картину

Одна и та же модель на коротких чат-репликах и на длинных RAG-запросах с документами ведёт себя по-разному, и тестировать нужно оба профиля отдельно.

Обработка входного промпта (prefill) и генерация ответа (decode) — это два разных по нагрузке этапа. Prefill считает сразу для всех токенов промпта параллельно за один проход — это вычислительно тяжёлая, compute-bound операция, и её время растёт вместе с длиной промпта. Decode генерирует токены по одному, и это, как правило, упирается в пропускную способность памяти, а не в вычисления — подробнее механика разобрана в статье про пропускную способность памяти как потолок локальной LLM. Значит, короткий системный промпт с длинным ответом и длинный RAG-контекст с коротким ответом нагружают железо совершенно по-разному, даже если суммарное число токенов похоже.

Здесь же прячется менее очевидная зависимость: каждый параллельный диалог держит в VRAM свой KV-кэш, объём которого растёт линейно с длиной контекста. Чем длиннее промпты в профиле нагрузки, тем меньше параллельных запросов помещается в ту же видеопамять — и тем ниже потолок throughput при той же карте. Механика KV-кэша и то, почему именно он часто оказывается ограничителем числа параллельных пользователей, разобрана в статье про KV-кэш и почему диалог ест VRAM.

Практический вывод: замеряйте throughput отдельно на реалистично коротком профиле (обычный чат) и на реалистично длинном (RAG, агенты с историей инструментов) — если продакшн-нагрузка смешанная, сделайте третий тест со смешанным профилем, ближе всего к тому, что реально пойдёт на сервер.

Роль квантизации в пропускной способности

Квантизация — снижение точности хранения весов модели (FP16/BF16 → INT8 → INT4 и промежуточные схемы вроде Q4_K_M, Q5_K_M, GPTQ, AWQ, FP8 на новых картах) — влияет на throughput сразу по двум каналам, и оба стоит проверять раздельно.

Во-первых, меньше весов — меньше байт нужно прочитать из памяти на каждом шаге decode. Поскольку generation в основном memory-bound, уменьшение объёма весов почти пропорционально повышает потолок токенов в секунду для одного потока — это прямое следствие формулы «скорость ≈ пропускная способность памяти / объём весов», разобранной в статье про потолок памяти выше.

Во-вторых, и это часто более заметный на практике эффект: меньший объём весов освобождает VRAM под KV-кэш. Это значит, что при той же карте можно держать в разы больше параллельных диалогов одновременно, прежде чем упереться в лимит памяти, — то есть суммарный throughput при параллельной нагрузке растёт не столько из-за более быстрого чтения весов, сколько из-за того, что помещается больше параллельных запросов. Сравнение этого эффекта на разных степенях квантизации разобрано в статье про Q4/Q5/Q8-квантизацию.

Важная оговорка: выигрыш в скорости от квантизации не универсален и зависит от того, поддерживает ли конкретный движок и конкретное железо быстрые ядра для этой схемы — где-то INT4-умножение действительно быстрее FP16, а где-то движок просто разворачивает веса обратно в FP16 перед вычислением, и выигрыш ограничивается только экономией памяти. Единственный надёжный способ узнать, какой эффект у вас, — прогнать шаги 1-2 методики на одной модели в разных квантизациях и сравнить кривые «throughput от параллелизма» напрямую. Не забывайте и проверить качество ответов после квантизации отдельным прогоном на своём наборе тестовых вопросов — экономия VRAM не должна обесцениваться заметной деградацией ответов.

Где узкое место: VRAM, вычисления или память

Число «X запросов в секунду» без понимания, почему система упёрлась именно в это число, бесполезно для планирования — вы не поймёте, что докупать или менять. Диагностика по показаниям nvidia-smi dmon, снятым во время теста из шага 4, обычно даёт один из трёх ответов.

Упор в пропускную способность памяти. Типичная картина для decode на плотных (не MoE) моделях: загрузка вычислительных блоков держится на скромном уровне, а прирост throughput замедляется задолго до того, как вычисления выглядят загруженными под завязку. Лечится более быстрой памятью (другая карта), уменьшением объёма весов (квантизация) или сокращением числа параллельных длинных контекстов.

Упор в вычисления. Проявляется чаще на обработке длинных промптов (prefill) или на очень больших батчах — загрузка вычислительных блоков подходит к максимуму синхронно с остановкой роста throughput. Помогает более мощная карта по вычислениям либо сокращение доли compute-bound работы — например, кэширование повторяющихся системных промптов.

Упор в объём VRAM. Самый частый на практике сценарий: движок начинает отклонять запросы или падать с ошибкой нехватки памяти задолго до того, как вычисления или память выглядят перегруженными. Предел параллелизма определяется не скоростью железа, а тем, сколько KV-кэша помещается в видеопамять. Решения — квантизация модели, уменьшение максимальной длины контекста на запрос, вытеснение части кэша на CPU (если движок это поддерживает) либо карта большего объёма. Именно этот сценарий чаще всего стоит за жалобами вида «модель внезапно начала отвечать минутами» при росте числа одновременных пользователей.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Сколько времени занимает полноценный замер?

Базовая latency и throughput-кривая на 5-6 уровнях параллелизма обычно укладываются в час-полтора, включая настройку инструмента. Отдельный прогон на длинном контексте и проверку квантизаций закладывайте как ещё полтора-два часа.

Нужно ли тестировать на проде или можно на staging-копии?

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

Что делать, если throughput ниже, чем нужно, а докупить GPU нельзя?

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

Можно ли доверять числу «токенов в секунду» из документации к модели или движку?

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

Отличается ли методика для CPU-инференса?

Подход тот же — latency на одном запросе, throughput на серии уровней параллелизма, мониторинг ресурсов, — но параллельный батчинг на CPU почти всегда даёт заметно меньший прирост, чем на GPU, из-за куда более скромной пропускной способности оперативной памяти.

Обсудить статью, задать вопрос или начать новую тему

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

Перейти в сообщество →