
Más bits donde duele: cuanticé un Qwen3.8-27B a la medida de mi 4070 Ti
El Q4 se pasa de 16 GB y el Q3 pierde calidad. Un parche de llama.cpp reparte los bits capa por capa: medí un Qwen3.8-27B al 94 % de una referencia de 8 bits que sí entra en una 4070 Ti.
Quería correr el mejor Qwen3.8-27B posible en mi RTX 4070 Ti, que tiene 16 GB. El problema es de talla de zapato: el escalón Q4_K_M ocupa 15,66 GB de archivo y al cargarlo con contexto se pasa de los 16 GB, da error de memoria. El de abajo, Q3_K_M, entra con holgura pero regala calidad. No había un punto medio.
Un parche de llama.cpp lo da: en vez de un escalón fijo, un objetivo continuo. Medí que a 15 GB el modelo queda con 94,3 % de acuerdo con la referencia de 8 bits y carga donde el Q4 fijo ni arranca.
Qué es cuantizar, en simple
Un modelo son millones de números, sus pesos. Guardarlos en 16 bits es fiel pero pesado. Cuantizar es guardarlos con menos bits (8, 5, 4, 3) para que el archivo quepa en la GPU. Es la misma idea que pasar una foto de RAW a JPG: ocupa mucho menos y a simple vista se ve casi igual, pero si comprimes de más aparecen los cuadros.
Hasta ahora esa compresión venía en tallas fijas. Q4_K_M significa, más o menos, “cuatro bits y medio por peso, para todo el modelo”. Eliges una talla y rezas para que calce en tu GPU.
Cada peldaño es un valor que se puede guardar. El eje es el mismo; lo que baja es cuántos peldaños caben. Peldaños ilustrativos, no a escala.
El dial: pedir un tamaño, no una talla
El parche agrega dos opciones a llama-quantize: --target-bpw (bits por peso) y --target-size (tamaño de archivo). Le dices “quiero que pese 15 GB” y un algoritmo decide, tensor por tensor, qué tipo de cuantización usar para cumplir ese presupuesto perdiendo la menor calidad posible.
La diferencia con el escalón fijo se ve mejor mirando lo que decidió de verdad. Le pedí 15 GB al Qwen3.8-27B y revisé el reparto que hizo sobre sus 503 tensores:
Con menos presupuesto medio que el escalón fijo comparable (4,77 contra 4,98 bits) y mejor repartido: las capas de arriba se llevan más que las de abajo. Las celdas apagadas son bloques donde ese tensor no existe (el modelo alterna atención y ssm).
El escalón fijo pondría la misma altura en todas las barras. El dial levantó ffn_down y la proyección de valores de la atención a 5,5 bits —son las que más duelen al comprimir—, dejó las consultas y las claves cerca de 5,1, y hundió el resto (ffn_gate, ffn_up, ssm) a 4,25. Y lo hace con menos presupuesto medio que el escalón fijo comparable: 4,77 bits contra 4,98, mejor repartidos.
Para que el algoritmo sepa qué tensor “duele” más necesita una matriz de importancia (imatrix): una medición de qué activaciones pesan más, calculada pasando texto por el modelo. La calibré con un corpus diverso, no solo con el texto con el que después mido, para no hacer trampa conmigo mismo.
Instalación, con los tropiezos
El parche es el PR #15550 de EAddario, todavía sin integrar a la rama principal. Lo compilé del commit 325319b. Que no esté integrado importa: nadie reproduce esto desde el main de llama.cpp, y encontré al menos un fallo de escritura en el camino.
Tres cosas costaron tiempo:
- La matriz de importancia no cabía en RAM. Generarla exige correr el modelo, y el original en 16 bits pesa 54 GB, más de lo que tengo entre RAM y VRAM. La calibré desde la versión
Q8_0(8 bits, 28 GB), que para medir importancia es casi idéntica al original. Lo mismo vale para cuantizar: todas las variantes salen del mismoQ8_0, así que la comparación entre ellas es limpia. - Un
iostream errorque no era del parche. A media cuantización, el proceso moría con “error de flujo de entrada/salida”. Perseguí un fantasma en las capas del MTP durante un rato. Era el disco: se había llenado al 100 %. El archivo de logits que genera la comparación de divergencia ocupaba cientos de GB. Acotarlo lo resolvió. - El tamaño del archivo no es la VRAM. Un GGUF de 15,66 GB no corre en 16 GB. Entre los pesos, la caché de contexto y los buffers de cómputo, se desborda. Hay que medir la VRAM real, no leer el tamaño del archivo.
Lo que medí
Perplexity sobre wikitext-2 y sobre código (fuera del dominio de calibración), divergencia contra el Q8_0 como referencia, VRAM real con contexto de 8k y velocidad. Todo en la misma 4070 Ti, todo con la misma imatrix.
| variante | archivo | PPL wiki | PPL código | acuerdo top con Q8 | ¿entra en 16 GB? | tok/s |
|---|---|---|---|---|---|---|
| Q3_K_M (fijo) | 12,57 GB | 7,186 | 1,949 | 90,4 % | sí | 37,8 |
| dial ≈ Q3 | 12,00 GB | 7,131 | 1,945 | 90,6 % | sí | 42,6 |
| dial · 15 GB | 15,00 GB | 7,037 | 1,908 | 94,3 % | sí | 36,2 |
| Q4_K_M (fijo) | 15,66 GB | 6,953 | 1,906 | — | no carga | — |
| Q5_K_M (fijo) | 18,19 GB | 6,962 | 1,897 | — | no carga | — |
Dos lecturas. A igual tamaño que el Q3_K_M, el dial baja la perplexity (7,131 contra 7,186) y encima va más rápido (42,6 contra 37,8 tokens por segundo). El margen es chico y roza el error de medición, pero la divergencia lo confirma: el dial se queda más cerca del original.
El Q5 mide un pelo peor que el Q4 en wikitext (6,962 contra 6,953): la diferencia cae dentro del error, y de todos modos ninguno de los dos carga en 16 GB.
La lectura que importa es la otra.
El presupuesto de 16 GB
- dial · 12 GB: cabe, y deja sitio para el MTP
- dial · 15 GB: el más fiel que entra
- Q4_K_M (fijo): out of memory: no carga
El único escalón fijo que cargaba con contexto útil era el Q3_K_M. El Q4_K_M, mejor en calidad, se pasa. El dial a 15 GB entra por 683 MiB de margen y coincide en el 94,3 % de los tokens con la referencia de 8 bits, contra el 90,4 % del Q3; su error medio es 3,63 % contra 6,63 %, casi la mitad. Es lo que un escalón fijo no puede darte: la calidad que cabe justo en tu GPU, ni una talla más ni una menos. Paga 1,6 tok/s por los bits extra (36,2 contra 37,8 del Q3): barato por casi cuatro puntos de acuerdo.
Ese Q8_0 de referencia sirve para leer la tabla: 94,3 % de acuerdo significa que, de cada 100 tokens, el modelo a 15 GB elige el mismo que la versión de 8 bits en 94, contra 90 del Q3.
El MTP no entra, y es un dato
El Qwen3.8-27B trae MTP, un borrador interno que propone varios tokens de golpe para acelerar. En otra medición me subió de 42 a 80 tokens por segundo.
El borrador propone, el modelo grande verifica. Casi el doble de rápido (42 → 80 tok/s) y el texto que sale es idéntico.
Quise ponérselo encima al modelo de 15 GB.
Cuándo sí y cuándo no
El dial gana cuando el objetivo es llenar un presupuesto exacto: una GPU cuya talla fija de arriba se pasa por poco. Ahí aporta calidad que el escalón no alcanza y carga donde el escalón se desborda.
No lo usaría si te sobra memoria. Si el Q4 te carga con holgura, ya tienes tu modelo, y encima con margen para el MTP, que en velocidad rinde más que el par de décimas de perplexity que araña el dial. Tampoco si necesitas reproducibilidad estricta hoy: el parche no está en el main de llama.cpp.
Para mi caso (27B en 16 GB, sin margen) es la diferencia entre un modelo mediocre que cabe y uno bueno que también cabe.
Fuentes
- PR #15550,
--target-bpwen llama.cpp, de EAddario. Compilado del commit325319b. - Qwen3.8-27B, licencia Apache-2.0.
- Perplexity y divergencia con
llama-perplexitysobre wikitext-2; los GGUF y la imatrix salieron de la versiónQ8_0del modelo.
Comentarios
Todavía no hay comentarios. El primero es tuyo.