Monitoring inferencije sa Prometheusom i Grafanom

Benchmark iz prethodnog teksta ti kaže kako je bilo u trenutku merenja. Nadzor ti kaže kako je sada i kako je bilo prošle srede u tri ujutru — a to je jedini način da odgovoriš na pitanje da li je sporije nego pre.

Ovaj tekst postavlja tri komponente: prikupljanje metrika sa kartice i iz servisa, njihovo čuvanje i prikaz.

Šta se prati

Sloj Metrika Zašto
Kartica Memorija, potrošnja, temperatura Kapacitet i usporavanje
Servis Zahtevi u obradi i u redu, KV keš Da li stižeš da obradiš
Proxy Trajanje zahteva, kodovi odgovora Šta korisnik zaista doživljava

Sva tri sloja su potrebna. Kartica na devedeset posto ne znači da je servis zagušen; servis bez grešaka ne znači da korisnici ne čekaju.

Metrike sa kartice

Zvanični alat je DCGM exporter. Radi kroz isti nvidia-container-toolkit iz teksta o Dockeru:

$ docker run -d \
    --name dcgm-exporter \
    --gpus all \
    --cap-add SYS_ADMIN \
    -p 127.0.0.1:9400:9400 \
    --restart unless-stopped \
    nvcr.io/nvidia/k8s/dcgm-exporter:latest
$ curl -s localhost:9400/metrics | grep -E '^DCGM_FI_DEV_(GPU_UTIL|FB_USED|POWER)'
DCGM_FI_DEV_GPU_UTIL{gpu="0",modelName="NVIDIA GeForce RTX 3090"} 94
DCGM_FI_DEV_FB_USED{gpu="0",modelName="NVIDIA GeForce RTX 3090"} 18432
DCGM_FI_DEV_POWER_USAGE{gpu="0",modelName="NVIDIA GeForce RTX 3090"} 287.431

Najkorisnija polja:

Metrika Značenje
DCGM_FI_DEV_FB_USED Zauzeta memorija u MiB
DCGM_FI_DEV_FB_FREE Slobodna memorija
DCGM_FI_DEV_POWER_USAGE Potrošnja — pravi pokazatelj rada
DCGM_FI_DEV_GPU_TEMP Temperatura
DCGM_FI_DEV_GPU_UTIL Iskorišćenost — zavarava

Kao što stoji u tekstu o nvidia-smi, iskorišćenost meri samo da li je kartica imala posla. Potrošnja je pouzdaniji pokazatelj i na grafikonu se lepše čita.

Bez DCGM-a

Na potrošačkim karticama DCGM ume da zakaže. Jednostavan zamenski exporter:

#!/usr/bin/env python3
"""GPU metrike za Prometheus, preko nvidia-smi."""
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler

POLJA = "index,memory.used,memory.free,utilization.gpu,power.draw,temperature.gpu"

class Rukovalac(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/metrics":
            self.send_response(404); self.end_headers(); return

        ispis = subprocess.run(
            ["nvidia-smi", f"--query-gpu={POLJA}",
             "--format=csv,noheader,nounits"],
            capture_output=True, text=True, timeout=10,
        ).stdout

        redovi = []
        for red in ispis.strip().split("\n"):
            i, mem_u, mem_s, util, snaga, temp = [p.strip() for p in red.split(",")]
            redovi += [
                f'gpu_memory_used_mib{{gpu="{i}"}} {mem_u}',
                f'gpu_memory_free_mib{{gpu="{i}"}} {mem_s}',
                f'gpu_utilization_percent{{gpu="{i}"}} {util}',
                f'gpu_power_watts{{gpu="{i}"}} {snaga}',
                f'gpu_temperature_celsius{{gpu="{i}"}} {temp}',
            ]

        telo = ("\n".join(redovi) + "\n").encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(telo)

    def log_message(self, *a):
        pass

HTTPServer(("127.0.0.1", 9401), Rukovalac).serve_forever()

Stavi ga u systemd jedinicu i imaš isto što i DCGM, u trideset redova.

Metrike iz servisa

vLLM

Uključene same od sebe:

$ curl -s localhost:8000/metrics | grep -E '^vllm:(num_requests|gpu_cache)'
vllm:num_requests_running 7.0
vllm:num_requests_waiting 2.0
vllm:gpu_cache_usage_perc 0.63

Ova tri broja su srce nadzora. Zahtevi u redu koji ne opada znače premašen kapacitet; keš blizu jedinice znači da se zahtevi odbacuju i vraćaju u obradu.

Uz njih idu i histogrami trajanja:

vllm:time_to_first_token_seconds_bucket
vllm:time_per_output_token_seconds_bucket
vllm:e2e_request_latency_seconds_bucket

llama.cpp

Traži izričito uključivanje:

$ llama-server -m model.gguf --metrics ...
$ curl -s localhost:8080/metrics | grep llamacpp
llamacpp:tokens_predicted_seconds_total 1247.3
llamacpp:requests_processing 2
llamacpp:requests_deferred 0

Ollama

Nema ih. Ako ti nadzor treba, ostaje ti da meriš spolja — kroz proxy, ili malom skriptom koja povremeno šalje upit i beleži trajanje:

#!/bin/bash
# Provera odziva, za textfile collector
POCETAK=$(date +%s.%N)
KOD=$(curl -s -o /dev/null -w '%{http_code}' --max-time 60 \
    http://localhost:11434/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{"model":"qwen3:8b","messages":[{"role":"user","content":"test"}],"max_tokens":5}')
TRAJANJE=$(echo "$(date +%s.%N) - $POCETAK" | bc)

cat > /var/lib/node_exporter/textfile/ollama.prom.$$ <<EOF
ollama_up $([ "$KOD" = "200" ] && echo 1 || echo 0)
ollama_probe_seconds $TRAJANJE
EOF
mv /var/lib/node_exporter/textfile/ollama.prom.$$ /var/lib/node_exporter/textfile/ollama.prom

Upis u privremeni fajl pa premeštanje sprečava da Prometheus pročita polupisan fajl. Zakazivanje kroz cron na svakih minut.

Prometheus i Grafana

$ nano compose.yaml
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./pravila.yml:/etc/prometheus/pravila.yml:ro
      - prom-data:/prometheus
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.retention.time=90d
      - --web.listen-address=127.0.0.1:9090

  grafana:
    image: grafana/grafana
    container_name: grafana
    restart: unless-stopped
    ports:
      - "127.0.0.1:3001:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=promeni-me
      - GF_USERS_ALLOW_SIGN_UP=false

volumes:
  prom-data:
  grafana-data:
$ nano prometheus.yml
global:
  scrape_interval: 15s

rule_files:
  - /etc/prometheus/pravila.yml

scrape_configs:
  - job_name: gpu
    static_configs:
      - targets: ['127.0.0.1:9400']

  - job_name: vllm
    static_configs:
      - targets: ['127.0.0.1:8000']

  - job_name: node
    static_configs:
      - targets: ['127.0.0.1:9100']
$ docker compose up -d
$ curl -s localhost:9090/api/v1/targets | jq -r '.data.activeTargets[] | "\(.labels.job) \(.health)"'

Port 3001 za Grafanu je namerno — 3000 verovatno već drži Open WebUI. Oba idu iza proxy-ja, sa istim pravilima.

Grafanu nikad ne izlaži bez izmene podrazumevane lozinke. Ona ima pristup svim tvojim metrikama, uključujući i one koje otkrivaju kako je server postavljen.

Upiti koje ćeš koristiti

Zauzeće memorije u procentima:

DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE) * 100

Prosečna brzina generisanja u poslednjih pet minuta:

rate(vllm:generation_tokens_total[5m])

Medijana vremena do prvog tokena:

histogram_quantile(0.5, rate(vllm:time_to_first_token_seconds_bucket[5m]))

Devedeset peti percentil trajanja zahteva:

histogram_quantile(0.95, rate(vllm:e2e_request_latency_seconds_bucket[5m]))

Percentil je ovde važniji od proseka. Prosek od dve sekunde uz p95 od trideset znači da svaki dvadeseti korisnik čeka pola minuta — što je ono na šta se ljudi zapravo žale.

Udeo grešaka iz proxy loga, ako koristiš nginx exporter:

sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
  / sum(rate(nginx_http_requests_total[5m]))

Grafikoni koji vrede

Manje je bolje. Šest panela dovoljno pokriva:

  1. Zauzeće VRAM-a — s pragom na devedeset posto
  2. Potrošnja i temperatura — na istoj osi, otkrivaju usporavanje
  3. Zahtevi u obradi i u redu — dve linije, jedna ispod druge
  4. Zauzeće KV keša — ranije upozorenje od svega ostalog
  5. TTFT, medijana i p95 — ono što korisnik oseća
  6. Tokena u sekundi — ukupna propusnost

Grafana ima gotove table koje se uvoze po broju; potraži one za DCGM i za vLLM pod Dashboards → Import. Brže je nego crtati od nule, a posle ih doteraš.

Obaveštenja

$ nano pravila.yml
groups:
  - name: ai-servis
    rules:
      - alert: ServisNeRadi
        expr: up{job="vllm"} == 0
        for: 2m
        annotations:
          summary: "AI servis ne odgovara"

      - alert: MemorijaPriKraju
        expr: DCGM_FI_DEV_FB_FREE < 1024
        for: 5m
        annotations:
          summary: "Manje od 1 GB slobodnog VRAM-a"

      - alert: RedRaste
        expr: vllm:num_requests_waiting > 5
        for: 10m
        annotations:
          summary: "Zahtevi čekaju duže od deset minuta"

      - alert: KesPun
        expr: vllm:gpu_cache_usage_perc > 0.95
        for: 5m
        annotations:
          summary: "KV keš pun, zahtevi se odbacuju"

      - alert: TermalnoUsporavanje
        expr: DCGM_FI_DEV_GPU_TEMP > 83
        for: 10m
        annotations:
          summary: "Kartica na granici usporavanja"

      - alert: OdzivLos
        expr: histogram_quantile(0.95,
                rate(vllm:e2e_request_latency_seconds_bucket[5m])) > 60
        for: 10m
        annotations:
          summary: "p95 trajanja zahteva preko minuta"
$ docker compose restart prometheus
$ curl -s localhost:9090/api/v1/rules | jq -r '.data.groups[].rules[].name'

Vrednost for je ono što deli koristan nadzor od zvona koje niko ne sluša. Kartica na sekund puna nije problem; kartica puna pet minuta jeste.

Iskorišćenost kartice namerno nije među pravilima. Devedeset posto tokom inferencije je normalno stanje, ne uzbuna.

Šta ovo otkriva

Nekoliko obrazaca koje ćeš prepoznati posle nedelju dana prikupljanja:

Postepen rast TTFT kroz nedelje — promptovi rastu, jer je neko proširio sistemski prompt ili je RAG počeo da vraća više delova.

Skokovi zauzeća memorije u isto vreme svakog dana — cron zadatak koji radi u paketu.

Potrošnja niska uz visoku iskorišćenost — kartica čeka memoriju, što je za inferenciju normalno i ne popravlja se.

Zauzeće koje raste i ne vraća se — model ostaje učitan zbog keep-alive postavke, ili se nakupljaju zaostali procesi.

Praktični scenariji

Scenario 1: DCGM ne radi na potrošačkoj kartici

Uobičajeno. Koristi zamenski exporter preko nvidia-smi iz ovog teksta.

Scenario 2: Prometheus ne vidi metu

$ curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health, lastError}'

Ako je exporter na domaćinu, a Prometheus u kontejneru bez network_mode: host, 127.0.0.1 pokazuje na sam kontejner.

Scenario 3: Ollama nema metrike

Nema ih i neće ih imati. Meri spolja, kroz proxy ili skriptom.

Scenario 4: obaveštenja stižu bez prestanka

Prag je prenizak ili for prekratak. Podigni oba, pa posmatraj nedelju dana.

Scenario 5: disk se puni od metrika

$ docker exec prometheus du -sh /prometheus

Skrati retention.time ili podigni scrape_interval na trideset sekundi.

Scenario 6: grafikoni prazni posle restarta

Volumen nije podešen, pa su podaci nestali sa kontejnerom. Ista greška kao kod modela u tekstu o Dockeru.

Kratka referenca

  • dcgm-exporter — GPU metrike, port 9400
  • DCGM_FI_DEV_FB_USED, FB_FREE — memorija
  • DCGM_FI_DEV_POWER_USAGE — pouzdaniji od iskorišćenosti
  • vllm:num_requests_waiting — premašen kapacitet
  • vllm:gpu_cache_usage_perc — najranije upozorenje
  • llama-server --metrics — traži izričito uključivanje
  • Ollama nema metrike — meri spolja
  • histogram_quantile(0.95, ...) — ono što korisnik oseća
  • rate(...[5m]) — brzina promene brojača
  • for: 5m u pravilima — bez toga nadzor postane buka
  • --storage.tsdb.retention.time — koliko se čuva
  • network_mode: host — da Prometheus vidi exportere na domaćinu

Vežba

  1. Pokreni exporter za karticu i potvrdi da vraća metrike na /metrics.
  2. Podigni Prometheus i proveri da su sve mete u stanju up.
  3. Napravi grafikon zauzeća VRAM-a i pusti test opterećenja iz prethodnog teksta.
  4. Izračunaj p95 trajanja zahteva i uporedi ga sa prosekom.
  5. Postavi pravilo za nedostupan servis, zaustavi ga i sačekaj da se javi.
  6. Prikupljaj podatke nedelju dana, pa pronađi obrazac koji se ponavlja.

Sledeći tekst u serijalu: Deploy modela na Kubernetes

Povezano:

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd