U-16 · pruebas · agosto de 2026

Offloadear expertos MoE en llama.cpp: dónde está el muro y por qué más bits no compró nada

Todos los modelos densos de esta serie de bake-offs —Devstral, Qwen3.8— caben enteros en la GPU o no caben, sin término medio. Un modelo MoE (Mixture of Experts) rompe esa dicotomía: solo una fracción de sus parámetros se activa por token, así que llama.cpp puede dejar las capas de atención en la GPU y mandar los expertos —la parte más pesada— a la RAM del sistema con --n-cpu-moe (-ncmoe). La pregunta que quería responder era simple: ¿cuánto cuesta eso en velocidad, y compensa alguna vez pagarlo para poder subir de cuantización?

Hardware: la misma RTX 3090 Ti de 24GB y 64GB de RAM dual-channel de siempre. Modelo: Ornith-1.5-35B-A3B (arquitectura qwen35moe, 40 capas, 256 expertos con 8 activos por token, ~3B parámetros activos de 35.5B totales) — lo bastante grande para forzar un offload real, a diferencia de DeepSeek-Coder-V2-Lite (16B), que cabe entero en 9.7GB y no sirve para este experimento.

El sweep con el modelo que ya cabía entero (Q4_K_M)

Con la cuantización que ya tenía en disco (Q4_K_M, 21.7GB), el modelo entero cabe en los 24GB de la tarjeta. Offloadear aquí es opcional, así que sirve como línea base de cómo degrada la velocidad capa a capa:

--n-cpu-moeVRAM picopp512 (tok/s)tg128 (tok/s)
022.0 GB3741189.0
818.2 GB1449128.6
1614.7 GB98695.3
2411.1 GB74872.7
327.6 GB60356.0
403.8 GB51750.8

(Tabla completa, de 4 en 4 capas, en el repo — aquí solo los puntos que cuentan la forma de la curva.) No hay ningún salto brusco: la degradación es suave y continua, de 189 a 51 tok/s de generación entre 0 y 40 capas offloadeadas. Con Q4_K_M el punto óptimo es simplemente -ncmoe 0 — offloadear aquí solo tiene sentido si hace falta liberar VRAM para otra cosa (más contexto, otro proceso), no para que el modelo quepa.

El sweep con la cuantización que no cabe entera (Q6_K)

Para forzar un offload obligatorio descargué la misma arquitectura en Q6_K (28.4GB, no cabe en 24GB de VRAM):

--n-cpu-moeVRAM picopp512 (tok/s)tg128 (tok/s)Resultado
0–8OOM al cargar
1222.4 GB89987.8primer punto que carga
1619.9 GB76875.1
2414.8 GB57557.6
329.7 GB46547.1
404.4 GB38939.4

Aquí sí hay un muro real, pero no es de rendimiento — es de admisión: por debajo de -ncmoe 12 el proceso ni siquiera arranca, y a partir de ahí cae en la misma curva suave de antes. Con ncmoe=12 quedan solo ~1.2GB de margen sobre 24GB totales, demasiado justo para nada más que el propio benchmark; para uso real el punto operativo razonable es ncmoe=16 (19.9GB, ~4GB de margen), sacrificando ~15% de tok/s de generación por ese colchón.

La comparación que importa: ¿compensa subir de Q4 a Q6 si eso te obliga a offloadear?

Con las mismas baterías de calidad que uso en los bake-offs de modelo (HumanEval, código propio, agente propio), Q4_K_M full-GPU contra Q6_K + offload en el punto operativo (ncmoe=16):

Q4_K_M, full GPUQ6_K + offload (ncmoe=16)
HumanEval (n=60)47/60 (78.3%)48/60 (80.0%)
Código propio (n=30)27/30 (90%)27/30 (90%)
Agente propio (n=6)6/6 (100%)6/6 (100%)
tok/s generación185.470.8
TTFT0.133s0.63s

La diferencia de +1/60 en HumanEval es ruido de muestreo, no señal — con pass@1 determinista por greedy sampling, un problema de diferencia sobre 60 no prueba nada; haría falta repetir con varios seeds o subir a los 164 problemas completos. Código y agente dan el mismo resultado exacto en ambos quants. A cambio, Q6_K + offload cuesta más de la mitad del throughput de generación y casi 5x el tiempo hasta el primer token.

Con un modelo A3B (~3B parámetros activos por token), subir de 4 a 6 bits en los pesos no compra calidad medible en estas tareas — a esa escala de activación, Q4_K_M ya no es el cuello de botella de precisión. Q4_K_M full-GPU gana en todos los ejes: misma calidad, más del doble de velocidad, sin depender del ancho de banda de la RAM.

Por qué cae tanto, y no solo en generación

Lo que más me sorprendió no fue la caída en generación —esperable, cada token offloadeado paga la latencia de RAM— sino que el prompt processing también se desploma un 75-79%. La razón es que --n-cpu-moe manda todos los expertos de esa capa a RAM, no solo los 8 de 256 que se activarían para ese prompt concreto: da igual que el cálculo real solo toque una fracción mínima, la capa entera paga el viaje.

Conclusión práctica


El comando de reproducción (llama-bench con el sweep de --n-cpu-moe) y el harness de calidad (candidate_bench.py, con HumanEval, código y agente) están en local-llm-arena, repo público de los bake-offs de esta serie. Las tablas de este experimento concreto son locales por ahora, no forman parte del repo.

← volver al rack