TL;DR
- Instalamos Darwin-180B-RSI en Q4 (111,3 GB en GGUF) en un portátil ARM con 32 GB de memoria unificada y lo medimos con
llama.cpp. El mejor resultado sin tocar la calidad del modelo fue unos 3 tokens por segundo, solo con CPU y 10 hilos (mediana 3,13 en tres corridas). - Los dos modelos más pequeños, POCKET-26B y POCKET-35B, caben enteros en memoria y generan 11,79 y 13,15 tok/s en la misma máquina. La diferencia de cuatro veces coincide con que esos dos modelos caben enteros en RAM y el de 180B no.
- Al medir el disco, cada solicitud de unos 95 tokens vuelve a leer entre 16 y 29 GB del SSD. Subir el tope de memoria de 20 a 23 GB bajó esa lectura de 29,0 a 22,3 GB. El cuello de botella es la residencia en la caché de páginas, no el cálculo.
- Lo que no funcionó, con números: poner la GPU a trabajar fue más lento (1,60 tok/s) porque la memoria es compartida; un modelo borrador especulativo dio 1,14 tok/s; la lectura anticipada (
madvise) no mejoró nada. - Todas las cifras son mediciones propias de una sola máquina, con una variación de corrida a corrida cercana a ±10%. Al final hay un script de 20 líneas para medir lo mismo en tu equipo.
¿Por qué un MoE enorme corre en un portátil?
Un modelo de mezcla de expertos (MoE) tiene muchos parámetros en total, pero en cada token solo se activa una fracción pequeña. En Darwin-180B hay 48 capas con 512 expertos por capa (24.576 expertos en total) y se consultan 10 por capa en cada token. Eso significa que, por token, solo se tocan unos 480 expertos, de unos 3 MiB cada uno en Q4. Un cálculo rápido:
10 expertos x 48 capas x 3,0 MiB ~= 1,5 GB de expertos activos por token
Con 1,5 GB por token, el cómputo en sí es modesto. Por eso es posible ejecutar un 180B en un equipo de consumo, y por eso llama.cpp puede mapear el archivo GGUF en memoria (mmap) y dejar que el sistema operativo traiga del disco solo las páginas que se usan.
El truco tiene una condición: las páginas que se necesitan en el siguiente token deben seguir en la caché de páginas. Si el sistema las expulsó, hay que volver a leerlas del SSD, y esa lectura es sincrónica con respecto al token que se está generando.
¿Con qué hardware y qué configuración medimos?
| Elemento | Valor |
|---|---|
| Equipo | Moore Threads AIBOOK, 12 núcleos ARM Cortex-A78, Ubuntu 24.04 |
| Memoria | 32 GB unificados (CPU, GPU y NPU comparten la misma RAM), unos 23 GB disponibles para el proceso |
| Almacenamiento | NVMe de 1 TB |
| Modelo grande | POCKET-Darwin-180B en GGUF, cuantización UD-Q4_K_XL, 111,3 GB en 4 archivos (SHA-256 verificado contra Hugging Face) |
| Modelos pequeños | POCKET-35B-Q4_K_M (21,2 GB) y POCKET-26B-Q4_K_M (16,8 GB) |
| Motor |
llama.cpp (fork con soporte MUSA), servidor local, 1 solicitud a la vez |
| Método | La misma pregunta (una explicación corta), generación determinista (greedy), 64 tokens (128 en los modelos pequeños), 3 corridas por configuración, mediana |
Dos advertencias sobre el método. Primero, es una sola máquina y una sola pregunta, así que sirve para entender dónde está el cuello de botella, no para publicar una cifra de rendimiento general. Segundo, entre corridas hay una variación de aproximadamente ±10%, de modo que diferencias del 5% (por ejemplo 10 contra 12 hilos) quedan dentro del ruido.
¿Cuánto más rápido va un modelo que cabe en memoria?
Este es el resultado que abre el artículo. En la misma máquina, solo con CPU, 10 hilos y tope de 23 GB:
| Modelo | Tamaño en disco | Generación (mediana de 3) | Lectura de disco |
|---|---|---|---|
| POCKET-26B (Gemma 4 A4B) | 16,8 GB | 11,79 tok/s (10,93 / 12,09 / 11,79) | casi nula (2,8 MB) |
| POCKET-35B (Darwin-36B, qwen35moe) | 21,2 GB | 13,15 tok/s (9,64 / 13,15 / 13,25) | casi nula |
| POCKET-Darwin-180B | 111,3 GB | 3,13 tok/s (2,86 / 3,26 / 3,13) | cientos de MB por segundo |
Los tres son MoE y activan solo una parte de sus pesos en cada token. Lo que separa a los dos primeros del tercero es que caben enteros en los 23 GB disponibles y el 180B no. Es una correlación en esta máquina, no una prueba causal, pero el siguiente experimento la refuerza.
¿Cuánto lee del SSD cada solicitud?
Añadimos al medidor contadores de fallos de página y de bytes leídos (/proc/<pid>/stat y /proc/diskstats). Con MTP de profundidad 1 y 10 hilos, para el 180B:
| Configuración | tok/s (mediana) | GB leídos por solicitud | Fallos mayores |
|---|---|---|---|
| Tope de memoria 20 GB | 2,61 | 29,0 | 538.140 |
| Tope 23 GB | 2,74 | 22,3 | 338.992 |
| 23 GB, lectura anticipada apagada (2 corridas) | 2,62 / 3,14 | 16,2 / 15,8 | 310.160 / 233.193 |
| 23 GB, lectura anticipada encendida (2 corridas) | 2,91 / 2,84 | 18,8 / 22,2 | 227.387 / 304.480 |
Una solicitud (unos 95 tokens entre entrada y salida) obliga a leer entre 16 y 29 GB del SSD. Con tres GB más de caché, la lectura bajó de 29,0 a 22,3 GB. Eso es una señal clara: cada gigabyte de residencia que ganas es lectura que dejas de hacer.
¿Qué tasa de aciertos de caché implica eso?
Con los datos anteriores se puede estimar cuántos bytes por token no estaban en memoria. Es una cuenta aproximada (supone 95 tokens por solicitud y 1,5 GB de expertos activos por token, e ignora los pesos densos):
TOK_REQ = 95
EXPERT_MIB, LAYERS, K = 3.0, 48, 10
activos_gb = EXPERT_MIB * LAYERS * K * 1.048576 / 1000 # ~1,51 GB por token
for nombre, tps, gb_req in [("tope 20 GB", 2.61, 29.0),
("tope 23 GB", 2.74, 22.3)]:
mb_tok = gb_req * 1000 / TOK_REQ
fallo = mb_tok / (activos_gb * 1000)
print(f"{nombre}: {mb_tok:.0f} MB/token desde disco, "
f"aciertos ~{(1 - fallo) * 100:.0f}%, "
f"lectura necesaria ~{mb_tok * tps / 1000:.2f} GB/s")
Resultado:
tope 20 GB: 305 MB/token desde disco, aciertos ~80%, lectura necesaria ~0.80 GB/s
tope 23 GB: 235 MB/token desde disco, aciertos ~84%, lectura necesaria ~0.64 GB/s
Un acierto de 80% a 84% suena alto, pero fíjate en lo que cuesta el 15% restante: unos 235 MB por token que hay que traer del SSD. Y como los fallos se resuelven de forma sincrónica, el tiempo del token incluye esa espera. Es posible que por eso, con 12 hilos, los 12 núcleos aparezcan ocupados al 1.145% aunque parte de ese uso sea espera activa y no cálculo útil. No separamos todavía esa fracción, así que es una hipótesis, no una medición.
¿Qué intentamos para acelerarlo y qué pasó?
Probamos varias vías. La tabla resume lo que quedó medido, siempre en la misma máquina y con la misma pregunta:
| Configuración | tok/s (mediana de 3) | Salida | Nota |
|---|---|---|---|
| Solo CPU, 10 hilos | 3,13 | correcta | el mejor resultado |
| Solo CPU, 12 hilos | 2,73 | correcta | dentro del ruido frente a 10 hilos |
| Solo CPU, 12 hilos, tope 22 GB | 2,48 | correcta | |
| GPU completa + expertos en CPU | 1,60 | correcta | más lento que solo CPU |
| Lo anterior + grafos MUSA | 1,51 | corrupta | salida incoherente, descartada |
| Borrador de 4B en GPU (decodificación especulativa) | 1,14 | correcta | 34 de 55 tokens del borrador aceptados (62%) |
| Caché de expertos en GPU (4 u 8 GB) | no medible | no arrancó en 5 minutos |
Hay tres lecciones que sirven más allá de este equipo.
La GPU no ayuda cuando la memoria es la misma. En una máquina con memoria unificada, mover los cálculos a la GPU no te da más ancho de banda, porque CPU y GPU leen de la misma RAM. Además, con los expertos en la CPU, cada capa obliga a pasar datos entre ambos lados y a sincronizar. El resultado fue 1,60 contra 3,13 tok/s. Una caché de expertos en GPU añade una copia más de los mismos datos en la misma RAM, justo lo que no sobra.
La decodificación especulativa tiene un costo distinto en un MoE. Un borrador de 4B aceptó el 62% de sus tokens, que es una tasa razonable, y aun así el resultado fue más lento. En un modelo denso, verificar k tokens cuesta casi lo mismo que verificar uno. En un MoE, cada token distinto puede activar otros 10 expertos por capa, así que verificar k tokens se acerca a k veces el trabajo. Con MTP (predicción multitoken) de profundidad 1 vimos una mejora aproximada del 20% en la mediana combinada de dos rondas (3,30 contra 2,74), pero los rangos de las 10 corridas se solapan, por lo que no lo damos por confirmado.
La lectura anticipada no resolvió un problema de capacidad. Probamos pedir al sistema que precargara los expertos recién elegidos con madvise(MADV_WILLNEED). La salida fue idéntica, pero la mejora quedó dentro del ruido y la lectura total de disco aumentó: los expertos precargados se expulsaban de una caché de 23 GB antes de usarse. Cuando el límite es la capacidad, cambiar el orden de las lecturas no ayuda.
¿Cuál es entonces el techo de un 180B en 32 GB?
Sin cambiar la calidad del modelo, nuestra conclusión es un techo cercano a 3 tokens por segundo (pasamos de 1,26 en la línea base inicial a unos 3,3 con ajustes, aunque la línea base usaba otro tope de memoria y no es una comparación estricta). Para pasar de ahí hay solo dos caminos honestos:
- Más memoria. Con 64 GB, más expertos permanecen residentes y los fallos se reducen de forma drástica.
- Pesos más pequeños. Una cuantización más agresiva o menos expertos por token reduce lo que hay que leer, pero cambia la calidad, así que pasa por una medición de precisión antes de adoptarla. En una prueba anterior, reducir a la mitad la cantidad de expertos activos dio una mejora de velocidad de apenas 4,5% a costa de 5,4 puntos de precisión, así que la descartamos.
Y hay un techo que sirve de referencia para cualquier plan que dependa del SSD: si todos los 1,5 GB por token vinieran del disco a 0,6 GB/s (la lectura que observamos), el límite sería de 0,4 tok/s. Que alcancemos 3 tok/s es coherente con que más del 80% de lo que se consulta ya esté en memoria (estimación).
¿Cómo medirlo en tu propio equipo?
Este script mide lectura de disco y fallos mayores por solicitud contra un servidor llama-server local. Es genérico: sirve para cualquier modelo GGUF.
import time, psutil, requests
def fallos_mayores(pid):
with open(f"/proc/{pid}/stat") as f:
campos = f.read().rsplit(")", 1)[1].split()
return int(campos[9]) # majflt: campo 12 del archivo
def medir(url, pid, prompt, n_predict=64):
d0, f0, t0 = psutil.disk_io_counters().read_bytes, fallos_mayores(pid), time.time()
r = requests.post(f"{url}/completion", timeout=900, json={
"prompt": prompt, "n_predict": n_predict, "temperature": 0}).json()
dt = time.time() - t0
gb = (psutil.disk_io_counters().read_bytes - d0) / 1e9
tokens = r["tokens_predicted"] + r["tokens_evaluated"]
return {"tok_s": r["tokens_predicted"] / dt, "gb_leidos": gb,
"mb_por_token": gb * 1000 / tokens,
"fallos_mayores": fallos_mayores(pid) - f0}
# for _ in range(3): print(medir("http://127.0.0.1:8080", PID_DEL_SERVIDOR, "Explica por que el cielo es azul."))
Cómo interpretarlo: si mb_por_token es cercano a cero, tu modelo cabe en memoria y el cuello de botella es otro. Si es de cientos de MB, estás en el régimen que describimos aquí y vale la pena variar el tope de memoria antes de tocar nada más. disk_io_counters() cuenta todo el sistema, así que cierra otros programas durante la medición.
Preguntas frecuentes
¿Se puede usar un MoE de 180B de forma práctica en un portátil de 32 GB?
Para generación interactiva larga, 3 tok/s es lento, pero sirve para pruebas, evaluación sin conexión o cargas por lotes. Para uso diario en un portátil de esta memoria, los modelos de 26B y 35B (11 a 13 tok/s en nuestra medición) son una opción mucho más cómoda.
¿Por qué no ayuda la GPU?
En esta máquina la memoria es unificada: CPU y GPU leen de la misma RAM, así que la GPU no aporta ancho de banda adicional. Con los expertos en CPU, cada capa exige pasar datos y sincronizar, lo que añade costo. En un equipo con VRAM dedicada y expertos residentes en ella, el panorama sería otro y no lo medimos aquí.
¿Subir el tope de memoria siempre ayuda?
En nuestra medición, pasar de 20 a 23 GB redujo la lectura por solicitud de 29,0 a 22,3 GB y subió la mediana de 2,61 a 2,74 tok/s. Es una mejora pequeña en velocidad pero coherente con la lectura de disco. No extrapoles más allá: el sistema operativo y otros procesos también necesitan memoria.
¿Qué cantidad de memoria haría falta para evitar casi toda la lectura?
No lo medimos. El modelo ocupa 111,3 GB y se consulta de forma desigual, así que no hace falta alojarlo entero, pero con 23 GB de caché todavía leemos entre 16 y 22 GB por solicitud. Es razonable esperar una mejora clara con 64 GB, pero eso es una hipótesis hasta que lo midamos.
¿Qué pasa con la calidad de las respuestas?
Todas las configuraciones de la tabla que se marcan como correctas dieron salidas coherentes en la prueba corta de 64 tokens. No es una evaluación de calidad. La evaluación de benchmarks del modelo es independiente de esta prueba de velocidad.
¿Son cifras que se puedan comparar con las de otros equipos?
No directamente. Son una máquina, una pregunta, 64 tokens y tres corridas, con un ±10% de variación. Úsalas para entender qué factor domina, y mide con tu propio script antes de decidir hardware.
Conclusión
En un portátil de 32 GB, un MoE de 180B en Q4 no se ve limitado por la potencia de cálculo sino por cuánto de lo que necesita se queda en la caché de páginas. La evidencia es consistente: los modelos que caben van cuatro veces más rápido, una solicitud relee entre 16 y 29 GB del SSD, más memoria reduce esa lectura y las rutas que no atacan el problema (GPU, borrador especulativo, lectura anticipada) no aportaron. Si vas a ejecutar un MoE grande con mucha menos RAM que el tamaño del archivo, mide primero los bytes leídos del disco por token: ese número te dice si te conviene comprar memoria, reducir pesos o aceptar el techo.
Declaración: este artículo lo publica VIDRAFT sobre su propio modelo y sus propias mediciones. Los datos provienen de una sola máquina, con variación de corrida a corrida de unos ±10%; las tasas de acierto son estimaciones derivadas y se indican como tales.
Top comments (0)