KEN’S CAT LOG
▣

Quantifiquei meu LLM caseiro de 300 milhões de parâmetros e comparei velocidade e precisão na CPU

O modelo de 320 milhões de parâmetros para RAG em croata que criei no artigo anterior. Como ele foi pensado para rodar na CPU, desta vez fiz a quantização e testei até que ponto ele podia ficar menor e mais rápido, e quanto a precisão cairia.

Primeiro, a conclusão.

  • Q6_K é o ponto ideal. Ocupa 1/5 do tamanho (254 MB), gera cerca de 4 vezes mais rápido e tem a mesma precisão do modelo original
  • Q2_K quebra. É rápido, mas começa a dizer “não há resposta” até em perguntas que têm resposta
  • Surpreendentemente, Q3_K_M foi mais preciso que Q4_K_M

O que fiz

Convertti o modelo feito em PyTorch para um formato que pode ser executado pelo llama.cpp, muito usado para inferência em CPU (GGUF), e o salvei em oito níveis de precisão.

  1. Exportei o modelo no mesmo formato do Qwen3 e o converti para GGUF
  2. Como o tokenizador é próprio, o llama.cpp recusou a conversão por ser um “tokenizador desconhecido”. Comparei as regras de separação de palavras e elas eram idênticas, caractere por caractere, às do Qwen2; então o registrei como Qwen2 para prosseguir
  3. Verifiquei se a conversão estava correta. A tokenização de 160 textos coincidiu integralmente com a versão em PyTorch. Em 20 perguntas, ao gerar sempre escolhendo a palavra com maior probabilidade, as saídas também coincidiram perfeitamente nas 20
  4. Usei llama-quantize para criar as versões de F16 a Q2_K

Portanto, o F32 da tabela contém exatamente o mesmo conteúdo da versão em PyTorch. Ele é a referência da comparação.

Como medi

  • A CPU é um i7-13700K. Não usei GPU
  • As configurações do llama.cpp foram 8 threads para geração e 16 threads para leitura do texto (foi a combinação mais rápida que encontrei nos testes)
  • Para 100 perguntas de avaliação × 4 tipos de contexto, cada modelo respondeu 400 vezes
    • Parágrafo: um parágrafo de cerca de 100 tokens que contém a resposta
    • Texto longo: um texto de cerca de 3.000 tokens que contém a resposta (também há artigos irrelevantes misturados)
    • Sem resposta (parágrafo/texto longo): texto que não contém a resposta. Recusar conta como acerto
  • Como o artigo anterior usou 300 perguntas, os números diferem um pouco

Resultados: precisão

Formato Tamanho Acertos em parágrafos Acertos em textos longos Recusou em textos longos Alucinação (sem resposta: parágrafo/texto longo) Precisão após verificação
F32 (referência) 1.233MB 77% 53% 30% 1% / 9% 80,9%
F16 617MB 77% 53% 30% 1% / 9% 80,9%
Q8_0 329MB 76% 53% 30% 1% / 8% 81,9%
Q6_K 254MB 79% 54% 30% 2% / 9% 81,3%
Q5_K_M 223MB 72% 45% 41% 1% / 6% 84,4%
Q4_K_M 193MB 68% 37% 46% 1% / 7% 75,9%
Q3_K_M 160MB 72% 48% 38% 1% / 8% 80,4%
Q2_K 129MB 39% 11% 78% 4% / 7% 72,2%

“Precisão após verificação” é a proporção correta entre as respostas dadas após confirmar por programa se a citação do modelo realmente está no texto e, caso não esteja, substituí-la por “sem resposta”.

  • F16, Q8_0 e Q6_K são praticamente iguais à referência. A diferença é de apenas uma ou duas perguntas
  • A partir de Q5_K_M, aumentam as recusas em vez dos erros. Parece que o comportamento ensinado no artigo anterior — recusar quando não tem confiança — continua presente. As alucinações não aumentaram
  • Q4_K_M foi pior que Q3_K_M. Foram 37% contra 48% em textos longos. Há alguma variação com 100 perguntas, mas 11 pontos é bastante. Ainda não investiguei o motivo
  • Q2_K recusa 78% dos textos longos. Já é outro modelo

Resultados: velocidade

Formato Velocidade de geração Tempo médio por execução
PyTorch fp32 (referência) parágrafo 14,6 · texto longo 6,7 tok/s —
F32 18,7 tok/s 2,76 segundos
F16 35,9 tok/s 2,08 segundos
Q8_0 59,0 tok/s 1,93 segundos
Q6_K 73,1 tok/s 1,33 segundos
Q5_K_M 81,2 tok/s 1,26 segundos
Q4_K_M 89,1 tok/s 1,10 segundos
Q3_K_M 103,4 tok/s 1,12 segundos
Q2_K 119,9 tok/s 1,07 segundos

Os números do llama.cpp são a média de 400 execuções (reunindo parágrafos, textos longos e casos sem resposta). Os números do PyTorch foram medidos no artigo anterior.

  • A velocidade de geração é quase inversamente proporcional ao tamanho. Na CPU, ela depende mais da velocidade de leitura dos pesos da memória do que do cálculo; portanto, quanto menor, mais rápido
  • Mesmo em F32, o llama.cpp é mais rápido que o PyTorch
  • Porém, abaixo de Q6_K, o tempo por execução quase não diminui. Nesse comprimento, o tempo inicial para ler o texto passa a pesar mais

Vídeo: fiz a CPU responder à mesma pergunta com texto longo

Forneci um texto de cerca de 3.200 tokens e perguntei: “Quando Need for Speed Heat foi lançado?”. A resposta correta é 8 de novembro de 2019.

Como a execução foi gravada e também renderizava a tela no navegador, as velocidades do vídeo são menores que as da tabela acima. Considere-o uma comparação lado a lado.

Formato Velocidade de geração no vídeo Até o primeiro caractere Total Resultado
PyTorch fp32 5,5 tok/s 4,8 segundos 18,5 segundos Correto
F32 14,2 tok/s 2,8 segundos 8,1 segundos Correto
F16 25,9 tok/s 3,5 segundos 6,4 segundos Correto
Q8_0 38,2 tok/s 4,0 segundos 5,9 segundos Correto
Q6_K 41,6 tok/s 2,6 segundos 4,4 segundos Correto
Q5_K_M 46,3 tok/s 2,6 segundos 4,3 segundos Correto
Q4_K_M 47,2 tok/s 2,3 segundos 4,7 segundos Correto (resposta mais longa)
Q3_K_M 56,7 tok/s 2,5 segundos 4,5 segundos Correto (resposta mais longa)
Q2_K 61,3 tok/s 2,7 segundos 2,8 segundos Recusou

De F32 a Q5_K_M, tanto a resposta quanto a citação foram idênticas, caractere por caractere, à versão em PyTorch. Q4_K_M e Q3_K_M deram respostas mais longas, mas as citações estavam corretas. Mesmo havendo resposta, Q2_K recusou dizendo: “Não há resposta no texto”.

PyTorch fp32 (modelo original)

PyTorch fp32 rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp F32 (1.233MB)

llama.cpp F32 rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp F16 (617MB)

llama.cpp F16 rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp Q8_0 (329MB)

llama.cpp Q8_0 rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp Q6_K (254MB)

llama.cpp Q6_K rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp Q5_K_M (223MB)

llama.cpp Q5_K_M rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp Q4_K_M (193MB)

llama.cpp Q4_K_M rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp Q3_K_M (160MB)

llama.cpp Q3_K_M rodando na CPU e respondendo a uma pergunta com contexto longo再生

llama.cpp Q2_K (129MB)

llama.cpp Q2_K rodando na CPU e respondendo a uma pergunta com contexto longo再生

Considerações

  • Para rodar este modelo na CPU, escolha Q6_K. Mantém a precisão original, ocupa 1/5 do tamanho e gera cerca de 5 vezes mais rápido que o PyTorch. Não há motivo para hesitar
  • Para uma margem extra de segurança, escolha Q8_0. É praticamente igual ao original e tem 329 MB. Ainda é suficientemente pequeno
  • Modelos pequenos, de 300 milhões de parâmetros, são frágeis à quantização. Dizem que modelos grandes quase não degradam mesmo em Q4, mas aqui a queda foi visível a partir de Q5. Como o modelo já não tem muita folga, reduzir os pesos faz diferença
  • É bom que ele quebre recusando. Para RAG, recusar é mais seguro do que aumentar as alucinações. O efeito de ter ensinado a recusar com aprendizagem por reforço no artigo anterior também aparece aqui
  • Nos modelos pequenos, a diferença de velocidade diminui. A partir de Q6_K, mesmo que a geração fique mais rápida, ainda resta o tempo para ler o texto; por isso, a percepção muda pouco. No vídeo, Q6_K a Q3_K_M ficaram todos na faixa dos 4 segundos
  • Ainda não sei por que Q4_K_M perdeu para Q3_K_M. Quero investigar aumentando o número de perguntas e examinando quais camadas são quantizadas e como