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 |
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 |
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× |
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/sqwen3: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 |
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”
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:
- 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.
- 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. - 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.
- Modelos de raciocínio não respondem pelo campo que você está lendo.
qwen3edeepseek-r1emitem o pensamento num campo separado da resposta. Cronometrar o primeiro token olhando sóresponseregistra 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.