Pular para o conteúdo
Fantástico Mundo de Jon
RSS

Quando o modelo não cabe: o custo real do offload para CPU

No post anterior estimei que jogar camadas para a CPU custa dez vezes o desempenho. Medi nas duas placas e errei: o mesmo modelo cai de 66 para 3,1 tokens por segundo. E nada no runtime avisa quando isso acontece.

Com pressa? Peça o TL;DR ao Claude — ele lê a página e resume.

No post sobre quanta VRAM um LLM ocupa eu fechei com uma lista de saídas para quando o modelo não cabe. A quarta era jogar camadas para a CPU, com esta ressalva: “funciona, cabe quase qualquer coisa — e é dez vezes mais lento.”

Dez vezes era estimativa. Fui medir.

Errei — para menos. O mesmo modelo, na mesma máquina, com a mesma pergunta: 66,3 tokens por segundo na placa onde cabe, 3,1 na placa onde não cabe. Vinte e uma vezes.

A bancada

Bancada · agosto de 2026

Placas
RTX 5090 (32 GiB) e RTX 5070 (12 GiB), mesma máquina
Runtime
Ollama 0.32.5, flash attention ligado
KV cache
q8_0 — a troca recomendada no post anterior
Contexto
32.768 tokens
Carga
prompt técnico real, 200 tokens de saída
Amostragem
mediana de 3 execuções, após warmup

Seis modelos, de 4,9 a 20 GB, escolhidos para atravessar a fronteira dos 12 GiB da placa menor. Cada um roda em instância própria do runtime, com a GPU fixada — por motivos que ficam claros no fim do post.

A medição

RTX 5090 · 32 GiB

Modelo Arquivo Na GPU tok/s TTFT Prefill tok/s
llama3.1:8b 4,9 GB 100% 215,3 0,20 s 6946
qwen3-vl:8b 6,1 GB 100% 190,1 0,20 s 6320
gemma3:12b 8,1 GB 100% 115,1 0,44 s 2828
deepseek-r1:14b 9,0 GB 100% 125,6 0,21 s 3889
gpt-oss:20b 13,0 GB 100% 230,1 0,36 s 6036
qwen3:32b 20,0 GB 100% 66,3 0,26 s 2164
Mediana de 3 execuções em RTX 5090, Ollama 0.32.5, KV cache q8_0, 32k de contexto, 200 tokens de saída. TTFT medido no cliente; throughput reportado pelo runtime. Todos os seis modelos couberam inteiros na VRAM.

Na placa de 32 GiB tudo cabe, e a leitura é simples: modelo maior tende a ser mais lento. Agora a mesma bateria na placa de 12 GiB.

RTX 5070 · 12 GiB

Modelo Arquivo Na GPU tok/s TTFT Prefill tok/s
llama3.1:8b 4,9 GB 100% 105,9 0,26 s 3864
qwen3-vl:8b 6,1 GB 100% 100,7 0,21 s 3766
gemma3:12b 8,1 GB 100% 62,5 0,48 s 1562
deepseek-r1:14b 9,0 GB 100% 59,6 0,26 s 2141
gpt-oss:20b 13,0 GB 75% 52,2 0,84 s 407
qwen3:32b 20,0 GB 52% 3,1 3,62 s 46
Mesma bateria, mesma máquina, RTX 5070. A coluna 'Na GPU' vem do próprio runtime e indica quanto do modelo ficou residente na VRAM; o restante foi para a RAM do sistema. Confirmado contra o pico de VRAM lido do driver.

Os quatro primeiros modelos cabem inteiros e a placa maior entrega cerca do dobro — diferença real, mas linear e sem graça. A história está nas duas últimas linhas.

O que a queda tem de específico

O qwen3:32b não fica “duas vezes mais lento” na placa menor. Ele fica 21 vezes mais lento, e a degradação não é uniforme:

Métrica RTX 5090 (100% GPU) RTX 5070 (52% GPU) Fator
Geração 66,3 tok/s 3,1 tok/s 21×
Tempo até o 1º token 0,26 s 3,62 s 14×
Processamento do prompt 2164 tok/s 46 tok/s 47×
qwen3:32b nas duas placas, mesma pergunta e mesmo runtime. O prefill é a etapa que mais sofre: é onde o modelo lê o que você escreveu antes de começar a responder.

O prefill é o mais castigado, com fator 47. Ele é a etapa em que o modelo processa o que você escreveu — e é justamente a etapa que cresce quando o prompt é longo. Ou seja: quanto mais contexto você usa, mais dolorido fica o offload, exatamente no cenário em que ter um modelo grande faria diferença.

Vale dizer que os 3,1 tokens por segundo saíram de uma configuração já otimizada: KV cache em q8_0, que é a segunda recomendação daquela lista. Sem ela, o número seria pior.

Nada disso dá erro

Este é o ponto que me fez escrever o post em vez de só atualizar uma tabela.

Quando o modelo não cabe, nada quebra. Não há erro, não há aviso, não há linha vermelha no log. O runtime move parte das camadas para a RAM e segue funcionando. Você pergunta, ele responde, a resposta está correta.

Ela só demora vinte e uma vezes mais.

E como você provavelmente nunca rodou esse modelo numa placa onde ele coubesse, não tem com o que comparar. A conclusão natural é “modelo grande é lento nessa máquina” — quando o certo seria “este modelo específico está metade na RAM, e existe um modelo melhor para esta placa”.

A recomendação anterior se confirma

A lista do post anterior terminava assim: “um 12B bem escolhido rodando inteiro na GPU entrega mais valor por segundo do que um 32B metade na CPU.”

Nos números desta bancada, na mesma placa de 12 GiB:

  • gemma3:12b, inteiro na GPU: 62,5 tok/s
  • qwen3:32b, 52% na GPU: 3,1 tok/s

Vinte vezes mais rápido. A recomendação estava certa, e por uma margem maior do que eu imaginava ao escrevê-la.

Duas coisas que a aritmética não previu

O tamanho do arquivo não diz se vai caber. O gpt-oss:20b ocupa 13 GB em disco — mais do que os 12 GiB nominais da placa — e ainda assim carregou 75% na GPU e manteve 52,2 tok/s, perfeitamente utilizável. Já o qwen3:32b, com 20 GB, ficou em 52%. Quantização, formato dos pesos e a configuração do cache mudam a conta, e nada disso aparece no tamanho que o ollama list mostra.

Modelo maior não é modelo mais lento. O mais rápido da RTX 5090 não foi o menor da lista: foi o gpt-oss:20b, com 230,1 tok/s — à frente de um modelo de 8B com menos de metade do tamanho. É arquitetura MoE, que ativa só uma fração dos parâmetros por token. Quem escolhe o modelo pequeno “porque é mais rápido” está usando uma regra que não se sustenta na medição.

O erro que quase entrou neste post

Aqui está o que eu faria diferente — e o motivo de cada modelo rodar em instância isolada com a GPU fixada.

A primeira versão desta bancada produziu estes números para a placa menor:

Modelo RTX 5090 RTX 5070 (falso) RTX 5070 (real)
qwen3:32b 65,8 tok/s 61,6 tok/s 3,1 tok/s
llama3.1:8b 215,7 tok/s 186,8 tok/s 105,9 tok/s
Coluna do meio: resultado da bateria em que a GPU não estava de fato fixada. Os números são plausíveis e estão completamente errados — foram medidos na placa grande.

Repare na coluna do meio. 61,6 contra 65,8 tokens por segundo parece exatamente a diferença que se espera entre duas placas diferentes. Nada ali levanta suspeita.

Só que a RTX 5070 nunca foi tocada. A bateria inteira rodou na 5090.

O motivo: numa máquina com duas GPUs, CUDA_VISIBLE_DEVICES não basta para fixar a placa no Ollama. Ele filtra o backend CUDA — mas o Ollama também enumera placas pelo backend Vulkan, que é outra API e ignora essa variável. O agendador enxergou a placa “escondida”, viu que tinha mais memória livre, e escolheu ela. O log deixa isso explícito para quem sabe o que procurar:

grep 'selecting single GPU' ollama.log

msg=“user overrode visible devices” CUDA_VISIBLE_DEVICES=1 library=Vulkan name=Vulkan0 description=“NVIDIA GeForce RTX 5090” library=CUDA name=CUDA0 description=“NVIDIA GeForce RTX 5070” msg=“selecting single GPU” main_gpu=0 library=Vulkan description=“NVIDIA GeForce RTX 5090”

saída

O que denunciou o erro não foi o número — foi ter instrumentado o pico de VRAM lido do driver. A placa que eu achava estar medindo aparecia com 15 MB em uso. Uma placa ociosa não gera 61 tokens por segundo.

A correção são três variáveis, e não uma:

CUDA_VISIBLE_DEVICES=1      # filtra o backend CUDA
GGML_VK_VISIBLE_DEVICES=1   # filtra o backend Vulkan
OLLAMA_VULKAN=0             # ou simplesmente desliga o Vulkan

Os outros quatro, para quem for medir a própria máquina:

  1. A primeira geração mede o seu SSD. Ela inclui carregar o modelo do disco. Sem descartá-la com um warmup, o “tempo de resposta” de um modelo de 20 GB é um teste de armazenamento.
  2. Descarregar o modelo entre as repetições arruína o TTFT. Com keep_alive=0, cada repetição recarrega tudo, e o tempo até o primeiro token vira tempo de recarga: medi 8,5 s onde o valor real era 0,19 s.
  3. Repetir o mesmo prompt faz o cache mentir. O runtime reaproveita o prefill já processado e o TTFT despenca. Varie o texto a cada repetição.
  4. Modelos de raciocínio não respondem pelo campo que você está lendo. qwen3 e deepseek-r1 emitem o pensamento num campo separado da resposta. Cronometrar o primeiro token olhando só response registra TTFT zero nesses modelos — e zero não é um valor suspeito, é um valor impossível.

Meça a sua

O código da bancada está aberto, e roda em qualquer máquina com GPU NVIDIA e Ollama. Ele sobe a própria instância do runtime, uma GPU por vez, sem tocar na configuração da máquina:

git clone https://github.com/SEU-USUARIO/bench-llm-local
cd bench-llm-local && pip install requests matplotlib
python3 bench.py       # todas as GPUs detectadas
python3 relatorio.py   # tabela e gráfico

Um aviso para fechar, porque ele importa mais que qualquer número acima: velocidade não é competência. O gpt-oss:20b foi o mais rápido desta bancada, e isso não diz nada sobre ele acertar mais que o qwen3:32b, que faz um terço da velocidade. Se o modelo erra a tarefa, tokens por segundo só significam chegar mais rápido à resposta errada.

O que esta medição responde é a outra metade da pergunta — a que costuma ser pulada, e a que faz gente desistir de rodar modelo local achando que a máquina não dá conta. Quase sempre dá. Só não com aquele modelo.