KEN’S CAT LOG
▣

Ho quantizzato il mio LLM da 300 milioni di parametri e ne ho confrontato velocità e accuratezza su CPU

Il modello da 320 milioni di parametri per RAG in croato creato nel precedente articolo. Dato che deve girare su CPU, questa volta l'ho quantizzato per vedere quanto potevo ridurne dimensioni e aumentare la velocità, e fino a che punto sarebbe calata l'accuratezza.

Prima la conclusione.

  • Q6_K è il punto giusto. Occupa un quinto dello spazio (254 MB), genera circa 4 volte più velocemente e mantiene la stessa accuratezza del modello originale
  • Q2_K si rompe. È veloce, ma inizia a rispondere «non ci sono risposte» anche a domande per cui una risposta esiste
  • Sorprendentemente, Q3_K_M è stato più accurato di Q4_K_M

Cosa ho fatto

Ho convertito il modello creato in PyTorch in un formato eseguibile con llama.cpp (GGUF), molto usato per l'inferenza su CPU, e l'ho salvato in otto diversi livelli di precisione.

  1. Ho esportato il modello nello stesso formato di Qwen3 e l'ho convertito in GGUF
  2. Poiché il tokenizer è fatto da me, llama.cpp rifiutava la conversione perché lo considerava «sconosciuto». Confrontando le regole di segmentazione delle parole, erano identiche a Qwen2 carattere per carattere, quindi l'ho registrato come Qwen2 e ho completato la conversione
  3. Ho verificato che la conversione fosse corretta. La tokenizzazione di tutti i 160 testi corrispondeva alla versione PyTorch. Su 20 domande, generando ogni volta la parola con la probabilità più alta, anche l'output coincideva perfettamente in tutti e 20 i casi
  4. Ho creato versioni da F16 a Q2_K con llama-quantize

Quindi l'F32 nella tabella contiene gli stessi dati della versione PyTorch. È il riferimento per il confronto.

Come ho misurato

  • La CPU è un i7-13700K. Non ho usato la GPU
  • Le impostazioni di llama.cpp erano 8 thread per la generazione e 16 thread per la lettura del testo (dopo le prove, questa era la combinazione più veloce)
  • Ho fatto rispondere ogni modello 400 volte: 100 domande × 4 tipi di contesto
    • Paragrafo: un paragrafo di circa 100 token che contiene la risposta
    • Testo lungo: un testo di circa 3.000 token che contiene la risposta, con anche articoli irrilevanti mescolati dentro
    • Nessuna risposta (paragrafo/testo lungo): testi che non contengono la risposta. Rifiutare la risposta conta come corretto
  • L'articolo precedente misurava 300 domande, quindi i numeri differiscono leggermente

Risultati: accuratezza

Formato Dimensione Paragrafo corretto Testo lungo corretto Testo lungo rifiutato Allucinazioni (nessuna risposta, paragrafo/testo lungo) Accuratezza dopo verifica
F32 (riferimento) 1.233 MB 77% 53% 30% 1% / 9% 80,9%
F16 617 MB 77% 53% 30% 1% / 9% 80,9%
Q8_0 329 MB 76% 53% 30% 1% / 8% 81,9%
Q6_K 254 MB 79% 54% 30% 2% / 9% 81,3%
Q5_K_M 223 MB 72% 45% 41% 1% / 6% 84,4%
Q4_K_M 193 MB 68% 37% 46% 1% / 7% 75,9%
Q3_K_M 160 MB 72% 48% 38% 1% / 8% 80,4%
Q2_K 129 MB 39% 11% 78% 4% / 7% 72,2%

L'«accuratezza dopo verifica» è la percentuale di risposte corrette dopo aver controllato via programma che la citazione del modello fosse davvero presente nel testo e, in caso contrario, averla sostituita con «nessuna risposta».

  • F16, Q8_0 e Q6_K sono quasi identici al riferimento. La differenza è di appena una o due domande
  • Da Q5_K_M in giù, aumentano più i rifiuti che gli errori. Sembra emergere 그대로 il comportamento insegnato nell'articolo precedente: quando non è sicuro, rifiuta. Le allucinazioni non sono aumentate
  • Q4_K_M va peggio di Q3_K_M. 37% contro 48% sui testi lunghi. Con 100 domande può esserci una certa variabilità, ma 11 punti sono tanti. Non ne ho ancora investigato il motivo
  • Q2_K rifiuta il 78% dei testi lunghi. Ormai è un altro modello

Risultati: velocità

Formato Velocità di generazione Tempo medio per esecuzione
PyTorch fp32 (riferimento) Paragrafo 14,6 · testo lungo 6,7 tok/s —
F32 18,7 tok/s 2,76 secondi
F16 35,9 tok/s 2,08 secondi
Q8_0 59,0 tok/s 1,93 secondi
Q6_K 73,1 tok/s 1,33 secondi
Q5_K_M 81,2 tok/s 1,26 secondi
Q4_K_M 89,1 tok/s 1,10 secondi
Q3_K_M 103,4 tok/s 1,12 secondi
Q2_K 119,9 tok/s 1,07 secondi

I numeri di llama.cpp sono la media di 400 esecuzioni, aggregando paragrafi, testi lunghi e casi senza risposta. I numeri di PyTorch sono quelli misurati nell'articolo precedente.

  • La velocità di generazione è quasi inversamente proporzionale alle dimensioni. Su CPU dipende più dalla velocità di lettura dei pesi dalla memoria che dal calcolo, quindi più piccolo è il modello, più è veloce
  • Anche a parità di F32, llama.cpp è più veloce di PyTorch
  • Tuttavia, sotto Q6_K il tempo per esecuzione diminuisce poco. A questa lunghezza, diventa più rilevante il tempo iniziale per leggere il testo

Video: ho fatto risolvere la stessa domanda su un testo lungo usando la CPU

Ho fornito un testo di circa 3.200 token e chiesto: «Quando è stato pubblicato Need for Speed Heat?». La risposta corretta è l'8 novembre 2019.

Poiché stavo registrando e allo stesso tempo disegnando lo schermo nel browser, le velocità nel video sono inferiori a quelle della tabella sopra. Consideratelo un confronto affiancato.

Formato Velocità di generazione nel video Tempo fino al primo carattere Totale Risultato
PyTorch fp32 5,5 tok/s 4,8 secondi 18,5 secondi Corretto
F32 14,2 tok/s 2,8 secondi 8,1 secondi Corretto
F16 25,9 tok/s 3,5 secondi 6,4 secondi Corretto
Q8_0 38,2 tok/s 4,0 secondi 5,9 secondi Corretto
Q6_K 41,6 tok/s 2,6 secondi 4,4 secondi Corretto
Q5_K_M 46,3 tok/s 2,6 secondi 4,3 secondi Corretto
Q4_K_M 47,2 tok/s 2,3 secondi 4,7 secondi Corretto (risposta più lunga)
Q3_K_M 56,7 tok/s 2,5 secondi 4,5 secondi Corretto (risposta più lunga)
Q2_K 61,3 tok/s 2,7 secondi 2,8 secondi Ha rifiutato

Da F32 a Q5_K_M, sia la risposta sia la citazione sono identiche alla versione PyTorch carattere per carattere. Con Q4_K_M e Q3_K_M la risposta è diventata più lunga, ma la citazione era corretta. Q2_K ha risposto «nel testo non c'è la risposta» anche se una risposta era presente.

PyTorch fp32 (il modello originale)

PyTorch fp32 in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp F32 (1.233 MB)

llama.cpp F32 in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp F16 (617 MB)

llama.cpp F16 in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp Q8_0 (329 MB)

llama.cpp Q8_0 in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp Q6_K (254 MB)

llama.cpp Q6_K in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp Q5_K_M (223 MB)

llama.cpp Q5_K_M in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp Q4_K_M (193 MB)

llama.cpp Q4_K_M in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp Q3_K_M (160 MB)

llama.cpp Q3_K_M in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

llama.cpp Q2_K (129 MB)

llama.cpp Q2_K in esecuzione su CPU mentre risponde a una domanda in un contesto lungo再生

Considerazioni

  • Se volete eseguire questo modello su CPU, scegliete Q6_K. Mantiene l'accuratezza dell'originale, occupa un quinto dello spazio e genera circa 5 volte più velocemente di PyTorch. Non c'è motivo di esitare
  • Per maggiore sicurezza, Q8_0. È quasi identico all'originale e pesa 329 MB. Rimane comunque molto piccolo
  • I modelli piccoli da 300 milioni di parametri sono vulnerabili alla quantizzazione. Si dice che anche Q4 degradi appena i modelli grandi, ma qui il calo è diventato visibile a partire da Q5. Il modello aveva già poco margine, quindi ogni riduzione pesa
  • È positivo che il guasto si manifesti come rifiuto. Per il RAG, rifiutare è più sicuro che aumentare le allucinazioni. L'effetto dell'addestramento con rinforzo usato nell'articolo precedente per insegnargli a rifiutare emerge anche qui
  • Nei modelli piccoli, le differenze di velocità si riducono. Sotto Q6_K, anche se la generazione accelera, resta il tempo di lettura del testo, quindi la differenza percepita è minima. Anche nel video Q6_K–Q3_K_M sono tutti attorno ai 4 secondi
  • Il motivo per cui Q4_K_M perde contro Q3_K_M resta sconosciuto. Vorrei indagare aumentando il numero di domande e osservando quali livelli vengono quantizzati e in che modo