Инфраструктура локальных моделей
Планирование мощности для локальной LLM: от длины контекста до очереди запросов
Размер весов отвечает только на вопрос, поместится ли модель в память в состоянии покоя. Рабочая система дополнительно хранит KV-кэш, обслуживает несколько последовательностей, формирует динамические пакеты и накапливает очередь. Поэтому конфигурацию следует выбирать не по одной цифре VRAM, а по измеренному профилю задержки и пропускной способности.
Что именно нужно спланировать
Для интерактивного сервиса недостаточно получить максимальное число токенов в секунду. Пользователь сначала ждет начала ответа, а затем оценивает скорость генерации. Эти фазы создают разные нагрузки:
- prefill обрабатывает входной контекст; его стоимость растет с длиной запроса;
- decode последовательно генерирует выходные токены и использует уже созданный KV-кэш;
- очередь добавляет задержку еще до начала вычислений;
- параллельные последовательности делят вычислительные ресурсы и память;
- пакетная обработка может повысить общий throughput ценой задержки отдельного запроса.
Планирование мощности сводится к проверяемому условию: при заданном распределении длин входа и выхода, целевой интенсивности и допустимой очереди система выполняет соглашение об уровне сервиса.
Оценка памяти до запуска теста
Разделите потребление памяти на четыре части:
M_total ≈ M_weights + M_runtime + M_KV + M_margin
M_weights включает веса модели после выбранной квантизации. M_runtime — буферы вычислительного графа, библиотеки и временные тензоры. M_KV зависит от архитектуры, формата кэша, числа активных последовательностей и фактически занятых токенов. M_margin нужен для колебаний нагрузки и предотвращения аварийного исчерпания памяти.
Для распространенной схемы хранения приблизительный объем KV-кэша можно выразить так:
M_KV ≈ L × T × B × 2 × H_kv × D_head × S
L— число слоев;T— число сохраненных токенов на последовательность;B— число одновременно активных последовательностей;2— ключи и значения;H_kv— число KV-голов;D_head— размерность головы;S— байт на элемент кэша.
Это оценка, а не универсальная формула фактического выделения памяти. Сервер может резервировать кэш блоками, переиспользовать префиксы, выгружать страницы или хранить KV в другом формате. Архитектурные параметры берите из локальной конфигурации конкретной модели, а итог проверяйте телеметрией процесса.
Почему максимальный контекст вводит в заблуждение
Настройка окна в 32 000 токенов не означает, что каждый запрос занимает весь объем. Но если сервер заранее резервирует память исходя из максимума, слишком большое окно уменьшит допустимую параллельность. Для теста нужны не только максимум, но и распределение: например, медиана, 90-й и 99-й процентили длины входа и выхода.
Шаг 1. Зафиксируйте профиль нагрузки
Соберите обезличенную статистику реального приложения. Не переносите в тест пользовательские тексты, секреты, токены доступа или персональные данные. Для измерения вычислительной нагрузки достаточно синтетических текстов с тем же числом токенов.
Запишите профиль в отдельный файл. Ниже — только пример структуры; значения не являются рекомендацией или результатом измерения:
{
"model_id": "LOCAL_MODEL_ID",
"tokenizer_revision": "LOCAL_REVISION",
"input_tokens": {
"p50": 512,
"p90": 2048,
"p99": 4096
},
"output_tokens": {
"p50": 128,
"p90": 384,
"p99": 768
},
"arrival_pattern": "poisson",
"target_requests_per_second": 1.0,
"burst_requests": 8,
"slo": {
"ttft_p95_ms": 1500,
"itl_p95_ms": 80,
"e2e_p95_ms": 12000,
"error_rate_max": 0.01
}
}
Сохраните также шаблон промпта, параметры семплирования, stop-последовательности и лимит вывода. Они влияют на фактическую длину ответа. Если приложение использует несколько классов запросов — например, короткий чат и анализ документов — тестируйте смесь классов, а не их усредненный «идеальный» запрос.
Шаг 2. Зафиксируйте окружение
Воспроизводимость требует записать модель, квантизацию, версию сервера, драйвер, ускоритель и параметры запуска. Следующие команды только читают состояние системы и сохраняют вывод в текущем каталоге:
mkdir -p capacity-results
date --iso-8601=seconds > capacity-results/environment.txt
uname -a >> capacity-results/environment.txt
if command -v nvidia-smi >/dev/null 2>&1; then
nvidia-smi --query-gpu=name,uuid,driver_version,memory.total \
--format=csv,noheader >> capacity-results/environment.txt
fi
if command -v rocm-smi >/dev/null 2>&1; then
rocm-smi --showproductname --showdriverversion --showmeminfo vram \
>> capacity-results/environment.txt
fi
Добавьте к отчету точную команду запуска сервера, но удалите из нее ключи и секреты. Зафиксируйте ограничения контекста, формат KV-кэша, число параллельных последовательностей, размер пакетного бюджета и долю памяти, доступную процессу.
Шаг 3. Подготовьте генератор нагрузки
Клиент должен работать по HTTP только с явно указанным локальным адресом, ограничивать время ожидания и сохранять необработанные измерения. Не используйте публичный endpoint случайно: задайте адрес через переменную окружения и проверьте его перед стартом.
export LLM_BASE_URL="http://127.0.0.1:8000"
export LLM_MODEL="LOCAL_MODEL_ID"
case "$LLM_BASE_URL" in
http://127.0.0.1:*|http://localhost:*) ;;
*) echo "Отказ: разрешен только локальный endpoint" >&2; exit 1 ;;
esac
curl --fail --silent --show-error \
--connect-timeout 2 \
--max-time 10 \
"$LLM_BASE_URL/health"
Путь проверки здоровья зависит от локального сервера. Если он не поддерживается, замените его документированным локальным endpoint. Не считайте успешное открытие TCP-порта доказательством готовности модели.
Что обязан измерять клиент
- монотонное время отправки запроса;
- время получения первого фрагмента с содержимым;
- времена последующих токенов или потоковых фрагментов;
- время завершения, статус и тип ошибки;
- число входных и выходных токенов по тому же токенизатору, что использует сервер;
- идентификатор класса запроса и заданный уровень параллельности.
Не подменяйте токены количеством слов или символов. Если потоковый протокол группирует несколько токенов в одном событии, клиентская ITL отражает интервал между событиями, а не точную межтокенную задержку. Это ограничение следует отметить в отчете.
Шаг 4. Проведите ступенчатый тест
- Прогрев. Отправьте несколько запросов каждого класса, не включая их в итоговые процентили. Цель — завершить загрузку модели, компиляцию и выделение рабочих буферов.
- Базовая линия. Измерьте одиночный запрос для каждой контрольной длины входа и выхода. Это отделяет вычислительную задержку от очереди.
- Закрытый тест. Запустите фиксированное число одновременных клиентов: 1, 2, 4, 8 и далее, пока не нарушится SLO или не возникнет нехватка памяти. Новый запрос клиента начинается после завершения предыдущего.
- Открытый тест. Генерируйте прибытия с целевой интенсивностью независимо от завершения прежних запросов. Такой режим обнаруживает неограниченный рост очереди.
- Всплеск. Одновременно отправьте ожидаемое число запросов из кратковременного пика и измерьте время восстановления очереди.
- Длительный прогон. На выбранной рабочей точке проверьте устойчивость, отсутствие роста памяти и накопления очереди.
Каждую ступень проводите отдельно и записывайте конфигурацию рядом с результатом:
{
"run_id": "example-c4-r1",
"concurrency": 4,
"offered_rps": 0.8,
"duration_seconds": 600,
"warmup_requests": 10,
"seed": 42,
"server_config_sha256": "LOCAL_HASH",
"workload_config_sha256": "LOCAL_HASH"
}
Это пример формата метаданных. Хеши вычисляйте для реально использованных файлов:
sha256sum server-config.json workload.json \
> capacity-results/config-sha256.txt
Почему нужны оба режима
Закрытый тест удобен для поиска предела вычислительной эффективности, но маскирует перегрузку: медленный сервер автоматически снижает частоту новых запросов. Открытый тест сохраняет предложенную интенсивность и показывает, стабилизируется ли очередь. Если входящий поток длительно превышает обслуживаемый, средняя задержка перестает быть устойчивой независимо от объема памяти.
Шаг 5. Снимайте серверную телеметрию
Параллельно с клиентскими временами собирайте:
- занятую и зарезервированную память ускорителя;
- утилизацию вычислительных блоков;
- число выполняющихся и ожидающих запросов;
- занятость KV-кэша, если сервер публикует такую метрику;
- размер текущего пакета и число обработанных prefill/decode-токенов;
- ошибки выделения памяти, отмены и тайм-ауты.
Для NVIDIA безопасный локальный сбор можно выполнить так:
nvidia-smi \
--query-gpu=timestamp,index,utilization.gpu,memory.used,memory.total \
--format=csv \
--loop-ms=1000 \
> capacity-results/gpu-telemetry.csv
Остановите процесс сочетанием Ctrl+C после теста. Для другого ускорителя используйте штатный инструмент платформы. Частота опроса сама создает нагрузку, поэтому не меняйте ее между сравниваемыми прогонами.
Шаг 6. Найдите рабочую точку
Для каждой ступени рассчитайте минимум:
- p50, p95 и p99 для TTFT, ITL и полной задержки;
- фактически принятые и завершенные запросы в секунду;
- входные и выходные токены в секунду отдельно;
- долю ошибок и тайм-аутов;
- максимальную и типичную длину очереди;
- пиковую память и запас до лимита.
Процентили вычисляйте по отдельным запросам, не по средним значениям временных окон. Ошибочные и оборванные запросы не удаляйте молча: учитывайте их в error rate и публикуйте правило обработки неполных измерений.
Признаки насыщения
Система близка к пределу, если наблюдается хотя бы один из следующих эффектов:
- TTFT резко растет, хотя скорость decode меняется мало — вероятно, увеличилась очередь или конкуренция prefill;
- ITL ухудшается с ростом параллельности — вычислительный ресурс делится между слишком большим числом последовательностей;
- throughput перестает расти, а задержка продолжает увеличиваться;
- KV-кэш вытесняется, пересчитывается или выгружается;
- память приближается к лимиту и появляются отказы выделения;
- после всплеска очередь не возвращается к исходному уровню.
Выбирайте не последнюю успешную ступень, а предыдущую устойчивую конфигурацию с запасом. Величина запаса зависит от изменчивости профиля и требований сервиса; универсального процента нет.
Как связать тест с очередью
Обозначим интенсивность поступления запросов как λ, а устойчивую интенсивность обслуживания измеренной конфигурации как μ. Условие λ < μ необходимо, но недостаточно для хорошей задержки: при приближении λ к μ очередь становится чувствительной к разбросу длин запросов и всплескам.
Не применяйте простую формулу очереди как замену нагрузочному тесту: длительности обслуживания здесь неодинаковы, batching меняет их во времени, а длинные prefill могут влиять на короткие decode. Модель очереди полезна для первичной оценки, но рабочую точку подтверждает только тест с реальным распределением запросов.
Политики перегрузки
Продакшен-сервис должен иметь явные пределы:
- максимальную длину очереди;
- тайм-аут ожидания и полный deadline запроса;
- ограничение входных и выходных токенов;
- контролируемый отказ при перегрузке;
- раздельные очереди или приоритеты для несовместимых классов нагрузки.
Неограниченная очередь не увеличивает мощность: она превращает перегрузку в растущую задержку и расход памяти.
Как подобрать конфигурацию
| Наблюдение | Вероятная причина | Что проверить следующим прогоном |
|---|---|---|
| Модель не помещается без нагрузки | Веса и runtime превышают доступную память | Квантизацию, распределение по устройствам или меньшую модель |
| Одиночный длинный запрос вызывает OOM | Недостаточно места под KV и временные буферы | Меньшее окно, формат KV, лимит входа или дополнительную память |
| Память заканчивается только при конкуренции | Слишком много одновременных последовательностей | Ниже concurrency, меньший контекст или другой бюджет KV-кэша |
| GPU загружен слабо, очередь растет | Узкое место вне вычислений | Токенизацию, CPU, копирование данных, сериализацию и scheduler |
| Throughput растет, TTFT нарушает SLO | Слишком агрессивный batching | Меньший пакетный бюджет или меньшее ожидание формирования пакета |
| Короткие запросы страдают рядом с длинными | Блокировка длинным prefill | Chunked prefill, отдельные очереди или разные реплики |
Меняйте один существенный параметр за прогон. Иначе нельзя определить, что именно дало улучшение. После выбора параметров повторите исходную смесь нагрузки целиком.
Проверка результата
Конфигурация считается подтвержденной, если одновременно выполнены следующие условия:
- контрольные суммы конфигурации и профиля нагрузки сохранены;
- после прогрева проведено несколько сопоставимых прогонов;
- целевые p95 и p99 выполняются на реальной смеси длин;
- ошибки и тайм-ауты не превышают заданный предел;
- при целевой интенсивности очередь не имеет устойчивого восходящего тренда;
- после контрольного всплеска очередь возвращается к нормальному уровню;
- пиковая память остается ниже лимита с выбранным запасом;
- длительный прогон не показывает постепенного роста памяти или деградации задержки.
Итоговый отчет удобно свести к одной таблице:
configuration,input_mix,offered_rps,completed_rps,ttft_p95_ms,itl_p95_ms,e2e_p95_ms,error_rate,peak_memory_mib,max_queue,slo_pass
example-c4,mixed,0.8,LOCAL_VALUE,LOCAL_VALUE,LOCAL_VALUE,LOCAL_VALUE,LOCAL_VALUE,LOCAL_VALUE,LOCAL_VALUE,LOCAL_VALUE
LOCAL_VALUE означает место для фактического результата. Эти значения нельзя заполнять предположениями.
Типовые ошибки
- Считать только веса. Такой расчет игнорирует KV-кэш, runtime и временные буферы.
- Тестировать один короткий промпт. Он не воспроизводит prefill длинных запросов и рост памяти.
- Публиковать только среднюю задержку. Среднее скрывает хвост распределения и ожидание в очереди.
- Путать concurrency и RPS. Число клиентов не задает фиксированную интенсивность в закрытом тесте.
- Считать потоковые события токенами. Сервер или транспорт может объединять токены.
- Не отделять прогрев. Первый запуск может включать загрузку, компиляцию и выделение памяти.
- Сравнивать разные профили. Throughput без длин входа и выхода не позволяет сравнить конфигурации.
- Подбирать batch только по максимуму throughput. Это часто ухудшает TTFT и хвостовую задержку.
- Не задавать предел очереди. При перегрузке система продолжает принимать работу, которую не успеет выполнить в deadline.
- Записывать секреты в артефакты. Команды запуска и заголовки клиента необходимо очищать перед сохранением.
Ограничения методики
Результат относится только к зафиксированной комбинации модели, токенизатора, сервера, драйвера, аппаратуры и параметров. Обновление любого компонента требует повторной проверки.
Синтетический текст воспроизводит число токенов, но может не воспроизводить все эффекты реальных префиксов, structured output, speculative decoding, инструментальных вызовов или повторного использования кэша. Если приложение использует эти возможности, добавьте для них отдельные безопасные сценарии.
Тест одного узла не моделирует балансировщик, сеть, отказ реплики и холодный старт после перезапуска. Для многорепличной системы отдельно измеряйте распределение нагрузки и поведение при потере части мощности.
Наконец, максимальная пропускная способность не гарантирует экономическую оптимальность. Сравнивайте конфигурации при одинаковом SLO и одинаковом профиле, учитывая энергопотребление, число устройств и резервирование.
Краткий чек-лист
- Получить распределение входных и выходных токенов.
- Задать TTFT, ITL, E2E latency и допустимую долю ошибок.
- Оценить веса, runtime, KV-кэш и запас памяти.
- Зафиксировать аппаратную и программную конфигурацию.
- Выполнить прогрев, базовую линию, ступени concurrency и открытый тест.
- Сопоставить клиентские времена с памятью, batching и очередью сервера.
- Выбрать устойчивую рабочую точку ниже границы насыщения.
- Подтвердить ее всплеском и длительным прогоном.
Больше практических материалов собрано в разделе «Руководства», а определения метрик и терминов — в глоссарии Agent Lab Journal.