Є у нас сервер, на якому будемо запускати self-hosted LLM. Довго вибирали на чому саме запускати моделі – vLLM, SGLang, або llama.cpp, і врешті-решт зупинились саме на llama.cpp – принаймні на даний момент.
В пості NixOS: знайомство, установка пакетів та конфігурація системи як раз описана установка Node Exporter на NixOS, де у нас будуть запускатись LLM, ще є чорнетка по NVIDIA DCGM Exporter та FluentBit і VictoriaLogs, може допишу.
А сьогодні тема теж по моніторингу – але моніторингу саме llama.cpp.
Сервер ще не в production, реального трафіку нема, тому і графіки не дають якоїсь картини – але для початку просто розберемось з метриками і познайомимось з тим, що ми можемо побачити по роботі llama.cpp.
Що будемо робити:
- розберемось з деякими нюансами того, як llama.cpp віддає метрики
- глянемо які основні метрики вона повертає
- налаштуємо збір метрик до VictoriaMetrics – бо тут теж є нюанс
- і глянемо приклад Grafana dahshboard
Аби llama.cpp почала генерацію метрик – при запуску додаємо опцію --metrics, ендпоінт – стандартний, /metrics.
Але далі є один важливий момент в тому, як саме ці метрики отримувати.
Зміст
llama.cpp: метрики в single-model vs router mode
llama.cpp може працювати в двох режимах:
- single-model: один інстанс llama.cpp обслуговує запити до одної моделі
- router mode: в такому режимі вона має декілька сконфігурованих моделей, які запускає при потребі
Див. New in llama.cpp: Model Management.
При роботі в single-model всі метрики доступні стандартно при запитах до /metrics.
Але при роботі в router mode просто запит до /metrics поверне помилку 400:
# curl 'http://127.0.0.1:31000/metrics'
{"error":{"code":400,"message":"model name is missing from the request","type":"invalid_request_error"}}
Бо в цьому режимі llama.cpp очікує параметр ?model=<MODEL_NAME>:
# curl 'http://127.0.0.1:37259/metrics?model=gemma-4-26b-a4b-it-q8' # HELP llamacpp:prompt_tokens_total Number of prompt tokens processed. # TYPE llamacpp:prompt_tokens_total counter llamacpp:prompt_tokens_total 26 # HELP llamacpp:prompt_seconds_total Prompt process time # TYPE llamacpp:prompt_seconds_total counter llamacpp:prompt_seconds_total 0.093 ...
Є і інший варіант: при роботі в router mode кожна модель запускається окремим інстансом (процесом) llama.cpp і слухає окремий порт:
# ps aux | grep 'llama-server' [...] /nix/store/3vq7fxic7zaznzsip9zwbj4nblxfqpk5-llama-cpp-cuda-0.0.0/bin/llama-server --metrics --no-ui --host 0.0.0.0 --port 31000 --models-dir /srv/llm/models/llama-cpp/local --models-preset /nix/store/4g0chnaiidzbz25izygnxf1lj2rqwx78-matrix-llama-models.ini --models-max 3 [...] /nix/store/3vq7fxic7zaznzsip9zwbj4nblxfqpk5-llama-cpp-cuda-0.0.0/bin/llama-server --host 127.0.0.1 --metrics --port 37259 --no-webui --alias gemma-4-26b-a4b-it-q8 --ctx-size 65536 --device CUDA0 --flash-attn on --fit off --model /srv/llm/models/gguf/gemma-4-26b-a4b-it-q8-gguf/gemma-4-26B-A4B-it-Q8_0.gguf --n-gpu-layers all --parallel 1 --split-mode none
Тут на порту 31000 маємо “master process”, який, власне, займається роутингом запитів до моделей, а другий процес на порту 37259 приймає запити до Gemma-4.
Тому можна просто отримувати метрики через 127.0.0.1:37259/metrics:
# curl -s http://127.0.0.1:37259/metrics # HELP llamacpp:prompt_tokens_total Number of prompt tokens processed. # TYPE llamacpp:prompt_tokens_total counter llamacpp:prompt_tokens_total 83 # HELP llamacpp:prompt_seconds_total Prompt process time # TYPE llamacpp:prompt_seconds_total counter llamacpp:prompt_seconds_total 0.36 ...
Втім, цей варіант не дуже надійний, бо для кожної моделі треба знати порти, які можуть змінюватись – тому краще використовувати саме “мастер-інстанс”.
Тут, звісно, є свої складності і підхід не дуже зручний, і є GitHub Feature Request [Feat Request] Aggregated Prometheus metrics endpoint in router mode на те, щоб мати агрегований ендпоінт – може, колись і змінять цю поведінку.
Бо чисто імхо – це, звісно, доволі дивне рішення: чому не можна було просто зробити ім’я моделі в лейблі і повертати метрику типу llamacpp:prompt_tokens_total{model_name="gemma-4-26B"}?
Але розробникам llama.cpp видніше, якісь причини зробити саме так, як є зараз явно були, тому маємо те, що маємо.
Метрики llama.cpp
Звісно, краще було б спочатку поганяти llama.cpp локально і подивитись на її налаштування взагалі, бо я з локальних рантаймів грався тільки з Ollama, і це було давно (див. AI: знайомство з Ollama для локального запуску LLM), але зараз на це нема часу – тому розберемось з метриками “на льоту”.
Документація по метрикам – GET /metrics: Prometheus compatible metrics exporter, можливо будуть зміни, але на сьогодні з цікавого маємо такі:
llamacpp:prompt_tokens_total: загальна кількість токенів вхідних промптів, оброблених від моменту запуску сервераllamacpp:prompt_seconds_total: сумарний час у секундах, витрачений на обробку вхідних промптів від моменту запуску сервераllamacpp:prompt_tokens_seconds: середня швидкість обробки промптів в токенах на секунду (більше значення – швидше обробка)llamacpp:tokens_predicted_total: скільки токенів згенеровано для відповідейllamacpp:tokens_predicted_seconds_total: скільки часу витрачено на генерацію відповідейllamacpp:predicted_tokens_seconds: аналогічно доprompt_tokens_seconds– середня швидкість генерації відповіді в токенах на секунду (більше токенів в секунду згенеровано – швидше генерація)llamacpp:requests_processing: скільки запитів від клієнтів оброблюється зараз- корисно було б порівнювати із загальною кількістю слотів (задається в
--parallel– максимальна кількість паралельної обробки запитів), але окремої метрики на це (поки що?) нема - можна отримати значення з
GET /propsі поляtotal_slots, абоGET /slots– порахувати кількість елементів, або просто читати значення--parallelз конфігу
- корисно було б порівнювати із загальною кількістю слотів (задається в
llamacpp:requests_deferred: кількість запитів, які зараз очікують на початок обробки (наприклад, через нестачу ресурсів чи зайнятість слотів)llamacpp:n_tokens_max: найбільша кількість токенів в контекстному вікні- тут теж було б цікаво порівнювати з розміром самого контексту (значення з
--ctx-size) – але в метриках теж нема
- тут теж було б цікаво порівнювати з розміром самого контексту (значення з
llamacpp:n_decode_total: кількість запусків обчислень моделі (виклики функціїllama_decode()) для обробки вхідних або згенерованих токенів- або, умовно кажучи – скільки раз llama.cpp “прогнав” дані через модель
- маємо на увазі, що один виклик
llama_decode()може обробляти одразу кілька токенів і запити з кількох слотів – а тому ця метрика не дорівнює кількості токенів або запитів - див. The Core Llama Class — Request Lifecycle
llamacpp:n_busy_slots_per_decode: середня кількість зайнятих слотів на один викликllama_decode()- відображає ефективність continuous batching – об’єднання токенів із кількох паралельних запитів в один пакет для обробки моделлю (див. Continuous Batching: Optimizing LLM Inference Throughput)
- якщо
n_busy_slots_per_decode~1 – запити оброблюються послідовно, якщо більше – то запити об’єднуються в batches, які потім передаються до моделі через викликllama_decode()
llamacpp:spec_decode_num_draft_tokens_total: кількість токенів, запропонованих draft-моделлю при використанні Speculative Decoding- при використанні speculative decoding запит спочатку передається простішій, “допоміжній” моделі, яка швидко генерує кілька наступних токенів, а основна модель потім перевіряє їх одним batch і приймає ті, що підходять
- задається опцією
--spec-type, але у нас (поки?) не використовується
llamacpp:spec_decode_num_accepted_tokens_total: кількість токенів, запропонованих draft-моделлю, і які були прийняті основною LLM- чим вище значення тут – тим ефективніша робота draft model і speculative decoding
llamacpp:spec_decode_num_drafts_total: кількість кроків перевірки припущень draft-моделіllamacpp:spec_decode_num_accepted_tokens_per_pos_total: кількість прийнятих токенів для кожної позиції у припущені, які пропонує draft model
Збір метрик llama.cpp до VictoriaMetrics
Тут основна складність як раз в тому, що нам треба передавати конкретну модель для отримання метрик.
Опція 1: VMAgent та static_configs
Найпростіший варіант – просто описати кілька targets в scrape job, і кожній вказати власний параметр з іменем моделі.
Перевіряємо всі моделі, які зараз запущені:
# curl -sS http://127.0.0.1:31000/v1/models \ | jq -r '.data[] | select(.status.value == "loaded") | .id' gemma-4-26b-a4b-it-q8
Додаємо нову job до VMAgent:
- job_name: matrix-llama-cpp
metrics_path: /metrics
params:
model: ["gemma-4-26b-a4b-it-q8"]
autoload: ["false"]
static_configs:
- targets: ["matrix.neoc.vpn.ops.example.co:31000"]
labels:
model: "gemma-4-26b-a4b-it-q8"
Тут значення в params формує query-параметри, які додаються до URI, тобто весь запит буде виглядати як /metrics?autoload=false&model=gemma-4-26b-a4b-it-q8, див. How to modify scrape URLs in targets.
З autoload вказуємо llama.cpp не завантажувати модель, якщо вона зараз не запущена.
А в labels додаємо статичну лейблу з іменем моделі, аби потім простіше було робити графіки і алерти (власне саме те, чого я очікував від самої llama.cpp).
Деплоїмо, перевіряємо targets:
І перевіряємо метрики – запит sum({job="matrix-llama-cpp"}) by (__name__):
Опція 2: VMAgent та http_sd_configs
В PR є варіант автоматизації через додавання локального до llama.cpp сервісу, який опитує ендпоінт /models і повертає JSON з моделями (див. приклад в коментах до PR).
Тоді для VMAgent можна було б створити job із http_sd_configs типу такого:
scrape_configs:
- job_name: llama-cpp
metrics_path: /metrics
http_sd_configs:
- url: http://matrix.neoc.vpn.ops.example.co:31000/targets.json
relabel_configs:
- source_labels: [llama_model_id]
target_label: __param_model
- target_label: __param_autoload
replacement: "false"
- source_labels: [llama_model_id]
target_label: model
Але, по-перше, це має сенс коли моделей багато або вони часто змінюються, по-друге – сподіваюсь, що сам PR таки приймуть, і колись будемо мати простіший спосіб отримання метрик, а тому зараз робити якусь автоматизацію сенсу нема. Тим більш, що конкретно в моєму випадку буде 2-3 моделі максимум.
Grafana dashboard
Є вже готові дашборди, наприклад llama.cpp Monitoring, і для початку можна просто взяти якусь таку і трохи змінити під себе.
Власне, я так і зробив, але додав можливість вибору моделі:
Відповідно, треба було оновити queries і додати sum by (model) (лейбла, яка задається в нашій scrape_job) та фільтр по змінній $model_name.
Наприклад, запит для графіку “Tokens per Decode”:
sum by (model) (
rate(llamacpp:tokens_predicted_total{model=~"$model_name"}[$__rate_interval])
)
/
on(model)
sum by (model) (
rate(llamacpp:n_decode_total{model=~"$model_name"}[$__rate_interval])
)
І вся дашборда зараз виглядає так – тут, знов-таки, реального трафіку ще нема, але загальна ідея поки така:
Готово.
І по ходу гуглінга знайшов цікавий пост llama.cpp guide – Running LLMs locally, on any hardware, from scratch – залишу тут лінк на майбутнє.
![]()



