
NVFP4 vs FP8 vs GGUF en una GPU de 16 GB sin Blackwell: NVFP4 genera 2,3 veces más rápido que BF16 y empata en las 1319 preguntas de GSM8K
Medí el mismo modelo de NVIDIA en BF16, NVFP4, MXFP8, FP8 y GGUF Q5_K_M en una RTX 4070 Ti SUPER. NVFP4 genera a 117 tokens por segundo contra 51 de BF16, y FP8 es el más rápido para leer un prompt largo. En las 1319 preguntas de GSM8K los tres formatos de vLLM empatan con BF16 dentro de ±3 puntos, aunque token por token NVFP4 cambia la elección de BF16 una de cada diez veces.
Cuando busco «nvfp4 vs», Google me completa con «fp8», «gguf», «q4_k_m» y «bf16». Con «mxfp8 vs», con «fp8» y con «nvfp4 vs bf16». Es la pregunta que se hace cualquiera con una tarjeta de 16 GB: qué formato bajar. Casi siempre se responde comparando modelos distintos, cuantizados por gente distinta.
El 16 de octubre NVIDIA publicó Nemotron Labs Dory 1.0, un modelo de 4.000 millones de parámetros, en cuatro versiones a la vez: BF16, NVFP4, MXFP8 y GGUF. Es el mismo modelo, cuantizado por su autor. Lo medí en una RTX 4070 Ti SUPER, una Ada sin hardware FP4, en velocidad y en calidad.
Qué medí, y con qué reglas
Antes de la primera corrida dejé escrita una ficha con las variantes, las métricas y la regla de decisión. Dory tiene una arquitectura «looped»: según su config.json, de 72 capas (16 de entrada, 40 recurrentes y 16 de salida, contando atención y MLP por separado; la ficha de NVIDIA las cuenta como bloques de 8, 20 y 8), las 40 del medio se repiten. Lo dejé en dos vueltas, el valor que recomienda NVIDIA, en todas las variantes.
- En vLLM (desde el PR que agrega Dory, versión 0.32.0rc1.dev319): BF16 como referencia, NVFP4 y MXFP8 de NVIDIA, y FP8 que vLLM cuantiza al cargar los pesos BF16 (pesos por tensor, activaciones cuantizadas al vuelo).
- En llama.cpp (desde el commit que cita la ficha de NVIDIA): el GGUF Q5_K_M oficial.
- Velocidad: tokens por segundo generando 256 tokens con lote 1, y leyendo un prompt de 2048 tokens, con un cronómetro externo alrededor de cada llamada completa: la llamada a vLLM y la petición HTTP a llama.cpp. Mediana de 5 corridas después de una de calentamiento.
- Calidad: las mismas 250 preguntas de GSM8K, elegidas con semilla fija y commiteadas antes de correr. Razonamiento activado, decodificación determinista y hasta 4096 tokens por respuesta. Una respuesta que no termina dentro de ese límite cuenta como error.
- Regla: un formato «empata» con BF16 si el intervalo de confianza del 95 % de la diferencia de aciertos cabe entero dentro de ±3 puntos, y «pierde» si cae entero bajo −3. Si no, «no concluyente».
- Ampliación: como las 250 preguntas no alcanzaron, registré una tercera enmienda antes de correrla: GSM8K completo con tope de 8192 tokens y una comparación token por token con BF16 sobre textos fijos en inglés y en español.
Lo que se rompió antes de medir
Ninguno de los dos motores trae Dory todavía, y armarlos en un nodo NixOS tuvo tropiezos que vale la pena contar.
- El convertidor de llama.cpp no acepta el checkpoint publicado. La rama que cita NVIDIA exige claves de un formato interno (
dory_format_version,ngpt,hybrid_layer_pattern) que elconfig.jsonde Hugging Face no trae. No las inventé. El PR dice haber probado el formato bf16, además de q4_k_m y q5_k_m, y el convertidor exige claves que elconfig.jsonpúblico no trae: hay una incompatibilidad entre esa revisión del convertidor y el checkpoint publicado. Por eso en llama.cpp solo medí el Q5_K_M oficial: no tengo F16, Q8_0 ni Q4_K_M propios, y el GGUF se compara contra el BF16 de vLLM, con el motor cambiando al mismo tiempo. - vLLM no cargaba la configuración. La imagen trae transformers 5.17, que al normalizar los parámetros de RoPE recorre también las capas
mlpde Dory y falla conKeyError: 'mlp'. Con transformers 5.20.1 carga bien. - Tres errores míos en el arnés, que corregí antes de publicar. Con la caché de prefijos encendida, la corrida de calentamiento dejaba el prompt guardado y la «lectura» daba entre 78.000 y 135.000 tokens por segundo: repetí la velocidad con la caché apagada. Y cuando una respuesta se cortaba en 4096 tokens, vLLM devolvía el razonamiento a medio camino y mi extractor tomaba su último número como respuesta. Así contaba hasta 40 aciertos por suerte por variante, mientras llama.cpp devolvía la respuesta vacía. Ahora las dos ramas cuentan esos casos como error, como decía la regla desde el principio. El tercero lo encontró la auditoría externa: en vLLM cronometraba la llamada completa y en llama.cpp usaba la tasa interna del servidor, que deja fuera la lectura del prompt. Repetí llama.cpp con un cronómetro externo alrededor de cada petición, como en vLLM: el GGUF genera a 58,8 tokens por segundo y no a 62,2, y lee a 1.710.
Velocidad: el mismo modelo, cinco formatos
Generar 256 tokens (lote 1)
- NVFP4vLLM116,8 tok/s · 2,19 s
- MXFP8vLLM81,4 tok/s · 3,14 s
- FP8vLLM78,5 tok/s · 3,26 s
- GGUF Q5_K_Mllama.cpp58,8 tok/s · 4,35 s
- BF16vLLM51,0 tok/s · 5,02 s
Leer un prompt de 2048 tokens
- FP8vLLM10.118 tok/s · 0,20 s
- BF16vLLM7.003 tok/s · 0,29 s
- NVFP4vLLM6.978 tok/s · 0,29 s
- MXFP8vLLM6.061 tok/s · 0,34 s
- GGUF Q5_K_Mllama.cpp1.710 tok/s · 1,20 s
NVFP4 3,28 · MXFP8 4,76 · FP8 4,67 · GGUF Q5_K_M 3,42 · BF16 7,69 GiB de pesos al cargarlos en vLLM (GGUF: tamaño del archivo)
Generando de a un token, el orden de los formatos de vLLM sigue casi exacto al tamaño de los pesos. Con lote 1, cada token nuevo obliga a leer todos los pesos desde la memoria de la tarjeta, y el resultado es consistente con que menos bytes den más velocidad. NVFP4 ocupa 2,3 veces menos que BF16 (3,28 contra 7,69 GiB) y generó 2,3 veces más rápido. FP8 y MXFP8 ocupan 1,6 veces menos y generaron 1,5 y 1,6 veces más rápido.
Leyendo un prompt largo el orden cambia. Ahí la tarjeta hace cuentas, no espera a la memoria, y lo que importa es en qué precisión las hace.
Ada tiene tensor cores FP8 e INT8; no tiene FP4 ni escalado por bloques MX, así que NVFP4 y MXFP8 se expanden al vuelo. Fila del GGUF: llama.cpp tiene una ruta INT8 (activaciones Q8_1) y otra FP16; no registré cuál corrió (s/r). bits: pesos / activaciones.
De los formatos cuantizados que corrí en vLLM, FP8 es el único cuyas multiplicaciones Ada hace en 8 bits. vLLM lo corre con pesos y activaciones en 8 bits, y leyó el prompt a 10.118 tokens por segundo, contra 7.003 de BF16. NVFP4 y MXFP8 pasan por kernels Marlin, que guardan los pesos chicos y los expanden a 16 bits dentro del kernel: la cuenta se hace igual que en BF16. Es consistente con que NVFP4 leyera a 6.978 y MXFP8 a 6.061, sin ganarle a BF16. Es un kernel de la misma familia Marlin que encontré en el código de TensorRT-LLM en el post anterior, y que ahí nunca llegó a correr.
llama.cpp con el Q5_K_M generó a 58,8 tokens por segundo y leyó a 1.710, con pesos de tamaño parecido a NVFP4. Esa comparación no me deja separar el motor del formato. Su backend CUDA tiene una ruta que cuantiza las activaciones a 8 bits y multiplica los k-quants con productos enteros, y otra que los expande a 16 bits; no registré cuál usó en cada operación. Además corrió con flash attention apagada, como en los comandos de la ficha de NVIDIA, con su propio tokenizador y con Dory recién agregado en una rama. Cualquiera de esas cosas puede explicar la diferencia.
Calidad: ¿alguno se volvió más tonto?
La medí de tres maneras. La primera, la que fijé al principio, salió no concluyente. Las otras dos las registré en una enmienda a la ficha antes de correrlas, para tener una respuesta con más precisión.
Primera pasada: 250 preguntas
Las cuatro comparaciones quedaron no concluyentes. BF16 acertó 202 de 250, FP8 199, NVFP4 208, MXFP8 210 y el GGUF 212. Para NVFP4, MXFP8 y el GGUF, el intervalo de confianza descarta que el formato pierda más de 3 puntos. Para FP8 no: su intervalo va de −6,0 a +3,6. Con 250 preguntas se detectan diferencias de unos 4 o 5 puntos, no de 1.
Lo que más pesaba en ese puntaje era otra cosa: entre 37 y 50 respuestas por variante no terminaron de razonar dentro de los 4096 tokens. Entre las que sí terminaron, todos los formatos acertaron casi todo: BF16 202 de 204, FP8 199 de 200, MXFP8 210 de 211, NVFP4 208 de 210 y el GGUF 212 de 213.
GSM8K completo: 1319 preguntas
Corrí las 1319 preguntas del conjunto de prueba con el tope subido a 8192 tokens, en los cuatro formatos de vLLM. El GGUF quedó afuera: corre en otro motor y no tiene referencia propia.
Con el conjunto completo, los tres formatos empatan con BF16 según la misma regla. BF16 acertó 1249, MXFP8 1250, FP8 1241 y NVFP4 1238. La diferencia de NVFP4 es −0,8 puntos, con un intervalo de −1,8 a +0,2; la de FP8, −0,6 (de −1,5 a +0,3); la de MXFP8, +0,1 (de −0,8 a +0,9). Los tres intervalos caben enteros dentro de ±3 puntos. Con el tope nuevo se cortaron entre 32 y 44 respuestas de 1319, así que el corte dejó de dominar el puntaje: es la mitad de los errores, contra casi todos en la primera pasada. Lo que separa a FP8 de BF16 son nueve respuestas cortadas más, no respuestas erradas: FP8 razona más largo (mediana de 2386 tokens contra 1995).
Token por token
GSM8K mide si la respuesta final es correcta. La forma en que se compara una cuantización con su original es más fina: darle el mismo texto y mirar, en cada posición, cuánto cambia la distribución del token siguiente. Lo hice con 32.752 posiciones por idioma. En inglés, 16 tramos repartidos a lo largo de todo el texto de prueba de wikitext-2; en español, el comienzo de diez artículos de Wikipedia, unos 3.300 tokens de cada uno, con su revisión fijada. Los bloques de 2048 tokens que lee el modelo no coinciden con esos tramos: varios cruzan de un tramo al siguiente, igual para los cuatro formatos.
mismo token siguiente que BF16
80 % → 100 %
divergencia KL contra BF16 (estimada sobre su top 20)
0 → 0,051
Estimación: suma los 20 tokens más probables de BF16 (cerca del 90 % de su probabilidad); a un token que no está entre los 20 del formato le asigno la probabilidad de la vigésima del formato. Sirve para comparar formatos, no es una cota de la KL completa.
■ inglés · ■ español
perplejidad del texto: referencia BF16 10,01 / 8,33 · MXFP8 10,08 / 8,37 · FP8 10,08 / 8,39 · NVFP4 10,00 / 8,42
La perplejidad casi no se mueve: 10,01 en inglés y 8,33 en español para BF16, y entre 10,00 y 10,08, y entre 8,37 y 8,42, para los demás. En inglés, NVFP4 incluso queda apenas por debajo de BF16. Mirando solo ese número, los formatos serían iguales.
Token por token no lo son. NVFP4 elige el mismo token siguiente que BF16 en el 89,3 % de las posiciones en inglés y el 90,3 % en español: cambia la elección más o menos una de cada diez veces. FP8 coincide en el 94,1 y el 94,6 %, y MXFP8 en el 95,4 y el 95,9 %. Mi estimación de la divergencia KL para NVFP4 es unas 3 veces la de FP8, y 5 veces (inglés) y 7 veces (español) la de MXFP8. Era la hipótesis que dejé escrita antes de correr: NVFP4 se aleja más. Aun así, en GSM8K completo esa distancia no se tradujo en una caída que se pueda medir. MXFP8 quedó más cerca de BF16 que FP8, lo que es consistente con que una escala por cada bloque de 32 pesos se ajuste mejor que una sola escala por matriz.
Al GGUF le medí la perplejidad con llama-perplexity: 8,20 en inglés y 7,92 en español. Ese número no se compara con los de vLLM, porque llama.cpp usa su propio tokenizador y solo puntúa la segunda mitad de cada bloque.
Amenazas a la validez
- Es un solo modelo, de 4B, con una arquitectura poco común. El orden de velocidad puede no repetirse en un modelo grande, donde la lectura del prompt pesa más.
- Los dos motores corren desde ramas sin fusionar. Los números pueden cambiar cuando lleguen a una versión publicada.
- La ficha de NVFP4 lista como hardware soportado Hopper y Blackwell, no Ada: en esta tarjeta corre por una ruta que NVIDIA no declara.
- El GGUF se compara con otro motor. No pude medir un Q4_K_M ni una referencia F16 en llama.cpp, y llama.cpp corrió con flash attention apagada, 8 peticiones en paralelo para GSM8K (vLLM las procesó en lotes de hasta 64) y su propio tokenizador.
- vLLM avisó, en las cuatro variantes, que el tokenizador se carga con una expresión regular incorrecta. Afecta a las cuatro por igual, pero no al GGUF.
- FP8 no es un archivo de NVIDIA: es la cuantización por tensor que hace vLLM al cargar los pesos BF16.
- Me desvié de la ficha en dos cosas: en vez de la VRAM pico con nvidia-smi publico la memoria de pesos que informa vLLM al cargar (vLLM reserva la tarjeta de antemano), y el acuerdo pregunta a pregunta con BF16 lo dejo en los datos (215, 222, 218 y 224 de 250 en FP8, MXFP8, NVFP4 y el GGUF).
- GSM8K mide una sola habilidad, en inglés. En la primera pasada el tope de 4096 tokens cortó entre 37 y 50 de las 250 respuestas; con 8192 cortó entre 32 y 44 de 1319.
- La KL es una estimación: solo suma las 20 opciones más probables de BF16, que dejan afuera cerca del 10 % de su probabilidad, y cuando una de ellas no está entre las 20 de la variante uso la probabilidad de la vigésima de la variante, porque de cada formato guardé solo sus 20 opciones más probables. No es una cota de la KL completa; sirve para comparar formatos entre sí, y el acuerdo en el token siguiente, que no depende de esa aproximación, da el mismo orden. La comparación token por token no incluye al GGUF.
- La primera corrida token por token, por un error mío, midió solo el comienzo de cada archivo: en español, únicamente el artículo «Chile», y una de las diez revisiones era una redirección vacía. Lo detectó la auditoría; registré una cuarta enmienda con los textos repartidos y volví a medir. Las cifras de aquí son las de esa segunda corrida; la primera daba el mismo orden en el acuerdo del token siguiente y en la KL estimada, pero no en la perplejidad, que cambia de orden entre los formatos de una corrida a otra. Queda en el repositorio.
- GSM8K completo y la comparación token por token los decidí después de ver que la primera pasada no era concluyente. Las reglas las dejé escritas antes de correrlas, pero no estaban en el plan original.
- Medí con lote 1. Con muchas peticiones a la vez, la lectura del prompt y el cómputo ganan peso, y supongo que FP8 ganaría terreno, pero no lo medí.
- La memoria que publico es la que vLLM informa al cargar los pesos (y el tamaño del archivo en el GGUF), no el consumo total durante la inferencia, que incluye la caché de contexto.
Veredicto
En una tarjeta Ada de 16 GB, con este modelo, NVFP4 es el formato para conversar: genera 2,3 veces más rápido que BF16, sus pesos ocupan menos de la mitad al cargarlos y empató con BF16 en las 1319 preguntas de GSM8K. Token por token se aleja más que los formatos de 8 bits; si lo que importa es reproducir a BF16 lo más fiel posible, MXFP8 es el más cercano. FP8 es el formato para leer documentos largos, porque, de los formatos que corrí en vLLM, es el único que hace las cuentas en 8 bits en el hardware de Ada; para atender muchas peticiones a la vez probablemente también, pero eso no lo medí. MXFP8 generó apenas más rápido que FP8 y leyó bastante más lento, así que en esta tarjeta su ventaja es la fidelidad, no la velocidad. Y el GGUF fue más lento que los tres formatos cuantizados de vLLM, aunque esa comparación no separa el motor del formato.
Preguntas frecuentes
¿NVFP4 vs FP8: cuál es más rápido en una GPU que no es Blackwell?
Depende de qué mides. Generando de a un token, NVFP4 fue el más rápido: 116,8 tokens por segundo contra 78,5 de FP8 y 51,0 de BF16, en una RTX 4070 Ti SUPER con vLLM. Leyendo un prompt de 2048 tokens ganó FP8, con 10.118 tokens por segundo contra 6.978 de NVFP4, porque Ada tiene tensor cores FP8 y vLLM los usa con pesos y activaciones en 8 bits. NVFP4 en Ada solo achica los pesos.
¿NVFP4 pierde calidad contra BF16?
En GSM8K, dentro del margen que fijé, no. En las 1319 preguntas BF16 acertó 1249 y NVFP4 1238: −0,8 puntos, con un intervalo de confianza del 95 % de −1,8 a +0,2, dentro del margen de ±3 que fijé antes de medir. Token por token sí se aleja más que los formatos de 8 bits: elige el mismo token siguiente que BF16 en el 89-90 % de las posiciones, contra el 94-96 % de FP8 y MXFP8, con la perplejidad prácticamente igual.
¿MXFP8 vs FP8: hay diferencia?
En velocidad de generación casi no: 81,4 contra 78,5 tokens por segundo. Leyendo un prompt largo, FP8 fue mucho más rápido (10.118 contra 6.061 tokens por segundo), porque en Ada vLLM corre FP8 en los tensor cores FP8 y MXFP8 con un kernel que solo achica los pesos. En calidad, MXFP8 quedó más cerca de BF16: coincide en el token siguiente el 95-96 % de las veces contra el 94-95 % de FP8, y en las 1319 preguntas de GSM8K los dos empatan con BF16 (1250 y 1241 contra 1249).
¿NVFP4 vs GGUF: cuál conviene?
Con este modelo, en mi tarjeta, NVFP4 en vLLM generó a 116,8 tokens por segundo y el GGUF Q5_K_M oficial en llama.cpp a 58,8, con pesos de tamaño parecido (3,28 contra 3,42 GiB). El GGUF acertó 212 de 250 en GSM8K y NVFP4 208. Ojo: además del formato cambia el motor, y no pude hacer un Q4_K_M ni un F16 propios porque el convertidor de llama.cpp no acepta el checkpoint publicado.
¿Cuánta memoria ocupan los pesos en cada formato?
Según vLLM al cargar: 7,69 GiB en BF16, 4,67 en FP8, 4,76 en MXFP8 y 3,28 en NVFP4. El GGUF Q5_K_M pesa 3,42 GiB en disco. La velocidad de generación creció casi en la misma proporción en que se achicaron los pesos.
¿Puedo reproducirlo?
Sí. La ficha con las reglas fijadas antes de medir, los Dockerfiles de los dos motores, los scripts de medición y de puntaje, las preguntas (250 y 1319), cada respuesta, los textos fijos y las probabilidades token por token están en el repositorio y en un gist enlazado en las fuentes.
Fuentes
- Las fichas de NVIDIA: Dory 1.0 4B BF16, NVFP4, MXFP8 y GGUF, con los comandos de vLLM y llama.cpp y la recomendación de dos vueltas.
- El PR de Dory en vLLM y el de llama.cpp.
- Introducing NVFP4, de NVIDIA, y la especificación OCP Microscaling, que define MXFP8.
- GSM8K, el conjunto de problemas de matemática de primaria.
- wikitext-2 y los artículos de Wikipedia en español (CC BY-SA 4.0) usados como textos fijos; las revisiones exactas están en el gist.
- La ficha, los Dockerfiles, los scripts, las preguntas, cada respuesta y los tiempos.
Medido el 17 de octubre de 2026 en una RTX 4070 Ti SUPER de 16 GB (Ada, sm_89, driver 595.71.05). vLLM 0.32.0rc1.dev319+g55076950b desde el PR #60149 con transformers 5.20.1; llama.cpp en el commit 98a74c9 compilado para sm_89 con CUDA 12.8. Dory 1.0 4B con dos vueltas y caché por vuelta. GSM8K: 250 preguntas del split de prueba (semilla 17) con tope de 4096 tokens, y las 1319 con tope de 8192. Fidelidad: 16 bloques de 2048 tokens por idioma (16 tramos repartidos en wikitext-2-raw-v1 test, y unos 3.300 tokens del comienzo de cada uno de diez artículos de es.wikipedia).
BOLETÍN · MEDIDO
Una medición nueva, cuando la hay.
Cada post con sus números, sus fallos y los scripts para repetirlo. Entre uno y tres correos al mes. Te das de baja con un clic.
LO ÚLTIMO QUE LLEGÓ
- NVFP4 vs FP8 vs GGUF en 16 GB2,3× más rápido
- Qwen-Image NVFP4 en Ada19/21 con menos grano
- Kandinsky 6 en 16 GBWER 0,15 en español
Uso Listmonk en mi propio servidor; tu correo no sale de ahí. Confirmas por correo antes de recibir nada.
Comentarios
Todavía no hay comentarios. El primero es tuyo.