Qwen 35B en una RTX 4070: 2 Flags para Pasar de 12 a 34 tok/s (2.8x)
Una tarjeta gráfica con 12 GB de VRAM no debería tragarse un modelo de 35 mil millones de parámetros. Los pesos ocupan unos 20 GB incluso con cuantización de 4 bits. Las cuentas no salen, y punto.
Y aun así, en mi RTX 4070 de 12 GB, Qwen3.5-35B-A3B me genera a 34.6 tokens por segundo. La cifra aparece en cuanto le metes dos flags que van a contrapelo del sentido común, -ngl 99 y --cpu-moe.
Aquí cuento por qué la primera configuración obvia me daba 12.2 tok/s, por qué la que acabó ganando llega a 34.6 tok/s, y por qué esa diferencia de 2.8x se explica cambiando el modelo mental sobre qué parte del modelo debe vivir en qué memoria.
La configuración perezosa: Ollama, 12.2 tok/s
Arranco con lo más obvio. Ollama 0.20.2, misma cuantización Q4_K_M, misma tarjeta. Ollama huele el tamaño del modelo, mira cuánta VRAM tiene libre y hace el reparto automático: 58 % de los pesos a la CPU, 42 % a la GPU. VRAM consumida: 11.4 GB. Velocidad medida: 12.2 tok/s.
No está mal como cifra. Es lo que suele dar un runtime cuando decide por ti con una heurística de propósito general, y para muchos casos con eso te apañas. El pero salta cuando caes en la cuenta de que Ollama optimiza como si esto fuera un modelo denso, y Qwen3.5-35B-A3B de denso no tiene nada.
La pista escondida en el nombre: A3B
El nombre del modelo lleva un aviso que casi nadie mira: A3B. La A viene de “Active” y el 3B son los parámetros que se activan de verdad en cada paso de inferencia. Es un Mixture-of-Experts (MoE): guarda 35B en total, pero solo enciende 3B por token.
Ese dato tira por tierra la idea de qué significa “grande” en memoria. En un denso de 35B, los 35B se calculan todos, en cada token, sin excepciones. En un MoE de 35B-A3B, los 35B ocupan sitio pero solo 3B pasan por la unidad aritmética. Ocupar sitio apenas cuesta; la que devora ancho de banda es la parte que calcula.
Ahí es donde asoma el flag que va a contrapelo.

Los dos flags: -ngl 99 y --cpu-moe
Con llama.cpp compilado con CUDA habilitado, la línea que gana es:
llama-server -m qwen35.gguf -ngl 99 --cpu-moe -c 4096
Lo que dice cada flag:
-ngl 99: “mete todas las capas del modelo en la GPU”. El 99 es la convención para decir “todas”; el modelo tiene 48 capas, así que cualquier número por encima da lo mismo.--cpu-moe: “salvo los expertos MoE, que se quedan en la CPU”. La excepción a lo anterior.
Leído en frío suena a contradicción. Decimos “todo a la GPU” y en la misma línea “menos este trozo”. Pero justo ese trozo que se queda fuera es el que hace que el resto quepa. Y hay una razón física por la que encima corre más rápido.
El culpable es el ancho de banda de memoria. Attention y KV cache leen y escriben datos a mansalva por cada token; ahí la GPU, con sus cientos de GB/s, se pasea por delante de la CPU con sus decenas. Los expertos MoE juegan en otra liga: por cada token se activan 8 de 128, así que la carga queda dispersa y ligera. Para eso la CPU llega de sobra. Y quitarles ese hueco a los expertos deja libre en la GPU justo lo que sí saca partido al ancho de banda.
Cifras al canto: 11.7 GB de VRAM (el 95 % de los 12 GB disponibles), 34.6 tok/s de velocidad, y la salida sale igual de bien.
El barrido completo, para que nadie tenga que creerme
Es fácil soltar “2.8x más rápido” y esperar que el lector se lo trague. Prefiero enseñar el barrido entero, hecho con llama-bench, generando 128 tokens en 3 iteraciones y variando cuántas capas de expertos van a la CPU:
| n_cpu_moe | Capas de expertos en GPU | tg128 (tok/s) | vs Ollama |
|---|---|---|---|
| 48 | 0 (todos en CPU) | 34.60 | 2.8x |
| 44 | 4 | 27.19 | 2.2x |
| 40 | 8 | 16.88 | 1.4x |
| 36 | 12 | 15.29 | 1.3x |
| 32 | 16 | 14.06 | 1.2x |
| 28 | 20 | 12.85 | 1.1x |
| 24 | 24 | 11.71 | 0.96x |
La curva baja de forma monótona, sin sorpresas. Cuantos más expertos vuelves a colar en la GPU, más lento tira todo. Al final del recorrido, con la mitad en GPU y la mitad en CPU, la velocidad se queda por debajo del auto-reparto de Ollama.
Este cuadro fue el que me terminó de convencer. “Meter todo en la GPU” resulta ser peor incluso que dejarle el marrón al reparto automático de la herramienta perezosa. La intuición te la mete doblada en cuanto la arquitectura del modelo deja de encajar con lo que el runtime da por hecho.
Los tres detalles que impiden copiar y pegar
Este número no cae del cielo en cualquier RTX 4070 solo por pegar el comando. Hay tres cosas que quiero dejar apuntadas para ahorrarle frustraciones a quien venga detrás.
Primero: la VRAM tiene que estar de verdad libre. Chrome con cuarenta pestañas, Discord, un cliente de streaming; todo eso muerde VRAM en segundo plano. Si al lanzar el modelo la GPU ya arrastra 2 o 3 GB ocupados, las cuentas se van al garete: no queda hueco para attention y KV cache, y el rendimiento se cae por el precipicio. Mi medición se hizo con la VRAM lo más limpia posible, con los procesos de fondo cerrados.
Segundo: el modo “thinking” de Qwen3.5. Este modelo puede escupir tokens de razonamiento antes de la respuesta final. Al medir la velocidad de generación eso te embarulla la cifra, porque los tokens de thinking cuentan pero no salen en la respuesta visible. Para comparar en condiciones, hay que fijar la modalidad (con o sin thinking) y no moverla ni un dedo.
Tercero: la cuantización y la compilación. El 34.6 tok/s se midió con Q4_K_M y con llama.cpp compilado con CUDA activo. Si tiras de otra cuantización o de una compilación sin CUDA, tu número saldrá distinto. Y peor. Reproducir el resultado depende tanto de esas dos variables como del propio comando.
El precio de este truco: casi cero holgura de VRAM
Toca hablar del coste. Esta configuración se come el 95 % de la VRAM disponible, con lo que el colchón es mínimo. En cuanto le pides un contexto largo (típico: un agente CLI que te suelta 15 000 tokens de system prompt y definiciones de herramientas), la VRAM se satura y la velocidad se desploma en picado.
La salida en ese caso es otro par de flags, esta vez sobre la cuantización del KV cache. Ya lo conté en un artículo anterior. Lo que importa hoy es el orden: primero llegas a 34.6 tok/s con -ngl 99 --cpu-moe, y luego decides qué haces con el colchón que queda.
Lo que sigue valiendo cuando el modelo caduque
Qwen3.5-35B-A3B se va a quedar viejo. Aparecerá Qwen 4, con otra arquitectura y otra cuantización, más listo que su antecesor. El principio que hace que estos dos flags funcionen, en cambio, sigue en pie mientras existan modelos MoE en hardware doméstico: el ancho de banda de memoria es un recurso escaso en la GPU y los cálculos dispersos aguantan bien en la CPU. La faena del ingeniero es emparejar cada pieza con el recurso al que mejor se acopla.
Cuando llegue la siguiente generación, la pregunta útil será “¿qué parte del modelo depende del ancho de banda y qué parte es cálculo disperso?”. Con esa pregunta en la mano, dar con los flags buenos es cosa de una tarde con llama-bench.
Si quieres reproducir el número exacto o ver el proceso completo, tengo las notas de laboratorio del experimento con el paso a paso.
ken imoto · WebRTC & Voice AI engineer · kenimoto.dev
¿Te resultó útil este artículo?