Il mio GX10 lavora sodo: fa girare i modelli in locale 24 ore su 24 e nessuno dei due post precedenti raccontava quanto. Era ora di smettere di rispondere “credo vada bene” e aprire una dashboard. Ecco come l’ho costruita — e i numeri della prima settimana vera di lavoro.

L’architettura in dieci righe

Tutto gira in Docker con compose, quattro container:

node-exporter  :9100   → CPU, RAM, disco, rete (host)
prometheus     :9090   → raccolta, retention, regole
grafana        :3001   → dashboard
backend (flask):5000   → metriche applicative mie

Più una cosa fuori da Docker che è la parte più interessante: le metriche native di llama.cpp. Il server di inferenza è un servizio systemd sull’host (la storia è nel post sull’installazione), avviato con --metrics. Prometheus dentro il container lo vede attraverso la gateway della rete bridge:

# llama.cpp native metrics: the server runs on the host (systemd, --metrics).
# 172.18.0.1 is the gateway of the compose bridge — the host, as seen from
# inside the Prometheus container.
- job_name: 'llama_server'
  static_configs:
    - targets: ['172.18.0.1:30000']
      labels:
        service: 'llm-inference'
        model: 'qwen3.8-flash-next'

Oggi i target attivi sono tre (sistema, backend, llama.cpp) per un totale di 187 metriche. Zero agenti da installare a mano oltre a node-exporter.

Le regole che rendono i grafici leggibili

La differenza tra vent’anni fa e oggi non è Prometheus: sono le recording rules. llama.cpp espone counter grezzi (tokens_predicted_total, prompt_tokens_cached_total…); io mi precarico le derivate che servono ai grafici, così la dashboard non deve ricalcolare tutto a ogni refresh:

- record: llm:decode_tokens_per_second
  expr: sum by (model) (rate(llamacpp:tokens_predicted_total[5m]))
        / clamp_min(sum by (model) (rate(llamacpp:tokens_predicted_seconds_total[5m])), 0.001)

- record: llm:prompt_cache_hit_ratio
  expr: sum by (model) (rate(llamacpp:prompt_tokens_cached_total[15m]))
        / clamp_min(sum by (model) (rate(llamacpp:prompt_tokens_cached_total[15m])
            + rate(llamacpp:prompt_tokens_total[15m])), 0.001)

Il clamp_min è il trucco anti-divisione-per-zero: quando il modello è fermo il denominatore è 0 e il grafico si riempie di buchi; con il clamp la serie va a zero in modo pulito.

Due sole alert rules per ora: p95 delle richieste sopra i 5 secondi da 5 minuti (warning), e modello che non processa un token da 30 minuti (info — serve a capire se il servizio systemd è vivo ma non serve traffico, o è appeso).

La dashboard predefinita è provisioned, non cliccata a mano: il JSON sta nel repo (monitoring/grafana/dashboards/) e GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH la punta come home. Se Grafana muore, la riavvio e la home è già lì: stessa filosofia del cms che si fa le copertine da solo — la configurazione è codice, il click è un incidente.

Ecco com’è fatta, la dashboard “LLM Models” oggi: quattro stat card in alto (token generati, token di prompt, decode istantaneo, hit ratio della cache) e poi le serie storiche su tutta la settimana:

![La dashboard LLM Models di Grafana sul GX10]({{ “images/grafana-dashboard-completa.webp” | relURL }})

La settimana del GX10, in numeri

Aggiornato al 22 settembre: Prometheus raccoglie dal 16, quindi adesso la finestra a 7 giorni è piena, di storia vera — non più quattro giorni e mezzo estrapolati.

Inferenza (Qwen3.8 Flash Next, quantizzato, con speculative decoding MTP):

Token generati1,8 milioni
Token di prompt processati~150 milioni
…di cui già in cache142 milioni (95%)
Velocità di decode26 t/s mediana quando lavora, p95 ~40, picchi ~51 t/s
Prefillmediana ~20.000 t/s, picchi ~52.000 t/s
Accettazione MTP33–72%, mediana ~51%

![Token per ora su 7 giorni: completion e prompt]({{ “images/grafana-tokens-per-hour.webp” | relURL }})

La riga interessante è quella della cache: su ~150 milioni di token di prompt, 142 sono stati serviti dalla prompt cache. Tradotto: il lavoro vero di prefill è stato solo il 5%. È il motivo per cui le conversazioni lunghe con agente restano fluide — il contesto si ricostruisce quasi gratis. Il decode a 26 t/s di mediana con punte di 51 è la firma del draft MTP: quando l’accettazione è buona vola, quando cala torni alla velocità nuda del modello.

![Velocità di decode su 7 giorni]({{ “images/grafana-decode-speed.webp” | relURL }})

![Accettazione dei draft MTP]({{ “images/grafana-mtp-acceptance.webp” | relURL }})

![Hit ratio della prompt cache]({{ “images/grafana-cache-hit-ratio.webp” | relURL }})

Hardware (node-exporter):

CPU (20 core) media5,4%
CPU p95 / picco (media 10 min)16% / 66%
RAM unificata media80% di 128 GB
RAM picco94%
Disco letto / scritto3,4 TB / 360 GB

Due cose mi hanno sorpreso. La prima: la CPU è quasi sempre a riposo (media 5,4%, p95 al 16%) — i picchi sono i momenti in cui aggiorno i modelli o compilo, non l’inferenza: quella la fa il GB10 e i core ARM stanno a guardare. La seconda: 3,4 TB letti in sette giorni, quasi 500 GB al giorno. Sembra un assurdo finché non pensi che ogni caricamento di modello mmap-pa 15-20 GB dal NVMe, e tra cambi di modello, riavvii del servizio e cache che si raffredda l’NVMe macina in silenzio. I 360 GB scritti sono il contrario rassicurante: quasi non scrive mai, quindi il disco non se lo mangia.

La RAM al 94% di picco non è un allarme — su 128 GB unificati è il modello che sta in memoria di lavoro insieme al KV cache, ed è esattamente lì che voglio che stia.

Cosa dice il grafico giorno per giorno

I token generati per giorno di calendario, sulla settimana completa:

GiornoToken generati
Mer 1652.000
Gio 17293.000
Ven 18409.000
Sab 19313.000
Dom 20378.000
Lun 21247.000
Mar 22 (a metà)141.000

Venerdì è il giorno del CMS e delle copertine AI, e si vede: è esattamente il giorno in cui ho scritto il post sulle copertine facendolo scrivere più o meno al modello stesso. E il grafico non mente nemmeno sulla domenica: 378.000 token, più del sabato — gli agenti non conoscono il weekend, a differenza di chi li guarda.

Prossimi passi

  • Blackbox exporter per monitorare il sito pubblicato (http://gx10-8532/) dallo sguardo di un client, non dal container.
  • Un alert serio sul KV cache pieno (oggi osservo solo i token, non la pressione sulla cache).
  • Magari pushover/telegram per le notifche invece del silenzio attuale — l’alert “modello idle da 30 minuti” è nato e non ha ancora un canale.

![Totali d’uso per modello nella finestra selezionata]({{ “images/grafana-usage-totals.webp” | relURL }})

Se avete il dubbio classico — “ma con 128 GB unificati ci faccio inferenza seria o ci gioco?” — i dati dicono: 26 token/s di mediana mentre il resto della macchina sta a guardare, 1,8 milioni di token generati in una settimana, e la serie storica lo racconta meglio di qualsiasi spec.