Abaratar la inferencia: KV cache, batching y cuantización
El mismo modelo responde en 8,8 s y 78,9 s con salida idéntica. Luego INT4, medido de tres formas, no afirmado.
En esta página
El mismo modelo, en la misma máquina, respondiendo a la misma pregunta con los mismos 48 tokens. Las dos salidas son idénticas token por token: comprobado, no supuesto.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)Cambió un argumento: use_cache=False. Nada del modelo, el prompt, el muestreo ni la aritmética es distinto, y la segunda ejecución no es más precisa por la molestia. Es nueve veces más lenta para nada.
Esa es la forma de este capítulo. Todo lo que contiene —la cache, el batch, los pesos cuantizados— es un intento de dejar de pagar por trabajo que no cambia la respuesta, o de averiguar cuánto cuesta una respuesta más barata. El capítulo 10 estableció la lista de precios del entrenamiento. Esta es la lista de precios del lado por el que pagas siempre: un modelo desplegado gasta aproximadamente FLOPs por cada token que emite, en cada solicitud, durante el resto de su vida.
Adónde se fue el tiempo de la segunda ejecución
Enlace a la sección: Adónde se fue el tiempo de la segunda ejecuciónPara generar un token, un transformer solo decoder toma toda la secuencia hasta el momento, la pasa por cada capa y lee la distribución de probabilidad de la última posición. Luego añade el token elegido y vuelve a hacerlo. Esa descripción es correcta, y es lo que hace la ejecución lenta.
También es enormemente derrochadora, y la razón es la máscara causal del capítulo 9. Los vectores key y value de la posición 7 se calculan a partir de la entrada de la posición 7 y de las posiciones anteriores. Cuando llega la posición 8, la posición 7 no puede verla —eso es lo que significa causal—, así que los key y value de la posición 7 son exactamente los mismos números que antes. La ejecución lenta los recalcula de todos modos, en cada paso.
Así que guárdalos. Ese almacén es la key-value cache, la optimización más importante en el servicio de modelos de lenguaje:
out = model(prompt_ids, use_cache=True) # prefill: the whole prompt
past = out.past_key_values
nxt = out.logits[:, -1].argmax(-1, keepdim=True)
for _ in range(n - 1):
out = model(nxt, past_key_values=past, use_cache=True)
past = out.past_key_values
nxt = out.logits[:, -1].argmax(-1, keepdim=True)Mira qué se alimenta al modelo dentro del bucle: nxt, un token. No la secuencia. La query del nuevo token atiende contra cada key cacheada, y las keys cacheadas nunca iban a cambiar. Esto no es una aproximación: la comprobación de salida idéntica de arriba es precisamente el punto. La cache no intercambia calidad por velocidad; elimina aritmética redundante.
Para ver el escalado con limpieza, quita el transformer y mide una sola cabeza de attention con , un paso de generación calculado de las dos formas:
| tokens en context | recalcular todo | con cache | ratio | matriz de puntuaciones |
|---|---|---|---|---|
| 128 | 0.59 ms | 0.062 ms | 10x | 65,536 B vs 512 B |
| 256 | 1.20 ms | 0.163 ms | 7x | 262,144 B vs 1,024 B |
| 512 | 7.03 ms | 0.078 ms | 90x | 1,048,576 B vs 2,048 B |
| 1024 | 17.31 ms | 0.114 ms | 152x | 4,194,304 B vs 4,096 B |
| 2048 | 59.83 ms | 0.214 ms | 279x | 16,777,216 B vs 8,192 B |
| 4096 | 236.18 ms | 0.284 ms | 832x | 67,108,864 B vs 16,384 B |
La columna de la derecha es la causa. Recalcular construye toda la matriz de attention en cada paso: el del recuadro de notación asintótica del capítulo 9, pagado una vez por token. Con la cache construyes en su lugar una fila : con 4,096 tokens, 67 MB de puntuaciones frente a 16 KB.
Contar multiplicaciones-acumulaciones en lugar de milisegundos saca la máquina del argumento. Para generar tokens desde un arranque en frío:
| tokens generados | con cache | recalculando | ratio |
|---|---|---|---|
| 128 | 2.6 M | 192.0 M | 73x |
| 512 | 23.1 M | 7.36 G | 318x |
| 2048 | 293.7 M | 392.6 G | 1,336x |
Por paso, la versión cacheada es lineal en el context y la no cacheada cuadrática; sumado a lo largo de una generación, frente a , con una ratio que crece sin límite. La diferencia de nueve veces del inicio se midió sobre 48 tokens: menos que la primera fila de esa tabla.
La cache también cambia qué tiene que estar en memoria. En una GPU de portátil de 8 GB generando 256 tokens en fp16, tomando el pico del asignador y restando los pesos residentes:
| memoria de trabajo pico | |
|---|---|
| con cache | 21.8 MB |
| recalculando | 181.7 MB |
8,3 veces más memoria, gastada para producir los mismos tokens más despacio. Esta es la promesa hecha en el capítulo 5, llegando desde una dirección inesperada: allí, la autodiferenciación en modo inverso tenía que mantener vivo cada intermedio para la pasada hacia atrás, y las activaciones dominaban la memoria de entrenamiento. En inferencia no hay pasada hacia atrás ni nada que retener para ella; así que lo que domina la memoria en su lugar es la cache, y es una elección deliberada más que un coste inevitable.
Prefill y decode son dos máquinas distintas
Enlace a la sección: Prefill y decode son dos máquinas distintasMira de nuevo la ejecución rápida: su primer token se comportó de forma distinta a los otros cuarenta y siete.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepEl prompt costó 25,6 ms por token y cada token generado costó 166 ms. Mismo modelo, mismo hardware, mismos pesos, una diferencia de seis veces por token, y va en la dirección que la mayoría no espera. El prompt es la parte barata. La generación se divide en dos fases con físicas genuinamente distintas:
Prefill
Enlace a la sección: PrefillUna pasada hacia delante sobre todo el prompt. Cada token se procesa en paralelo, así que cada matriz de pesos se carga desde memoria una vez y se multiplica contra una matriz de cientos de vectores de token: un producto matriz-matriz, con mucha aritmética por byte movido, que es para lo que está hecha una GPU. Prefill está limitado por cómputo, y su coste es aproximadamente lineal en la longitud del prompt.
Una pasada hacia delante por token, batch de uno y secuencia de uno. Cada matriz de pesos se sigue cargando completa desde memoria y se multiplica contra un solo vector: un producto matriz-vector, con casi nada de aritmética por byte movido. Decode está limitado por el ancho de banda de memoria, y su coste por token apenas depende de la longitud del context.
Ambas mitades son medibles. Prefill, una pasada sobre tokens:
| tokens del prompt | segundos | ms por token |
|---|---|---|
| 16 | 0.3515 | 21.97 |
| 32 | 0.5254 | 16.42 |
| 64 | 1.0491 | 16.39 |
| 128 | 1.6552 | 12.93 |
| 256 | 3.0965 | 12.10 |
Decode, un token contra una cache de :
| tokens cacheados | ms para un token |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
Lee la segunda tabla dos veces. Pasar de 16 tokens de context a 1,024 —sesenta y cuatro veces más historial al que atender— cambió el coste de un paso en nada medible. La attention contra la cache es trabajo real, pero queda eclipsada por el coste fijo de arrastrar medio millardo de pesos por el bus de memoria para producir un vector. Ese coste fijo es la razón de todo en la siguiente sección.
Estas dos fases son el origen de los dos números que informa todo sistema de servicio. Tiempo hasta el primer token es esencialmente prefill, y crece con el prompt, por eso una conversación larga se siente lenta al empezar. Tokens por segundo es , y es aproximadamente constante, por eso la respuesta luego fluye de forma uniforme. Un chat que empieza despacio y después transmite con suavidad no es un truco de renderizado. Son estas dos tablas.
La cache también es la factura
Enlace a la sección: La cache también es la facturaLa cache intercambia aritmética por memoria, y la memoria que quiere no es poca. Por cada token en el context, cada capa guarda un vector key y un vector value por cabeza key-value:
El 2 es por keys y values; todo lo demás es la arquitectura. Para el modelo medido a lo largo de este capítulo —24 capas, 14 query heads, 2 key-value heads, dimensión de cabeza 64—, en fp16 son bytes por token.
Las fórmulas en este campo tienen la costumbre de equivocarse por un factor de dos, así que compruébalo contra el asignador en lugar de creerlo:
KV cache tensors per layer: (1, 2, 295, 64) float16
measured: 3,624,960 bytes for 295 tokens = 12,288 bytes/token
formula : 2 * 24 * 2 * 64 * 2 = 12,288 bytes/tokenExacto, y sigue siendo exacto en todas las formas probadas:
| batch | context | cache medida | predicción | memoria de trabajo pico |
|---|---|---|---|---|
| 1 | 512 | 6.0 MB | 6.0 MB | 15.4 MB |
| 1 | 16,384 | 192.0 MB | 192.0 MB | 207.3 MB |
| 1 | 65,536 | 768.0 MB | 768.0 MB | 793.7 MB |
| 8 | 4,096 | 384.0 MB | 384.0 MB | 401.5 MB |
| 32 | 2,048 | 768.0 MB | 768.0 MB | 794.2 MB |
| 64 | 1,024 | 768.0 MB | 768.0 MB | 797.0 MB |
| 128 | 512 | 768.0 MB | 768.0 MB | 816.4 MB |
Las tres últimas filas merecen otra mirada. Treinta y dos usuarios con 2,048 tokens cada uno, sesenta y cuatro con 1,024, ciento veintiocho con 512: la cache es de 768 MB en todos los casos, porque las tres contienen 65,536 tokens. La cache depende solo del número total de tokens residentes, no de cómo se distribuyen entre usuarios. Ese hecho es la base de la sección sobre batching.
De dónde salen MQA y GQA
Enlace a la sección: De dónde salen MQA y GQAEl capítulo 9 introdujo multi-query attention y grouped-query attention y pospuso la razón hasta este capítulo. La razón es esa fórmula, y en concreto el que contiene.
La multi-head attention estándar da a cada query head sus propias cabezas key y value. El modelo de aquí tiene 14 query heads; con multi-head attention completa, su cache sería de bytes por token: 84 KB en lugar de 12 KB, exactamente siete veces más, la ratio entre query heads y key-value heads.
Multi-query attention1 lleva esto al límite: todas las query heads comparten una sola key-value head. Grouped-query attention2 es el compromiso que ganó: un puñado de key-value heads, cada una compartida por un grupo de query heads, porque la pérdida de calidad de MQA era real y la de GQA no lo es. Ninguna compra aritmética. Existen para dividir esa fórmula por un entero, y se extendieron por la industria en cuanto los context largos hicieron de la cache la restricción vinculante.
Y lo hace rápido. Para un modelo de clase 7B con 32 capas y 8 key-value heads de dimensión 128, la cache es de 128 KB por token en fp16:
| tokens de context | un usuario | 8 usuarios | 64 usuarios |
|---|---|---|---|
| 4,000 | 0.49 GB | 3.91 GB | 31.2 GB |
| 32,000 | 3.91 GB | 31.25 GB | 250.0 GB |
| 128,000 | 15.62 GB | 125.00 GB | 1,000.0 GB |
| 1,000,000 | 122.07 GB | 976.56 GB | 7,812.5 GB |
Los propios pesos de ese modelo ocupan 13,0 GB en fp16, la cifra de la tabla al final de este capítulo. Así que con un context de 128,000 tokens, la cache de un usuario es mayor que el modelo. Esta es la aritmética que el capítulo 16 convierte en dinero, y por eso una conversación larga no es solo una conversación lenta: ocupa una porción fija de una máquina mientras la solicitud siga viva.
Batching: el número que sube y el número que baja
Enlace a la sección: Batching: el número que sube y el número que bajaDecode está limitado por memoria: los pesos se arrastran por el bus para producir un token, y las unidades aritméticas quedan ociosas. Así que mete más trabajo en el mismo paso. Ejecuta varias solicitudes a la vez, y los pesos, leídos una vez, sirven a todas. Medido en el mismo modelo, con cada solicitud manteniendo una cache de 64 tokens y decodificando un token:
| batch | latencia por paso | throughput | latencia vs B=1 |
|---|---|---|---|
| 1 | 0.1286 s | 7.78 tok/s | 1.00x |
| 2 | 0.1839 s | 10.88 tok/s | 1.43x |
| 4 | 0.1909 s | 20.95 tok/s | 1.49x |
| 8 | 0.2781 s | 28.76 tok/s | 2.16x |
| 16 | 0.3430 s | 46.64 tok/s | 2.67x |
| 32 | 0.6302 s | 50.78 tok/s | 4.90x |
Lee las dos columnas de la derecha una contra la otra, porque son todo el asunto. Pasar de una solicitud a dieciséis multiplica el throughput por 6,0 y multiplica la espera de cualquier solicitud individual por 2,67. El batch hizo mejor al servidor y peor a cada usuario.
No es un bug que se pueda ajustar hasta que desaparezca; es el intercambio en sí, y tiene un nombre en cada lado. La latencia es lo que experimenta una persona que espera una respuesta. El throughput es aquello entre lo que se divide la factura. Ningún ajuste mejora ambos.
Fíjate también dónde se detiene. De 16 a 32, el throughput gana un 9 % mientras la latencia casi se duplica: el paso ha dejado de estar limitado por memoria y ha pasado a estar limitado por cómputo, y más allá de esa rodilla el batch no compra nada. Todo despliegue tiene una rodilla así; su ubicación hay que medirla en el tuyo, pero su existencia no.
El batching estático desperdicia la mayor parte de lo que gana
Enlace a la sección: El batching estático desperdicia la mayor parte de lo que ganaLa forma ingenua de hacer batching es reunir solicitudes, ejecutarlas juntas y devolver cuando todas hayan terminado. Pero no terminan juntas: algunas respuestas tienen veinte tokens y otras quinientos. Un batch fijo se ejecuta hasta que termina su miembro más largo, y cada solicitud ya terminada sigue ocupando su hueco, aportando padding, hasta entonces.
Toma 64 solicitudes con una asimetría realista de longitudes de salida —mediana de 18 tokens, la más larga de 231, 1,874 en total— y simula ambas políticas con el coste por paso medido para ocho huecos:
| política | tiempo de reloj | throughput | latencia media por solicitud | slot-steps desperdiciados |
|---|---|---|---|---|
| batches estáticos de 8 | 176.9 s | 10.6 tok/s | 83.2 s | 3,214 |
| continuo, 8 huecos | 109.0 s | 17.2 tok/s | 8.1 s | 0 |
El throughput mejora 1,6x. La latencia media mejora más de diez veces, porque con batching estático una solicitud que terminó en cuatro pasos sigue esperando a un vecino de 231 tokens antes de que nadie oiga nada de ella.
Continuous batching3 es la solución, y es tan simple como suena: el batch no es un grupo sino un conjunto de huecos, y un hueco que se libera admite la siguiente solicitud en cola en el mismísimo paso siguiente. El planificador trabaja a la granularidad de un token en lugar de una solicitud. Todo stack de serving en producción lo hace ya.
Tiene una segunda mitad, que es la cache. Los huecos que aparecen y desaparecen dejan la memoria de cache fragmentada, y reservar para cada hueco su context máximo posible desperdicia la mayor parte de la reserva. PagedAttention4 toma prestada la respuesta de los sistemas operativos: almacenar la cache en bloques de tamaño fijo con una tabla de bloques por secuencia, para que la cache de una secuencia pueda estar físicamente dispersa mientras sigue siendo lógicamente contigua; lo que también permite que dos secuencias con un prefijo compartido compartan los bloques que lo contienen. Eso es sobre lo que se construye vLLM, y por eso un motor de serving es un asignador de memoria con un transformer acoplado.
Cuantización, y lo primero que falla
Enlace a la sección: Cuantización, y lo primero que fallaLa otra mitad de la factura son los propios pesos. Medio millardo de parámetros a cuatro bytes cada uno son 1,98 GB; a dos bytes, 0,99 GB; a un byte, 0,49 GB. Menos bits por peso reducen el modelo en disco, lo reducen en memoria y —como decode está limitado por ancho de banda— hacen que cada paso sea más rápido, porque hay menos bytes que mover.
El esquema más simple es la cuantización simétrica por máximo absoluto, y cabe en tres líneas:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedElige una escala para que el peso mayor se mapee al entero mayor, divide, redondea, guarda los enteros y la escala. Reconstruye multiplicando de vuelta. No tiene nada de ingenioso, y funciona, justo hasta que deja de hacerlo.
Medido sobre los pesos reales del modelo: las 168 matrices de proyección, 357,8 millones de parámetros, error relativo :
| esquema | error relativo medio | peor matriz |
|---|---|---|
| INT8, una escala para toda la matriz | 0.0400 | 0.1487 |
| INT8, una escala por fila de salida | 0.0100 | 0.0149 |
| INT4, una escala para toda la matriz | 0.6026 | 0.9931 |
| INT4, una escala por fila de salida | 0.1790 | 0.2589 |
| INT4, una escala por grupo de 128 | 0.1323 | 0.1992 |
| NF4, una escala por bloque de 64 | 0.0952 | 0.1205 |
| INT3, una escala por grupo de 128 | 0.3044 | 0.4123 |
| INT2, una escala por grupo de 128 | 0.7790 | 0.8076 |
La cuarta fila es el colapso. Un error relativo de 0,99 en la peor matriz significa que la reconstrucción no conserva esencialmente nada del original: la matriz ha sido reemplazada por ruido de una magnitud más o menos adecuada. La causa se ve en el mismo experimento sobre una sola matriz:
model.layers.12.mlp.down_proj.weight (896 x 4864)
mean |w| 0.01386 std 0.01822 max |w| 0.43945 max/std 24.1
weights beyond 6 sigma: 692 of 4,358,144 (0.016 %)Un peso de cada seis mil está más allá de seis desviaciones estándar, y el mayor está a 24. Con una sola escala para toda la matriz, ese peso fija el tamaño del paso para los 4,3 millones restantes. Con 8 bits hay 256 pasos y el peso típico aún cae en uno significativo. Con 4 bits hay 16, el más extremo reservado para un valor que casi nada tiene, y los pesos ordinarios —que son todos— redondean a dos o tres niveles distintos.
Todo después de esa fila es la misma reparación con distintas granularidades: da a la escala un territorio más pequeño. Por fila de salida divide el error por 3,4; por grupo de 128 pesos consecutivos lo vuelve a dividir. El coste es contabilidad: una escala de 16 bits por grupo de 128 son bits por peso en lugar de 4, y recupera la mayor parte de la brecha.
NF4 lo aborda desde el otro lado.5 Los niveles no tienen por qué estar espaciados por igual. Los pesos dentro de un bloque se distribuyen aproximadamente de forma normal, así que elige los dieciséis niveles como los cuantiles de una distribución normal: densos cerca de cero, donde están realmente los pesos, dispersos en las colas, donde no están. Los mismos cuatro bits, el mismo escalado por bloque, en un bloque más pequeño —4,25 bits por peso frente a los 4,125 del grupo de 128— y el error medido cae de 0,1323 a 0,0952, un 28 % menos. Parte de eso es el bloque más fino y el resto es poner los niveles donde está la masa; separarlos exigiría una tercera fila.
Las características outlier
Enlace a la sección: Las características outlierEl recuadro de coma flotante del capítulo 2 terminó con una promesa: que este capítulo cuantizaría pesos a 8 y 4 bits y encontraría un puñado de características outlier que se negaban a comprimirse. Aquí están, y explican por qué «simplemente redondear los números» nunca iba a funcionar con activaciones.
Los pesos de arriba se comportaban mal. Las activaciones juegan en otra liga. Toma un prompt ordinario de 84 tokens, captura el flujo residual en cada capa y mide la mayor magnitud que alcanza cada una de las 896 dimensiones:
| capa | mayor |h| | mayor |h| de la dimensión mediana | ratio | dimensiones por encima de 6x la mediana |
|---|---|---|---|---|
| 1 | 6.19 | 0.339 | 18x | 2 |
| 4 | 1543.48 | 1.550 | 996x | 34 |
| 8 | 1571.63 | 1.498 | 1049x | 36 |
| 12 | 1575.03 | 1.546 | 1019x | 34 |
| 16 | 1579.60 | 1.617 | 977x | 32 |
| 20 | 1577.98 | 2.361 | 668x | 24 |
| 24 | 204.44 | 10.760 | 19x | 12 |
La dimensión 62 llega a 1,579,6 mientras la dimensión mediana nunca supera 1,6. No es una rareza de un token o una capa: la misma dimensión está ahí en la capa 4 y sigue ahí en la capa 20, con casi el mismo valor. Estas son las outlier features,6 y son sistemáticas: una propiedad del modelo entrenado, no de la entrada.
El histograma de esos 896 máximos por dimensión en la capa 16 hace que la forma sea inconfundible:
0 - 1 | ######################################## 254
1 - 2 | ######################################## 283
2 - 4 | ######################################## 226
4 - 8 | ######################################## 93
8 - 16 | ################## 18
16 - 32 | ######### 9
32 - 64 | ####### 7
64 - 128 | ##### 5
128 - 256 | 0
256 - 512 | 0
512 - 1024 | 0
1024 - 4096 | # 1Novecientas dimensiones en un montón ordenado por debajo de 8, nada en absoluto durante tres octavas, y luego una dimensión sola en el extremo lejano. Ahora cuantiza ese tensor a INT8 y cuenta qué ocurre:
| esquema | error relativo | niveles enteros distintos usados, tensor completo |
|---|---|---|
| una escala para todo el tensor | 0.1083 | 14 de 256 |
| una escala por token (por fila) | 0.0433 | 158 |
| tensor completo, 1 dimensión outlier mantenida en fp32 | 0.0442 | 48 |
| tensor completo, 4 dimensiones outlier mantenidas en fp32 | 0.0279 | 57 |
| tensor completo, 16 dimensiones outlier mantenidas en fp32 | 0.0085 | 102 |
Catorce niveles de 256. La escala la fijó 1,579,6, así que cada paso tiene anchura 12,44, y la activación típica —magnitud mediana 0,26, percentil noventa y nueve 2,51— no tiene dónde caer. Por dimensión es más crudo:
single tensor-wide scale = 12.4378
dim 826 (max |h| = 4.77): 1 distinct level out of 256
dim 336 (max |h| = 1.62): 1 distinct level out of 256
dim 96 (max |h| = 0.69): 1 distinct level out of 256
after excluding the top 4 dimensions, scale = 0.5749 (22x smaller)
dim 826: 8 levels dim 336: 4 levels dim 96: 3 levelsUn nivel. Toda la dimensión, cada token, cuantizada al mismo número. Se asignaron ocho bits y se usaron aproximadamente cero, y el modelo que lee esas activaciones recibe una constante.
Esa medición es la justificación de todas las técnicas que la gente usa de verdad:
Deja fuera a los outliers. LLM.int8()6 descompone la multiplicación matricial: las dimensiones con magnitudes extremas se calculan en 16 bits, todo lo demás en INT8, y las mitades se suman. La tabla anterior es el recibo: quitar cuatro dimensiones reduce el error por un factor de casi cuatro. SmoothQuant7, en cambio, migra la dificultad: divide las activaciones por un factor por canal y multiplica por él la columna de pesos correspondiente, lo que deja el producto sin cambios y mueve el outlier fuera del tensor que no puede absorberlo hacia el que sí puede.
Elige el redondeo, no te limites a redondear. Nada de lo anterior pregunta para qué sirve la matriz. GPTQ8 cuantiza columna por columna y, después de cada una, ajusta las columnas restantes en precisión completa para compensar el error ya cometido, minimizando el error de la salida de la capa con entradas reales, no el de sus pesos. AWQ9 observa que una pequeña fracción de los canales de pesos importa mucho más que el resto, los encuentra a partir de estadísticas de activación y los escala hacia arriba antes de cuantizar para que caigan en niveles más finos. Ambos necesitan un conjunto de calibración; ninguno necesita gradientes.
Mostrar detalles
GGUF, y qué tiene que ver un formato de archivo con todo esto.
GGUF no es un método de cuantización; es el contenedor que usa llama.cpp, y la confusión en las comparaciones de gguf vs gptq viene de tratar ambos como si fueran el mismo tipo de cosa. GGUF contiene tensores, tokenizer, metadatos de arquitectura y plantilla de chat en un archivo mapeable en memoria, y lleva dentro una familia de esquemas por bloques: nombres como Q4_K_M codifican bits por peso, tamaño de bloque y si algunos tensores se mantienen a mayor precisión.
La diferencia de ingeniería que importa: GPTQ y AWQ producen pesos optimizados para un kernel de GPU, mientras que los esquemas de GGUF se decodifican de forma barata en una CPU con el archivo mapeado en lugar de cargado. Por eso el mismo «modelo 7B de 4 bits» nominal existe en ambos mundos con tamaños distintos y calidad distinta, y por eso la comparación honesta nunca es el formato: es la medición de abajo, ejecutada en tu propia tarea.
Cuánto cuesta realmente la cuantización, medido
Enlace a la sección: Cuánto cuesta realmente la cuantización, medidoCasi todos los artículos sobre cuantización se detienen en la sección anterior: explican el método, citan una ratio de compresión y afirman que la calidad se «preserva en gran medida». El capítulo 4 iba de no engañarte a ti mismo, así que averigüémoslo.
El mismo modelo, pesos cuantizados in situ con cada esquema, y luego tres mediciones: perplejidad en 2,048 tokens de prosa inglesa retenida —aquí, el borrador de este curso, motivo por el cual el repositorio lo sustituye por un libro fijo de dominio público e imprime una tabla de la misma forma con números distintos—, una batería de 16 preguntas factuales cortas con respuestas conocidas bajo greedy decoding, y la fracción de tokens en la que el modelo cuantizado coincide con el de precisión completa dado un context idéntico.
| esquema | error medio de pesos | perplejidad | batería de preguntas | coincide con fp32 |
|---|---|---|---|---|
| fp32 (referencia) | 0.0000 | 23.08 | 13/16 | 100.0 % |
| INT8 por tensor | 0.0400 | 23.58 | 13/16 | — |
| INT8 por fila | 0.0100 | 22.96 | 13/16 | 98.6 % |
| INT4 por tensor | 0.6026 | 365,416,000 | 0/16 | — |
| INT4 por fila | 0.1790 | 46.18 | 6/16 | 58.3 % |
| INT4 grupo 128 | 0.1323 | 31.08 | 10/16 | 71.5 % |
| NF4 bloque 64 | 0.0952 | 24.55 | 11/16 | 84.7 % |
| INT3 grupo 128 | 0.3044 | 213.09 | 0/16 | 5.6 % |
| INT2 grupo 128 | 0.7790 | 26,325,436 | 0/16 | 0.0 % |
Merece la pena decir claramente cuatro cosas de esa tabla.
INT8 bien hecho sale gratis. INT8 por fila puntúa 22,96 frente a los 23,08 de la referencia: una brecha de una parte en doscientas, que es ruido y debe leerse como «idéntico». Hacia qué lado apunta el ruido no es estable: en el corpus de dominio público del repositorio, los mismos dos esquemas salen 22,24 frente a 22,18: la mitad de esa distancia, y apuntando al otro lado. Coincide con el modelo de precisión completa en 142 de 144 tokens generados. Un cuarto de la memoria frente a la referencia fp32, la mitad frente al fp16 que desplegarías de verdad, y sin coste detectable. INT8 hecho sin cuidado también es casi gratis: una escala por matriz cuesta 0,5 puntos de perplejidad y ninguna respuesta de la batería. Ocho bits perdonan lo suficiente para que la granularidad apenas importe, que es exactamente por lo que la gente generaliza de INT8 a INT4 y se hace daño.
INT4 con una escala por tensor destruye el modelo. Perplejidad 365 millones: no degradado, aniquilado. La granularidad es entonces todo el juego: por tensor 365,416,000, por fila 46,18, por grupo de 128 31,08, NF4 24,55. Los mismos cuatro bits por peso, un factor de quince millones entre el peor y el mejor.
La perplejidad es un instrumento tosco y la batería uno más tosco aún. Entre NF4 e INT4 grupo 128 la brecha de perplejidad es de 6,5 puntos y la batería difiere en una pregunta; y el intervalo de confianza del capítulo 4 dice que una pregunta de dieciséis no distingue absolutamente nada. Hay una demostración más nítida que el intervalo: ejecuta la misma batería con la penalización de repetición por defecto del modelo desactivada, que es lo que realmente significa greedy decoding, y esas dos filas intercambian posiciones. Una pregunta de dieciséis no es un efecto pequeño, es ningún efecto. También aplica la advertencia del capítulo 8: la perplejidad solo es comparable entre modelos que comparten tokenizer, así que un número del artículo de otra persona no puede compararse con el tuyo.
La columna de coincidencia es la más afilada de las tres, y casi gratis: ejecuta el modelo de precisión completa de forma greedy, y luego pregunta al cuantizado, en cada posición, qué habría elegido dado el mismo prefijo. Tiene 144 observaciones independientes en lugar de 16, no necesita verdad de referencia y se degrada suavemente donde la batería se degrada a saltos. También es exactamente la cantidad que necesita la siguiente sección.
Esta es la promesa que hizo el capítulo 1 sobre este capítulo, llegando a tiempo: las matemáticas dicen que un modelo de 4 bits es posible, y la ingeniería decide si es utilizable.
Speculative decoding
Enlace a la sección: Speculative decodingEl capítulo 12 anunció esto y dejó aquí la factura.
La idea sale directamente de la división prefill/decode. Verificar una secuencia propuesta de tokens cuesta una pasada hacia delante sobre posiciones: un producto matriz-matriz, apenas más caro que la pasada sobre una. Así que:
Un modelo pequeño y barato genera tokens candidatos de forma autorregresiva.
El modelo grande ejecuta una pasada hacia delante sobre los candidatos a la vez, produciendo lo que habría dicho en cada posición.
Conserva el prefijo más largo en el que ambos coinciden, más el token que el modelo grande proporciona gratis en la primera discrepancia. Descarta el resto y empieza de nuevo.
La distribución de salida no cambia. Con greedy decoding eso es obvio: un token se acepta solo si el objetivo lo habría producido. Con sampling requiere una regla de aceptación modificada, y Leviathan et al. demuestran que la distribución resultante es exactamente la del objetivo.10 Esta es la segunda optimización exacta de este capítulo.
Por tanto, todo depende de la tasa de aceptación , que es medible: es la columna de coincidencia de arriba, por eso se calculó allí. Usando cada modelo cuantizado como draft para el objetivo de precisión completa, sobre 144 posiciones generadas:
| modelo draft | aceptación | racha aceptada más larga | tokens esperados por pasada del objetivo, |
|---|---|---|---|
| fp32 (el propio objetivo) | 100.0 % | 48 | 5.00 |
| INT8 por fila | 98.6 % | 48 | 4.86 |
| NF4 bloque 64 | 84.7 % | 20 | 3.69 |
| INT4 grupo 128 | 71.5 % | 13 | 2.85 |
| INT4 por fila | 58.3 % | 7 | 2.24 |
| INT3 grupo 128 | 5.6 % | 2 | 1.06 |
| INT2 grupo 128 | 0.0 % | 0 | 1.00 |
Los tokens aceptados esperados por pasada de verificación, con longitud de draft , son
y la aceleración neta divide eso por el propio coste del draft, una fracción del objetivo por token:
| aceptación | , | , | , | , |
|---|---|---|---|---|
| 30 % | 1.19x | 1.02x | 0.79x | 0.79x |
| 50 % | 1.61x | 1.38x | 1.08x | 1.11x |
| 70 % | 2.31x | 1.98x | 1.54x | 1.78x |
| 90 % | 3.41x | 2.93x | 2.28x | 3.40x |
La entrada en negrita es la que hay que recordar: speculative decoding puede hacer que la generación sea más lenta. Con un 30 % de aceptación y un draft que cuesta una quinta parte del objetivo, pagas cinco pasadas hacia delante y conservas 1,4 tokens. La última columna es la otra trampa: un draft más largo solo ayuda cuando la aceptación es alta, porque la cola de una conjetura de tokens casi nunca se alcanza. Con 90 % de aceptación, vale 3,40x; con 30 %, vale 0,79x: la misma configuración, una victoria o una pérdida según un número medido en tu tráfico.
Destilación, y qué contiene una etiqueta soft
Enlace a la sección: Destilación, y qué contiene una etiqueta softLa cuantización encoge un modelo almacenando la misma función en menos bits. La destilación lo encoge entrenando un modelo más pequeño para imitar a uno más grande11, una idea anterior al deep learning en casi una década.12
La parte sutil es de qué aprende el estudiante. No de la respuesta correcta: podría haberse entrenado directamente con ella. Lo que añade el profesor es la distribución completa. Pregunta al modelo qué sigue a una frase y mira más allá del argmax:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360La etiqueta hard dice jug y nada más. La etiqueta soft dice jug, y también que cup era casi igual de bueno, bowl plausible, y large —un adjetivo, una continuación gramatical completamente distinta— seguía vivo. Ese es el argumento original: esto es un 7, pero se parece bastante a un 1, y el parecido es información que la etiqueta hard tira a la basura.
También por eso la destilación usa una temperatura. Dividir los logits por antes de la softmax aplana la distribución y eleva el peso relativo de los segundos: en esta frase, la ratio entre el token superior y el tercero cae de 2,24 con a 1,50 con , la raíz cuadrada de la primera, que es lo que dividir los logits por dos hace a una ratio. Mismo orden, más attention de la pérdida en los fallos cercanos. El gradient del estudiante lleva la incertidumbre del profesor y no solo su veredicto.
Qué cabe en 8, 16 y 24 GB
Enlace a la sección: Qué cabe en 8, 16 y 24 GBTodo en este capítulo es ahora una suma:
donde son los tokens totales residentes entre todas las solicitudes concurrentes. Al aplicarlo: las filas 7B y 70B suponen 8 key-value heads de dimensión 128; la fila 13B, multi-head attention completa con 40 cabezas, que es como se construyeron esas generaciones de modelos, y se nota.
8 GB
| modelo | precisión | pesos | libre tras overhead | tokens de context que caben |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | no cabe | — |
| 7B | int8 | 6.5 GB | no cabe | — |
| 7B | int4 (g128) | 3.4 GB | 3.1 GB | 25,710 |
| 13B | int4 (g128) | 6.2 GB | 0.3 GB | 337 |
| 70B | int4 (g128) | 33.6 GB | no cabe | — |
16 GB
| modelo | precisión | pesos | libre tras overhead | tokens de context que caben |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 1.5 GB | 11,972 |
| 7B | int8 | 6.5 GB | 8.0 GB | 65,378 |
| 7B | int4 (g128) | 3.4 GB | 11.1 GB | 91,246 |
| 13B | int8 | 12.1 GB | 2.4 GB | 3,136 |
| 13B | int4 (g128) | 6.2 GB | 8.3 GB | 10,822 |
24 GB
| modelo | precisión | pesos | libre tras overhead | tokens de context que caben |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 9.5 GB | 77,508 |
| 7B | int8 | 6.5 GB | 16.0 GB | 130,914 |
| 7B | int4 (g128) | 3.4 GB | 19.1 GB | 156,782 |
| 13B | int8 | 12.1 GB | 10.4 GB | 13,622 |
| 13B | int4 (g128) | 6.2 GB | 16.3 GB | 21,308 |
| 70B | int4 (g128) | 33.6 GB | no cabe | — |
Mira la fila 13B de la tabla de 8 GB. Los pesos caben —6,2 GB de 8—, así que, según la forma habitual de hablar, un modelo 13B «funciona en una tarjeta de 8 GB». Tiene 337 tokens de context, que no es una conversación sino apenas un prompt. «¿Cabe?» es la pregunta equivocada. La correcta es «con cuánto context y para cuántos usuarios a la vez».
Mira también las dos filas int8 de 16 GB. El 7B consigue 65,378 tokens y el 13B consigue 3,136: una diferencia de veinte veces por 5,6 GB de pesos extra, porque este 13B tiene multi-head attention y su cache cuesta 800 KB por token frente a los 128 KB del 7B. Dos modelos de tamaño parecido, uno inutilizable para context largo, por una razón que no aparece en el titular de ninguna model card.
Adónde va esto ahora
Enlace a la sección: Adónde va esto ahoraHace trece capítulos esto era un perceptrón con dos pesos y un sesgo. Ahora es un transformer que ha sido diseñado, entrenado, alineado, enseñado a gastar cómputo en preguntas difíciles y servido a un coste medido por token, sin que quede ninguna caja sin abrir.
Esto termina aquí, y termina a propósito.
El capítulo 14 empieza con el modelo en otro lugar. No en tu proceso, no en tu memoria, no en una variable que puedas imprimir: en una máquina que no administras, detrás de una API key, un puerto y una factura. Todo lo medido aquí sigue ocurriendo: prefill sigue ejecutándose antes del primer token, la cache sigue creciendo con la conversación, el batch en el que estás sigue perteneciendo a otra persona y sigue decidiendo tu latencia; pero a partir de ahora lo observas a través de un flujo de Server-Sent Events, un finish_reason y un HTTP 429 con una cabecera Retry-After. Las preguntas cambian con el punto de vista: no cómo se calcula este gradient, sino por qué se ha triplicado mi factura. También cambia el lenguaje, y el capítulo 14 explica esa regla en lugar de anunciarla: hasta aquí el código contenía pesos, gradientes, logits y bytes de tokenizer; a partir de ahí contiene una conexión, un reintento, una cancelación y estado acumulado. Los trece capítulos que dejas atrás no se descartan al cruzar. Son la descripción de lo que se está ejecutando al otro lado del puerto.
Fuentes y método
Enlace a la sección: Fuentes y métodoDos omisiones son deliberadas. FlashAttention (Dao et al., arXiv:2205.14135) no es una attention distinta: calcula la misma función dividiendo la operación en teselas para que la matriz de puntuaciones nunca se escriba en memoria, por eso los 67 MB de la segunda tabla de este capítulo son menores en la práctica de lo que sugiere la aritmética. Y los propios kernels quedan delegados: la clase 10 del CS336 de Stanford cubre los sistemas de inferencia con una profundidad que esto no intenta, y el repositorio llama.cpp y la especificación GGUF son las fuentes primarias para el lado CPU.
Referencias
Enlace a la sección: Referencias-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). El artículo es en gran medida un argumento sobre ancho de banda de memoria, y se lee como tal. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Incluye la receta de uptraining que convierte un checkpoint multi-head existente, por eso GQA se extendió tan rápido. ↩
-
Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Introduce la planificación a nivel de iteración —continuous batching— y el batching selectivo. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. El artículo sobre el que se construye vLLM; el §3 desarrolla por completo la analogía con los sistemas operativos. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 se define en el §3; los dieciséis valores de nivel usados en la medición de arriba son los que deriva este artículo. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). El análisis de outlier features del §4 es la fuente del fenómeno medido arriba, incluido el hallazgo de que los outliers emergen sistemáticamente a escala. ↩ ↩2
-
Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022). ↩
-
Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023). ↩
-
Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). El teorema 1 es la prueba de que la distribución de salida no cambia; Chen et al. (arXiv:2302.01318) publicaron la misma idea de forma independiente. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). La temperatura y el argumento del «dark knowledge». ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Destilación, nueve años antes, para ensembles en lugar de transformers. ↩