Saltar al contenido
Fantástico Mundo de Jon
RSS

Cuando el modelo no cabe: el coste real del offload a la CPU

En la publicación anterior estimé que mover capas a la CPU cuesta diez veces el rendimiento. Medí en las dos tarjetas y me equivoqué: el mismo modelo cae de 66 a 3,1 tokens por segundo. Y nada en el runtime avisa cuando esto sucede.

¿Con prisa? Pide el TL;DR a Claude — lee la página y la resume.

es Traducción automática por gpt-oss:20b, revisada por el autor. Leer el original en portugués

En el post sobre cuánta VRAM ocupa un LLM cerré con una lista de salidas para cuando el modelo no cabe. La cuarta era mover capas a la CPU, con esta advertencia: “funciona, cabe casi cualquier cosa — y es diez veces más lento.”

Diez veces era una estimación. Me fui a medir.

Me equivoqué — en menor medida. El mismo modelo, en la misma máquina, con la misma pregunta: 66,3 tokens por segundo en la tarjeta donde cabe, 3,1 en la tarjeta donde no cabe. Veint y una veces.

La bancada

Mesa · agosto de 2026

Tarjetas
RTX 5090 (32 GiB) y RTX 5070 (12 GiB), misma máquina
Tiempo de ejecución
Ollama 0.32.5, flash attention ligado
Cache KV
q8_0 — el cambio recomendado en el post anterior
Contexto
32.768 tokens
Carga
prompt técnico real, 200 tokens de salida
Muestreo
mediana de 3 ejecuciones, después del warmup

Seis modelos, de 4,9 a 20 GB, elegidos para atravesar la frontera de los 12 GiB de la tarjeta menor. Cada uno corre en su propia instancia del runtime, con la GPU fijada — por motivos que quedan claros al final del post.

La medición

RTX 5090 · 32 GiB

Modelo Archivo En la 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 ejecuciones en RTX 5090, Ollama 0.32.5, KV cache q8_0, 32k de contexto, 200 tokens de salida. TTFT medido en el cliente; throughput reportado por el runtime. Todos los seis modelos cabieron íntegramente en la VRAM.

En la tarjeta de 32 GiB todo cabe, y la lectura es simple: el modelo mayor tiende a ser más lento. Ahora la misma batería en la tarjeta de 12 GiB.

RTX 5070 · 12 GiB

Modelo Archivo En la 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
Misma batería, misma máquina, RTX 5070. La columna 'En la GPU' proviene del propio runtime y indica cuánto del modelo quedó residente en la VRAM; el resto fue a la RAM del sistema. Confirmado contra el pico de VRAM leído del controlador.

Los cuatro primeros modelos caben íntegramente y la tarjeta mayor entrega casi el doble — diferencia real, pero lineal y sin gracia. La historia está en las dos últimas líneas.

Lo que la caída tiene de específico

Medida RTX 5090 (100% GPU) RTX 5070 (52% GPU) Factor
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 en las dos tarjetas, misma pregunta y mismo runtime. El prefill es la etapa que más sufre: es donde el modelo lee lo que escribiste antes de comenzar a responder.

El prefill es el más castigado, con factor 47. Es la etapa en que el modelo procesa lo que escribiste — y es justamente la etapa que crece cuando el prompt es largo. Es decir: cuanto más contexto uses, más doloroso se vuelve el offload, exactamente en el escenario donde tener un modelo grande haría diferencia.

Vale decir que los 3,1 tokens por segundo salieron de una configuración ya optimizada: KV cache en q8_0, que es la segunda recomendación de esa lista. Sin ella, el número sería peor.

Esto no produce errores

Este es el punto que me hizo escribir la publicación en lugar de solo actualizar una tabla.

Cuando el modelo no cabe, no se rompe nada. No hay error, ni advertencia, ni línea roja en el registro. El runtime mueve parte de las capas a la RAM y sigue funcionando. Tú preguntas, él responde, la respuesta es correcta.

Solo tarda veint y una veces más.

Y como probablemente nunca has ejecutado ese modelo en una tarjeta donde cabiera, no tienes con qué comparar. La conclusión natural es ‘el modelo grande es lento en esa máquina’ — cuando lo correcto sería ‘este modelo específico está a mitad en la RAM, y existe un modelo mejor para esta tarjeta’.

La recomendación anterior se confirma

La lista del post anterior terminaba así: ‘un 12B bien elegido corriendo entero en la GPU entrega más valor por segundo que un 32B a mitad en la CPU.’

En los números de esta bancada, en la misma tarjeta de 12 GiB:

  • gemma3:12b, entero en la GPU: 62,5 tok/s
  • qwen3:32b, 52% en la GPU: 3,1 tok/s

Veinte veces más rápido. La recomendación estaba correcta, y por una margen mayor de lo que imaginaba al escribirla.

Dos cosas que la aritmética no previó

El tamaño del archivo no indica si cabrá. El gpt-oss:20b ocupa 13 GB en disco — más de los 12 GiB nominales de la tarjeta — y aun así cargó 75% en la GPU y mantuvo 52,2 tok/s, perfectamente utilizable. Ya el qwen3:32b, con 20 GB, quedó en 52%. La cuantización, formato de los pesos y la configuración del cache cambian la cuenta, y nada de eso aparece en el tamaño que muestra el ollama list.

Modelo mayor no es modelo más lento. El más rápido de la RTX 5090 no fue el menor de la lista: fue el gpt-oss:20b, con 230,1 tok/s — a la frente de un modelo de 8B con menos de mitad del tamaño. Es arquitectura MoE, que activa solo una fracción de los parámetros por token. Quien elige el modelo pequeño “porque es más rápido” está usando una regla que no se sostiene en la medición.

El error que casi entró en esta publicación

Aquí está lo que haría diferente — y la razón por la cual cada modelo corre en una instancia aislada con la GPU fijada.

La primera versión de esta bancada produjo estos números para la tarjeta 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
Columna del medio: resultado de la batería en la que la GPU no estaba realmente fijada. Los números son plausibles y están completamente incorrectos — fueron medidos en la tarjeta grande.

Observa la columna del medio. 61,6 contra 65,8 tokens por segundo parece exactamente lo que se espera entre dos tarjetas distintas. Nada allí levanta sospecha.

Pero la RTX 5070 nunca fue tocada. La batería completa corrió en la 5090.

La razón: en una máquina con dos GPUs, CUDA_VISIBLE_DEVICES no basta para fijar la tarjeta en Ollama. Filtra el backend CUDA — además Ollama también enumera tarjetas por el backend Vulkan, que es otra API y ignora esa variable. El registro deja esto explícitamente para quien sepa lo que buscar:

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

Lo que denunció el error no fue el número — fue haber instrumentado el pico de VRAM leído del controlador. La tarjeta que yo creía estaba midiendo aparecía con 15 MB en uso.

La corrección son tres variables, y no una:

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

Los otros cuatro, para quien mida su propia máquina:

  1. La primera generación mide tu SSD. Incluye cargar el modelo del disco. Sin descartarlo con un warmup, el ‘tiempo de respuesta’ de un modelo de 20 GB es una prueba de almacenamiento.

  2. Descargar el modelo entre repeticiones arruina el TTFT. Con keep_alive=0, cada repetición recarga todo, y el tiempo hasta el primer token se convierte en tiempo de recarga: medí 8,5 s cuando el valor real era 0,19 s.

  3. Repetir el mismo prompt hace que la caché mienta. El runtime reutiliza el prefill ya procesado y el TTFT cae. Varia el texto en cada repetición.

  4. Los modelos de razonamiento no responden por el campo que estás leyendo. qwen3 y deepseek-r1 emiten el pensamiento en un campo separado de la respuesta. Cronometrar el primer token mirando solo response registra TTFT cero en estos modelos — y cero no es un valor sospechoso, es un valor imposible.

Mide la tuya

El código de la bancada está abierto, y corre en cualquier máquina con GPU NVIDIA y Ollama. Ele sube la propia instancia del runtime, una GPU por vez, sin tocar la configuración de la 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

Una advertencia para cerrar, porque importa más que cualquier número arriba: velocidad no es competencia. El gpt-oss:20b fue el más rápido de esta bancada, y eso no dice nada sobre él acertar más que el qwen3:32b, que hace un tercio de la velocidad. Si el modelo falla la tarea, tokens por segundo solo significan llegar más rápido a una respuesta errónea.

Lo que esta medición responde es la otra mitad de la pregunta — la que suele ser saltada, y la que hace que la gente deje de correr modelos locales pensando que la máquina no puede. Casi siempre funciona. Sólo no con ese modelo.