Ho un piccolo problema con gli screenshot dei benchmark di LLM locali: di solito mostrano un numero enorme, l’unità tok/s e abbastanza contesto da fuorviare chiunque sia nella stanza.

Un modello può ingoiare rapidamente un prompt lungo e poi scrivere una risposta con calma esasperante. Può sembrare scattante in una chat individuale e diventare pessimo quando più persone colpiscono il server. Può anche brillare in un ciclo sintetico che non assomiglia affatto al motivo per cui è stata comprata la macchina.

Quindi no: «il mio modello fa 18 token al secondo» non è un risultato di benchmark. È l’inizio di un risultato di benchmark.

L’inferenza ha due lavori

Per i modelli autoregressivi con decoder che molti di noi eseguono in locale, una richiesta ha due fasi materialmente diverse:

  1. Elaborazione del prompt, o prefill: il modello legge l’input e costruisce la cache KV.
  2. Generazione del testo, o decode: il modello produce la risposta un token alla volta e continua a estendere quella cache.

Lo strumento ufficiale llama-bench le considera test distinti: pp per il prompt processing, tg per la generazione e pg per una prova combinata. Non è pedanteria da autore dello strumento. È il punto centrale. I carichi di lavoro non sono intercambiabili.

L’elaborazione del prompt risponde a: quanto serve prima che possa comparire il primo token? La generazione risponde a: quanto velocemente arriva la risposta dopo? Fonderle rende il risultato molto dipendente da lunghezze di prompt e output scelte arbitrariamente.

Un benchmark che comunica un solo throughput è come una recensione d’auto che riporta soltanto la velocità massima. Tecnicamente è una misura. In pratica è un diversivo.

I due numeri che voglio vedere

Per un assistente locale interattivo voglio, separatamente:

  • il throughput di elaborazione del prompt per input realistici;
  • il throughput di generazione per output realistici.

Poi voglio un test end-to-end con la forma esatta della richiesta: token di prompt, token generati, dimensione del contesto, batch, backend, quantizzazione e ripetizioni. Senza questi dati, il numero è una curiosità.

llama-bench rende esplicita la separazione. Un test solo prompt usa -p, uno solo generazione usa -n, uno combinato usa -pg. Lo strumento ripete i test e riporta media e deviazione standard. È un punto di partenza molto più sano che cronometrare una completion fortunata mentre la macchina non fa altro.

# Ingestione del prompt per input corti, medi e lunghi
llama-bench -m model.gguf -p 512,2048,8192 -n 0 -r 5

# Velocità di decode per una risposta utile
llama-bench -m model.gguf -p 0 -n 256 -r 5

# Come si comporta una richiesta reale
llama-bench -m model.gguf -pg 2048,256 -r 5

I comandi non sono magici. Il loro valore è costringere la domanda a essere precisa.

Abbinare il benchmark al lavoro

Carichi locali diversi puniscono fallimenti diversi.

Chat e assistenza alla programmazione dipendono dalla pausa prima del primo token visibile e dal ritmo che segue. Una macchina che decodifica bene ma impiega un’eternità a ingerire una conversazione lunga sembra rotta, anche con un bel punteggio di generazione.

RAG, analisi documentale e grandi prompt di sistema gravano sul prompt processing. Se il lavoro utile inizia con migliaia di token recuperati, testare un prompt minuscolo è autoinganno con un cronometro.

L’estrazione in batch può preoccuparsi più del throughput aggregato che della latenza al primo token di un singolo utente. Per questo llama.cpp ha un benchmark batch separato. Distingue il tempo di prompt dalla generazione e permette di testare sequenze parallele. Un ciclo con un solo utente non dice come si comporta un server sotto domanda concorrente.

Gli agenti persistenti hanno bisogno di tutto questo, più ripetibilità. Un agente che legge grossi risultati di strumenti, scrive un’azione breve e ripete non è né chat pura né generazione di massa. Misurate la forma di ciclo che eseguite davvero.

Perciò non esiste una «migliore velocità universale» per un modello locale. Esistono modelli e macchine veloci per una distribuzione di richieste dichiarata. Risposta fastidiosa? Sì. Ma è quella onesta.

Cosa contiene un risultato credibile

Rifiuterei qualsiasi benchmark locale privo di:

  • file modello esatto e quantizzazione;
  • motore d’inferenza e backend;
  • conteggi di token del prompt e della generazione;
  • finestra di contesto e batch;
  • numero di ripetizioni e varianza;
  • tipo di misura: prompt, decode o end-to-end;
  • concorrenza, se si fa un’affermazione su un server.

C’è un’altra trappola. llama-bench documenta che le sue misure non includono tokenizzazione o sampling. Va benissimo: è un benchmark mirato del motore. Ma quel numero non è l’intera esperienza wall-clock di una richiesta applicativa. HTTP, code, formattazione del prompt, tokenizzazione, sampling, streaming e contesa vivono fuori da quel valore ordinato.

Non abusate di una misura precisa solo perché è comodamente incompleta.

Una scheda benchmark di cui mi fido

Modello: <file GGUF esatto e quantizzazione>
Motore/backend: <build e backend esatti>
Contesto / batch: <valori>
Ripetizioni: 5

Elaborazione prompt: prompt da 2.048 token, media ± deviazione standard
Generazione: decode da 256 token, media ± deviazione standard
End-to-end: prompt da 2.048 token + risposta da 256 token, media wall-clock ± deviazione standard
Concorrenza: 1 (oppure carico server dichiarato esplicitamente)

Questa scheda è meno eccitante di una gigantesca grafica tok/s. Bene. È riutilizzabile, comparabile e difficile da distorcere per caso.

Non sto contestando i token al secondo. Contesto l’idea che ne esista una sola specie. Ingestione del prompt e decode token-per-token sono lavori diversi; un benchmark utile assegna un punteggio a entrambi.

Tutto il resto sono due benchmark sotto lo stesso trench.

Fonti