
ExLlamaV3 contra llama.cpp con Qwen3.8-27B en 16 GB: decodifica más rápido, pero no siempre gana
Les di a los dos motores exactamente los mismos tokens de entrada dentro de un contenedor con límites de pod. ExLlamaV3 decodifica más rápido y procesó 131 mil tokens de contexto; en archivos de 13 GB pierde menos calidad, en los de 15 GB pierde contra el GGUF. En un turno agéntico, el prefill se come casi toda la ventaja.
Hace un mes medí si Qwen3.8-27B entraba en mi RTX 4070 Ti SUPER de 16 GB, y después cuanticé el modelo a la medida de la tarjeta con llama.cpp. Siempre con el mismo motor. El 13 de septiembre salió ExLlamaV3 1.5.0 y su tabla de rendimiento empieza en tarjetas de 24 GB. La mía no aparece.
Así que les di a los dos motores exactamente los mismos tokens de entrada, dentro del mismo contenedor. ExLlamaV3 decodificó más rápido en todo lo que medí y procesó 131 mil tokens de contexto, donde llama.cpp llegó a 64 mil, el máximo que probé. En calidad gana a 3 bits y pierde a 15 GB. Y en un turno agéntico real, el prefill se come casi toda la ventaja de velocidad.
Qué comparé, y por qué es justo
ExLlamaV3 usa su propio formato de cuantización, EXL3, y no carga GGUF. No se puede medir “el mismo archivo en dos motores”: lo más cercano es elegir archivos del mismo tamaño y ver qué hace cada motor con su formato.
- ExLlamaV3 1.5.0 con el EXL3 de 3.00 bits por peso que publica turboderp: 12,9 GiB en disco, con la cabeza MTP incluida. Para la comparación de calidad a mayor tamaño, también el de 3.50 bits (14,3 GiB).
- llama.cpp con un
Q3_K_Mde 12,6 GiB, cuantizado con la misma matriz de importancia del post anterior, y el GGUF de 15,0 GiB que ese post cuantizó a medida. Para el MTP usa un borrador aparte de 1,3 GB.
Los dos reciben los mismos ids de tokens. Tomé la batería de prompts que trae ExLlamaV3 para medir decodificación especulativa (código, agentes con herramientas, traducción, texto creativo), la tokenicé una vez y le mandé esos mismos números a llama-server. Con 16 mil tokens de contexto entran 15 de los 22 prompts; los 7 agénticos más largos quedan fuera en los dos motores.
Misma entrada, mismos límites, misma tarjeta. Lo que cambia es el motor, su formato de cuantización y la forma de su MTP.
Todo lo de velocidad corrió dentro de un contenedor con límites de pod: 8 CPU, 24 GB de RAM y la GPU entregada por CDI. Repetí llama.cpp también directo en el host y la diferencia no pasó de 0,6 %: la carga vive en la GPU. La batería de prompts corrió tres veces por motor y ninguna categoría varió más de 0,3 % entre repeticiones, salvo una que explico más abajo. El turno agéntico, cinco veces por configuración. El barrido de contexto es una corrida por tamaño en ExLlamaV3 y dos repeticiones internas de llama-bench en llama.cpp. La calidad y la VRAM pico de ExLlamaV3 con MTP las medí en el host: qbench es determinista y la memoria de la GPU no depende de los límites del contenedor.
Instalación, con los tropiezos
La instalación costó más que la medición, y casi nada fue culpa de ExLlamaV3.
- Al instalar torch desde el índice de PyTorch,
uvcortó dos veces conoperation timed outal traer paquetes de NVIDIA, tanto para CUDA 13.2 como para 12.8. No encontré la causa: más tarde, el mismo paquete de 218 MB bajó completo a 11 MiB/s concurl. Terminé instalando torch 2.11.0 desde PyPI, con un tiempo de espera de red más largo. - La release trae wheels por combinación de CUDA y torch. Mi script armaba el nombre con variables que quedaron vacías y generó
exllamav3-1.5.0+.torch.0, un nombre queuvrechaza. Con el nombre completo,+cu132.torch2.11.0, instaló sin problema. exllamav3no expone__version__. Mi verificación falló aunque todo estaba bien instalado.- Para medir calidad con qbench, la herramienta de ExLlamaV3, hizo falta compilar
llama-cpp-pythoncon CUDA (352 s, no hay wheels recientes), instalartransformersygguf, y corregir el nombre del dataset de wikitext, que la versión nueva dehuggingface_hubya no acepta sin el prefijoSalesforce/. - Dentro del contenedor aparecieron tres más.
llama-serverno encontrabalibcudart.so.13, que el host resuelve porldconfig. torch se caía buscando el nombre de un usuario que el contenedor no tiene. Y ExLlamaV3 no arrancaba en la imagen pelada de Fedora 43 porque Triton necesita un compilador de C para armar sus kernels: le agreguégcc.
La herramienta de medición también se mide
Dentro del contenedor, eval/spec_decode.py dio en dos categorías velocidades que no se sostenían, igual en tres procesos seguidos: 51 tokens por segundo con MTP en un prompt agéntico que el host resolvía a 89 con los mismos tokens, y 69,8 en repetición trivial, que el runner de abajo midió en 115,6. Corriendo ese prompt solo, el contenedor bajó aún más: 18,5, y en otras corridas hasta 14,4. No lo cambiaron los límites de CPU o memoria, ni /dev/shm, ni la librería de cuBLAS, ni la salida de texto, ni cambiar la caché de Triton. No encontré la causa. Lo que sí vi: dentro de un mismo proceso, la primera pasada de ese prompt es lenta y las siguientes no. Por eso la batería de ExLlamaV3 de abajo sale de un runner mío que repite cada prompt tres veces en el mismo proceso: la primera repetición dio 51, las otras dos 90,9.
Velocidad: los mismos tokens, dos motores
Sin decodificación especulativa, ExLlamaV3 generó entre 40,7 y 43,1 tokens por segundo según la categoría, y llama.cpp entre 36,0 y 38,0. En un prefill de 4 mil tokens quedaron casi iguales: unos 1 490 contra 1 530 tokens por segundo.
Con MTP, los dos con ventana de 3 tokens:
- ExLlamaV3 · código103,1
- llama.cpp · código79,5
- ExLlamaV3 · texto creativo75,2
- llama.cpp · texto creativo56,2
- ExLlamaV3 · traducción75,2
- llama.cpp · traducción60,2
- ExLlamaV3 · repetición trivial115,6
- llama.cpp · repetición trivial89,8
RTX 4070 Ti SUPER en contenedor de 8 CPU y 24 GB. Prompts de eval/spec_decode.py de ExLlamaV3 1.5.0, mismos ids de tokens en los dos motores, greedy, hasta 1024 tokens nuevos. Tokens generados sobre tiempo de generación, mediana de tres repeticiones.
La cabeza MTP propone, el modelo verifica. Poco más del doble de rápido (41 → 90.9 tok/s) y el texto que sale es idéntico.
ExLlamaV3 ganó las ocho categorías. Y no es porque su borrador acierte más: llama.cpp aceptó el 91 % de lo que propuso en el prompt agéntico de curl y ExLlamaV3 el 78 %; en traducción con razonamiento, 71 % contra 57 %. ExLlamaV3 va más rápido aun acertando menos. Las cuentas lo explican: en curl, llama.cpp saca 3,7 tokens por ronda de verificación y tarda unos 45 ms en cada una; ExLlamaV3 saca 3,3 tokens, pero cada ronda le toma unos 37 ms. Las rondas las derivo suponiendo tres tokens propuestos por ronda, que es la ventana configurada.
llama.cpp acepta más por ronda, pero cada ronda le toma casi 9 ms más. En el mismo medio segundo ExLlamaV3 alcanza a meter dos rondas más, y de ahí sale su ventaja. Las rondas y los milisegundos por ronda se derivan de los contadores, suponiendo tres tokens propuestos por ronda.
Una caché de Triton vacía arruina la medición
ExLlamaV3 compila kernels de Triton cuando no los tiene en caché, y esa compilación cae dentro del tiempo medido. En el host, con la caché de Triton vacía, el prompt de curl con MTP dio 38 tokens por segundo; con la caché llena, 89.
A velocidad real. Misma respuesta, misma aceptación del borrador.
A velocidad real. Contenedor de 8 CPU y 24 GB.
Compilar kernels o una primera pasada lenta pueden duplicar el tiempo de la misma respuesta. Por eso las cifras de la batería son la mediana de tres repeticiones.
Otro detalle raro: con cachés de Triton distintas cambió el texto generado, lo justo para mover la aceptación del borrador de 2,34 a 2,46 tokens por ronda. Mi hipótesis es que el autoajuste eligió kernels distintos, pero no lo comprobé. Por eso las cifras de la batería son la mediana de tres repeticiones: con una primera pasada lenta, como la de 51 en curl, la mediana cae en una de las dos rápidas. Con tres muestras no es una medida de variabilidad, solo evita que una pasada anómala defina el número.
El turno agéntico: donde la ventaja se encoge
Los prompts agénticos traen 11 a 14 mil tokens de historial y herramientas, y la respuesta es una llamada de 60 a 97 tokens. Ahí manda el prefill, no la decodificación.
Al doble de velocidad. Contenedor de 8 CPU y 24 GB.
Al doble de velocidad. Contenedor de 8 CPU y 24 GB.
La decodificación se multiplica, pero el turno completo baja apenas entre 6 y 8 %: casi todo el tiempo se va en leer el contexto.
En ese prompt, llama.cpp tardó 7,51 s en llegar al primer token y ExLlamaV3 7,87 s. En el de curl, con 13 703 tokens, 9,44 contra 10,11 s. Leyendo contexto largo, llama.cpp es más rápido. ExLlamaV3 recupera terreno escribiendo: con ventana 4 decodificó a 117,9 tokens por segundo contra 85,9 de llama.cpp. En el turno completo tarda menos en el prompt de código (8,62 contra 9,29 s) y lo mismo en el de curl (11,33 contra 11,34 s). Ojo con leerlo como empate exacto: las respuestas no tienen el mismo largo. En curl, ExLlamaV3 escribió 97 tokens y llama.cpp 81, así que en el mismo tiempo produjo un 20 % más; en código escribió 60 contra 64.
Calidad: cuánto se aleja cada uno del modelo de 8 bits
Medí la divergencia KL contra un Q8_0 del mismo modelo con qbench, sobre 16 bloques de 512 tokens de wikitext. Es una muestra chica, 8 192 tokens, y la referencia es de 8 bits, no el modelo original.
En archivos de unos 13 GB:
- ExLlamaV3 3.00 bpw (12,9 GiB): divergencia media 0,0435, mediana 0,0164.
- llama.cpp Q3_K_M (12,6 GiB): divergencia media 0,0554, mediana 0,0238.
En archivos de unos 15 GB:
- ExLlamaV3 3.50 bpw (14,3 GiB): divergencia media 0,0234, mediana 0,0089.
- llama.cpp a medida de 15 GB (15,0 GiB): divergencia media 0,0159, mediana 0,0075.
- ExLlamaV3 3.00 bpw · 12,9 GiB · media 0,0435
- llama.cpp Q3_K_M · 12,6 GiB · media 0,0554
confianza de la referencia
- ExLlamaV3 3.50 bpw · 14,3 GiB · media 0,0234
- llama.cpp a medida 15 GB · 15,0 GiB · media 0,0159
confianza de la referencia
A 13 GB ExLlamaV3 queda más cerca en cuatro de cinco tramos; solo en los tokens casi seguros el GGUF lo supera en la media. A 15 GB el GGUF a medida gana todos los tramos menos el último. Son 8 192 tokens de wikitext: una muestra chica.
La comparación está igualada por tamaño de archivo, no por bits por peso. En archivos de 13 GB, ExLlamaV3 queda más cerca de la referencia que el Q3_K_M en todos los tramos de confianza, salvo en la media de los tokens que el modelo de 8 bits predice con más de 95 % de confianza. En archivos de 15 GB se invierte: el GGUF cuantizado a medida gana, con 700 MiB más de disco. Con 15 GiB de modelo más el borrador de 1,3 GB, la cuenta ya no deja sitio para el MTP en 16 GB, y en mis pruebas no pudo crear un contexto de 4 mil tokens con llama-bench; el post anterior lo cargó con 8 mil desde llama-cli.
Hay un detalle de conteo que parece error y no lo es. En capas, el EXL3 de 3 bits usa 3,02 bits por peso y el Q3_K_M 3,84, pero el archivo EXL3 pesa más. Una parte es la cabeza MTP que trae incluida; el resto no lo desglosé.
Por qué la perplexity no calza con el post anterior
qbench dio 9,86 de perplexity para el Q3_K_M. En el post de cuantización publiqué 7,19 para el mismo archivo. No se contradicen: son herramientas distintas. Volví a medirlo con llama-perplexity, la del post anterior, y dio 7,1856. qbench cuenta la ventana completa de 512 tokens sobre 16 bloques, y llama-perplexity solo la segunda mitad de cada ventana sobre todo wikitext.
El presupuesto de 16 GB
- ExLlamaV3 + MTP · 16k: cabeza MTP incluida
- llama.cpp + MTP · 16k: borrador aparte de 1,3 GB
Con MTP, llama.cpp roza el techo: a 32 mil tokens de contexto ya no cupo. ExLlamaV3 deja unos 2 GB libres de los 15 945 MiB que la tarjeta reporta como utilizables.
Sin MTP, la pregunta es cuánto contexto cabe:
- ExLlamaV3 con caché a 4 bits procesó un prefill de 131 072 tokens, y con 130 816 tokens de contexto todavía generaba 31,4 tokens por segundo. A 32 mil daba 39,4. Con caché fp16 cargó 32 mil (37,8 tokens por segundo), pero no 64 mil.
- llama.cpp con caché q8_0 llegó a 64 mil tokens, el máximo que probé (28,9 tokens por segundo). Con caché f16 corrió 32 mil (34,0) y no pudo crear un contexto de 64 mil.
- ExLlamaV3 · caché Q4:43,5 → 31,4
- ExLlamaV3 · caché fp16:43,6 → 37,8 · 66k no carga
- llama.cpp · caché q8_0:37,9 → 28,9 · sin probar más allá 66k
- llama.cpp · caché f16:38,3 → 34,0 · 66k no carga
Con la caché cuantizada a 4 bits, ExLlamaV3 todavía decodifica 31,4 tokens por segundo a 131k. llama.cpp con caché q8_0 llega a 64k, el máximo que probé. Las dos cachés fp16 y f16 se detienen en 32k.
Mi opinión
Para una sola GPU NVIDIA de 16 GB, ExLlamaV3 es hoy lo más rápido que medí para este modelo: decodifica más rápido en todas las categorías, con y sin MTP, y procesó 131 mil tokens de contexto. No lo diría de todo uso. Si el trabajo es un agente que relee su historial en cada turno, con 10 mil tokens por vuelta, la diferencia en tiempo real es chica, y llama.cpp lee contexto largo más rápido. Y si lo que buscas es la máxima calidad que cabe en la tarjeta sin MTP, el GGUF cuantizado a medida de 15 GB se aleja menos del modelo de 8 bits que el EXL3 de tamaño parecido.
Tampoco es una comparación de motores puros. Cambian a la vez el motor, el formato de cuantización y la forma del MTP. Lo que mido es qué obtienes al elegir uno u otro con el mismo presupuesto de disco.
Cuándo sí y cuándo no
- Sí: GPU NVIDIA en Linux o Windows, respuestas largas (código, traducción, redacción), MTP en 16 GB con margen.
- No: sin GPU NVIDIA, porque ExLlamaV3 necesita CUDA (su descarga a CPU es solo para los expertos de modelos MoE); agentes donde domina el prefill; cuando quieres la mejor calidad posible a 15 GB sin MTP; cuando necesitas el ecosistema de GGUF (Ollama, LM Studio, un archivo que corre en cualquier lado).
Fuentes
- ExLlamaV3 v1.5.0, notas de versión, 13 de septiembre de 2026.
- turboderp/Qwen3.8-27B-exl3, rama 3.00bpw.
- eval/spec_decode.py y eval/qbench.py de ExLlamaV3 v1.5.0.
- llama.cpp, commit 458681e, el
llama-serverusado en la batería. - llama.cpp, PR #15550, el dial
--target-bpw, compilado del commit 325319b. - llama-cpp-python v0.3.35.
- Qwen/Qwen3.8-27B.
Comentarios
Todavía no hay comentarios. El primero es tuyo.