MAATRIX / Блог / Как мониторить нагрузку локальной LLM

Как мониторить нагрузку локальной LLM

Как мониторить нагрузку локальной LLM

MAATRIX

Модель отвечала за секунду, а через месяц реальной нагрузки ответ стал занимать восемь — и никто не может сказать, с какого дня это началось, потому что никто это не мерил. Нагрузка локальной LLM складывается из трёх независимых слоёв — железа, движка инференса и самих запросов, — и без метрик по каждому из них проблема обнаруживается по жалобе пользователя, а не по графику. Разберём, что снимать на каждом слое, какими инструментами и как читать получившиеся цифры.

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

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

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

Из чего складывается «нагрузка» локальной LLM

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

  • GPU — загрузка вычислительных ядер и отдельно занятая видеопамять: это разные величины, и полная VRAM при невысокой загрузке ядер — признак упёртости в память, а не в вычисления.
  • CPU и RAM — критичны и для GPU-инференса (токенизация, HTTP-слой, KV-кэш), и тем более для CPU-инференса, где это единственный ресурс.
  • Диск — веса читаются с NVMe при каждой загрузке модели в память, а логи и трассировка накапливаются постоянно.
  • Сам инференс-движок — сколько моделей загружено, есть ли очередь запросов, растёт ли задержка до первого токена.

Без метрик по всем четырём деградация выглядит как «сервер иногда тупит», а не как измеримая проблема. Хуже, если процесс уже упал: первым об этом узнаёт не мониторинг, а пользователь, у которого клиент вернёт curl: (7) Failed to connect to 127.0.0.1 port 11434: Connection refused. К этому моменту тренд по памяти или очереди запросов был виден заранее — просто на него никто не смотрел.

Почему одного nvidia-smi и top недостаточно

Первый инстинкт — открыть nvidia-smi или htop. Для разовой диагностики достаточно, для мониторинга — нет.

nvidia-smi без параметров — снимок на момент запуска команды. Есть режим наблюдения:

nvidia-smi dmon -s u -d 2

Он обновляется каждые две секунды, но это по-прежнему поток чисел в терминале: закрыли SSH-сессию — история пропала, и разобраться, что происходило ночью, нечем.

top/htop внутри контейнера обманывают ещё конкретнее. Если движок инференса запущен в Docker с лимитом --cpus=4 на 16-ядерном хосте, top внутри контейнера всё равно покажет все 16 ядер и посчитает проценты от них — реальная загрузка может упираться в лимит, а снаружи выглядеть как «всего 25%». Правильный источник — docker stats снаружи контейнера:

docker stats --no-stream ollama

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

Развернуть за пару минут

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

Развернуть Langfuse

Инфраструктурный мониторинг: GPU-метрики поверх Prometheus

База — Prometheus как хранилище метрик и Grafana как панель поверх него; если этой связки ещё нет, установка с нуля разобрана в статье Grafana и Prometheus на VPS: свой node_exporter для CPU, памяти и диска на порту 9100, готовый дашборд Node Exporter Full по ID 1860.

Для LLM-сервера этого мало — не хватает ровно того показателя, который отличает GPU-инференс от любого другого: видеопамяти. Лёгкий экспортёр поверх nvidia-smi добавляет её отдельным источником (нужен установленный nvidia-container-toolkit):

docker run -d --restart unless-stopped --gpus all -p 9835:9835 \
  utkuozdemir/nvidia_gpu_exporter:1.2.1

Проверка: curl -s http://127.0.0.1:9835/metrics | grep nvidia_smi_utilization. Ключевая пара метрик оттуда — nvidia_smi_utilization_gpu_ratio (загрузка ядер) и nvidia_smi_utilization_memory_ratio (занятость VRAM): именно эта пара различает «упёрлись в вычисления» и «упёрлись в память».

Если сервер отдаёт инференс через vLLM, третий источник — его собственный /metrics на порту 8000, без дополнительного экспортёра. Все три цели собираются в один prometheus.yml:

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']
  - job_name: 'gpu'
    static_configs:
      - targets: ['localhost:9835']
  - job_name: 'vllm'
    metrics_path: /metrics
    static_configs:
      - targets: ['localhost:8000']

И правило алерта — чтобы узнать о проблеме до жалобы пользователя, а не после:

groups:
  - name: llm-node
    rules:
      - alert: VRAMNearLimit
        expr: nvidia_smi_utilization_memory_ratio > 0.9
        for: 5m
        labels:
          severity: warning

Для GPU-экспортёра готовый дашборд Grafana ищите в каталоге на grafana.com по имени контейнера — панели под метрики nvidia_smi_* там уже собраны, рисовать с нуля не нужно.

Что отдают сами движки: ollama ps, /api/ps и метрики vLLM

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

Ollama. ollama ps показывает, что реально загружено в память прямо сейчас:

NAME         ID              SIZE      PROCESSOR         UNTIL
gemma2:9b    b1146d5aa3d2    7.4 GB    72%/28% CPU/GPU   9 minutes from now

Столбец PROCESSOR — первое, на что стоит смотреть: ожидали «100% GPU», а видите смешанный оффлоад, как в примере выше, — часть слоёв ушла на CPU из-за нехватки VRAM, и генерация заметно просядет. Для мониторинга полезнее не CLI, а JSON того же списка: curl -s http://127.0.0.1:11434/api/ps можно опрашивать по крону и слать алерт, если ожидаемая модель внезапно пропала из ответа — то есть выгрузилась раньше keep_alive или упала. Переменная OLLAMA_DEBUG=1 перед стартом добавляет в лог время каждого запроса отдельной строкой — без неё понять, что именно затормозило, можно только по косвенным признакам.

vLLM нативно отдаёт Prometheus-метрики на /metrics, без дополнительных экспортёров — сама установка и полный список метрик разобраны в статье про установку vLLM на VPS. Для мониторинга нагрузки важнее всего одна пара: vllm:num_requests_waiting, стабильно больше нуля, — очередь растёт быстрее, чем движок её разбирает, сигнал масштабироваться раньше, чем пользователи почувствуют таймауты; vllm:gpu_cache_usage_perc — заполненность KV-кэша, аналог VRAM-метрики, но именно для контекста запросов, а не весов модели.

Langfuse: то, что видно только на уровне запроса

Всё выше — это ресурсы: сколько занято железа и что творится внутри движка. Ответить на вопрос «а что вообще спрашивали и почему именно этот пользователь получил ответ за 12 секунд, а не за 2» инфраструктурные метрики не могут — агрегаты усредняют, а конкретный медленный запрос теряется в общем графике. Здесь начинается Langfuse: он трассирует каждый вызов отдельно — промпт, ответ, задержку до первого токена и полную, число токенов на входе и выходе — и позволяет резать это по пользователю, сессии и тегу. Перцентиль p95 по задержке в Langfuse почти всегда расскажет о нагрузке больше, чем средняя загрузка GPU: p50 может выглядеть прилично, пока часть пользователей стабильно ловит просадки при параллельных запросах.

В каталоге apps.maatrix.io Langfuse ставится автоматически при заказе сервера — под капотом у него связка из шести контейнеров (веб, воркер, Postgres, ClickHouse, Redis, S3-совместимое хранилище), собирать её руками не нужно, доступы к готовому инстансу появляются в личном кабинете. Подробный разбор архитектуры и установки — в статье «Как установить и настроить Langfuse на VPS», точный расчёт памяти под свой трафик — в материале «Сколько RAM нужно для Langfuse».

Подключение — на стороне вашего приложения, тремя переменными окружения:

export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_BASE_URL=https://langfuse.ваш-домен

Если запросы уже идут через шлюз вроде LiteLLM (его виртуальные ключи разбирали в отдельной статье) — трассировка включается вообще без кода в приложении, одной строкой success_callback: ["langfuse"] в litellm_settings. Честный момент: по умолчанию в трассу улетает весь текст промпта и ответа — если работаете с персональными данными, продумайте маскирование до включения трассировки в проде, а не после.

Пороговые значения: как читать дашборд, а не просто смотреть на него

Голые цифры без интерпретации бесполезны — важно, на что реагировать и когда решение не в железе.

VRAM растёт, загрузка ядер — нет. Если nvidia_smi_utilization_memory_ratio ползёт к единице, а nvidia_smi_utilization_gpu_ratio держится невысоко, — вы упираетесь в память, а не в вычисления. Более мощная карта по чистой производительности здесь не поможет: нужна модель с более агрессивным квантованием или карта с большим объёмом VRAM. Тот же симптом виден и на уровне движка — ollama ps покажет частичный CPU-оффлоад, как в примере из раздела выше.

Очередь не пустеет. vllm:num_requests_waiting, стабильно больше нуля дольше нескольких минут, — не повод крутить параметры движка, а прямой сигнал на горизонтальное масштабирование: добавить реплику или карту. Поднимать --max-num-seqs при заполненной очереди только доведёт до нехватки KV-кэша, а не ускорит ответ.

CPU занят, а токены еле идут. Если движок работает без GPU, все ядра хоста заняты почти под завязку, а скорость генерации не растёт вместе с загрузкой, — узкое место почти всегда пропускная способность памяти, а не число ядер: на каждый токен читаются все веса модели целиком, и это не задача, которую ускоряет ещё один поток. Проверяется той же связкой node_exporter плюс ollama ps, а не переустановкой сервера с большим числом vCPU.

Диск незаметно кончается. Ретеншен метрик в Prometheus (--storage.tsdb.retention.time) и растущий поток трасс в Langfuse без включённой чистки — оба тихо едят NVMe. Алерт на свободное место на диске — такая же обязательная часть связки, как алерт на память.

Какой сервер и локацию брать под мониторинг LLM в MAATRIX

Мониторинг — отдельная нагрузка поверх самой модели, и её стоит закладывать в конфигурацию заранее, а не добавлять по факту, когда Prometheus и Langfuse начнут конкурировать с моделью за одну и ту же память.

Сам стек метрик лёгкий: Prometheus, Grafana, node_exporter и GPU-экспортёр вместе укладываются в 2 vCPU и 2–4 ГБ RAM — ровно то, что требует связка Prometheus и Grafana для любого другого сервера. Langfuse тяжелее на порядок: документация проекта для всех шести контейнеров на одной машине называет ориентир от 4 ядер и 16 ГБ RAM — там уже колоночная база ClickHouse, Postgres, Redis и объектное хранилище разом, точный расчёт под свой поток трасс — в статье про RAM для Langfuse, ссылка выше. Поверх этого — сама модель: у нас на CPU-стенде модель уровня 7B в Q4 держит в памяти около 5 ГБ, точные цифры по моделям — в таблице «Сколько RAM нужно для Ollama», а под GPU-инференс — в материале «Выбор GPU-сервера для инференса LLM».

Честный минимум, если всё на одной машине: 6 vCPU, 24 ГБ RAM, 100 ГБ NVMe. Тесно, но реально: небольшая модель, лёгкий поток трасс, стек метрик рядом. Минус называем прямо — на одном сервере Langfuse, Prometheus и сама модель конкурируют за одну шину памяти, и всплеск нагрузки на любом из трёх задевает остальные два.

Разумный вариант — развести по машинам. Не только ради денег: если сервер с моделью падает под нагрузкой, вместе с ним обычно падает и мониторинг, который должен был объяснить, что случилось, — а это ровно те данные, что нужны после инцидента больше всего. Стек метрик и Langfuse на отдельном сервере переживают падение инференса и продолжают писать историю. Конфигурация под само наблюдение — 4 vCPU, 16 ГБ RAM, 80–100 ГБ NVMe; сервер с моделью считайте отдельно по ссылкам выше.

Локация — Великобритания (Лондон). В трейсах Langfuse по умолчанию лежит полный текст промптов и ответов пользователей — потенциально персональные данные, и держать эту копию рядом с юрисдикцией GDPR разумно для команды на европейскую аудиторию. Если сам инференс уже стоит в другой локации, ничего не мешает вынести именно наблюдение в Лондон отдельным сервером: Prometheus прекрасно опрашивает экспортёры на другом хосте по сети, а Langfuse просто принимает трассы по HTTP независимо от того, где физически считается модель.

Оплата — картой российского банка, по СБП, криптовалютой или токеном MAAT, без необходимости в иностранной карте. Сервер приходит чистым Ubuntu или Debian; Langfuse из каталога устанавливается автоматически и доступен из личного кабинета сразу после заказа, а Prometheus, Grafana и экспортёры — по командам из этой статьи.

Развернуть за пару минут

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

Развернуть Langfuse

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

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

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

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

Обязательно ли ставить Langfuse, если уже есть Prometheus и Grafana?

Нет, это разные слои. Prometheus и Grafana показывают состояние ресурсов — GPU, память, очередь; Langfuse показывает, что происходило с конкретным запросом конкретного пользователя. Для контроля нагрузки хватает первого, для разбора качества ответов и стоимости нужен второй.

Нужен ли GPU-экспортёр, если модель работает только на CPU?

Нет. Ставьте node_exporter и следите за CPU и памятью в Grafana — GPU-метрики без видеокарты просто нечего собирать.

Можно ли собирать метрики с нескольких LLM-серверов в одну Grafana?

Да. Добавьте node_exporter и GPU-экспортёр каждого сервера отдельной целью в prometheus.yml той машины, где стоит Prometheus, а Langfuse и так агрегирует трассы с любого числа источников — достаточно указать всем один и тот же LANGFUSE_BASE_URL.

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

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