llm.int8(): Cuantización 8-bit para Transformers

llm.int8(): Cuantización 8-bit para Transformers

En el post LLMs quantization explicamos la importancia de la cuantización de los LLMs para ahorrar memoria. Además, explicamos que existe una manera de cuantización que es la cuantización de punto cero que consiste en transformar los valores de los parámetros de los pesos linealmente, pero esto tiene el problema de la degradación de los modelos de lenguaje a partir del momento en que superan los 2.7B de parámetros

llm.int8()-degradation

Cuantización vectoriallink image 1

Como la cuantización de todos los parámetros de los modelos produce error en los grandes modelos de lenguaje, lo que proponen en el paper llm.int8() es realizar la cuantización vectorial, es decir, separar las matrices de los pesos en vectores, de manera que algunos de esos vectores se pueden cuantizar en 8 bits, mientras que otros no. Por lo que los que sí se pueden cuantizar en 8 bits se cuantizan y se realizan las multiplicaciones matriciales en formato INT8, mientras que los vectores que no pueden ser cuantizados se mantienen en formato FP16 y se realizan las multiplicaciones en formato FP16.

Veámoslo con un ejemplo

Supongamos que tenemos la matriz

llm.int8()-A

y la queremos multiplicar por la matriz

llm.int8()-B

Establecemos un valor umbral y todas las columnas de la primera matriz que tengan un valor mayor a ese umbral se dejan en formato FP16. Las filas equivalentes a las filas de la primera matriz, en la segunda matriz también se dejan en formato FP16.

Lo explico más claro, como la segunda y cuarta columna de la primera matriz (columnas amarillas) tienen valores mayores a un cierto umbral, entonces la segunda y la cuarta fila de la segunda matriz (filas amarillas) se dejan en formato FP16

En caso de tener valores umbrales en la segunda matriz se haría lo mismo, por ejemplo, si en la segunda matriz una fila tuviese un valor mayor a un umbral se dejaría en formato FP16, y esa columna en la primera matriz se dejaría en formato FP16

El resto de filas y columnas que no se dejan en formato FP16 se cuantizan en 8 bits y se realizan las multiplicaciones en formato INT8

Así que separamos la primera matriz en las dos submatrices

llm.int8()-A_separated

Y la segunda matriz en las dos matrices

llm.int8()-B_separated

Multiplicamos las matrices en INT8 por un lado

llm.int8()-AxB-int8

Y las que están en formato FP16 por otro lado

llm.int8()-AxB-fp16

Como se puede ver, multiplicar las matrices en formato INT8 nos da como resultado una matriz de tamaño 3x2, y multiplicar las matrices en formato FP16 nos da como resultado otra matriz de tamaño 3x2, por lo que si las sumamos

llm.int8()-fp16+int8

Curiosamente, nos da el mismo resultado que si hubiésemos multiplicado las matrices originales

llm.int8()-AxB

Para poder ver por qué ocurre esto, si desarrollamos el producto vectorial de las dos matrices originales

llm.int8()-AxB-explained

Vemos que la separación que hemos hecho no da problemas

Por tanto, podemos concluir, que podemos separar filas y columnas de las matrices para realizar las multiplicaciones matriciales. Esta separación se hará cuando algún elemento de la fila o la columna sea mayor que un valor umbral, de manera que als filas o columnas que no tengan un valor mayor a ese umbral se codificarán en INT8 ocupando solo un byte y las filas o columnas que tengan algún elemento mayor que ese umbral se pasarán a FP16 ocupando 2 bytes. De esta manera no tendremos problemas de redondeo, ya que los cálculos que hagamos en INT8 los haremos con valores que hagan que las multiplicaciones no superen el rango de los 8 bits.

Valor umbral αlink image 2

Como hemos dicho vamos a separar en filas y columnas que tengan algún elemento mayor que un valor umbral, pero ¿Qué valor umbral debemos elegir? Los autores del paper hicieron experimentos con varios valores y determinaron que ese valor umbral debía ser α=6. Por encima de ese valor empezaron a obtener degradaciones en los modelos de lenguaje

Uso de llm.int8()link image 3

Vamos a ver cómo cuantizar un modelo con llm.int8() con la librería transformers. Para ello hay que tener instalado bitsandbytes

pip install bitsandbytes

Cargamos un modelo de 1B de parámetros dos veces, una de manera normal y la segunda cuantizándolo con llm.int8()

	
< > Input
Python
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
checkpoint = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForCausalLM.from_pretrained(checkpoint).to(device)
model_8bit = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", load_in_8bit=True)
Copied
>_ Output
			
The `load_in_4bit` and `load_in_8bit` arguments are deprecated and will be removed in the future versions. Please, pass a `BitsAndBytesConfig` object in `quantization_config` argument instead.

Vemos cuánta memoria ocupa cada uno de los modelos

	
< > Input
Python
model.get_memory_footprint()/(1024**3), model_8bit.get_memory_footprint()/(1024**3)
Copied
>_ Output
			
(4.098002195358276, 1.1466586589813232)

Como se puede ver, el modelo cuantizado ocupa mucha menos memoria

Vamos ahora a hacer una prueba de generación de texto con los dos modelos

	
< > Input
Python
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(device)
input_tokens.input_ids
Copied
>_ Output
			
tensor([[ 1, 15043, 590, 1024, 338, 5918, 4200, 322, 306, 626,
263, 6189, 29257, 10863, 261]], device='cuda:0')

Vemos la salida con el modelo normal

	
< > Input
Python
import time
t0 = time.time()
max_new_tokens = 50
outputs = model.generate(
input_ids=input_tokens.input_ids,
attention_mask=input_tokens.attention_mask,
max_length=input_tokens.input_ids.shape[1] + max_new_tokens,
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
print(time.time() - t0)
Copied
>_ Output
			
Hello my name is Maximo and I am a Machine Learning Engineer. I am currently working at [Company Name] as a Machine Learning Engineer. I have a Bachelor's degree in Computer Science from [University Name] and a Master's degree in Computer Science from [University Name]. I
1.7616662979125977

Y ahora con el modelo cuantizado

	
< > Input
Python
t0 = time.time()
max_new_tokens = 50
outputs = model_8bit.generate(
input_ids=input_tokens.input_ids,
attention_mask=input_tokens.attention_mask,
max_length=input_tokens.input_ids.shape[1] + max_new_tokens,
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
print(time.time() - t0)
Copied
>_ Output
			
Hello my name is Maximo and I am a Machine Learning Engineer. I am currently working at [Company Name] as a Machine Learning Engineer. I have a Bachelor's degree in Computer Science from [University Name] and a Master's degree in Computer Science from [University Name]. I
9.100712776184082

Vemos dos cosas: por un lado, que a la salida obtenemos el mismo texto; por lo que con un modelo mucho más pequeño podemos obtener la misma salida. Sin embargo, el modelo cuantizado tarda mucho más en ejecutarse, por lo que si se necesita usar este modelo en tiempo real no sería recomendable.

Esto es contradictorio, porque podríamos pensar que un modelo más pequeño tendría que ejecutarse más rápido, pero hay que pensar que en realidad los dos modelos, el normal y el cuantizado, realizan las mismas operaciones, solo que uno realiza todas las operaciones en FP32 y el otro las hace en INT8 y FP16, sin embargo el modelo cuantizado tiene que buscar filas y columnas con valores mayores al valor umbral, separarlas, realizar las operaciones en INT8 y FP16 y luego volver a juntar los resultados, por lo que el modelo cuantizado tarda más en ejecutarse.

Posts relacionados

Preguntas frecuentes

¿Por qué un modelo cargado con load_in_8bit=True tarda más en generar texto que el mismo modelo en FP32, aunque ocupe mucha menos memoria?

Porque llm.int8() no hace una cuantización uniforme: en cada multiplicación matricial tiene que recorrer filas y columnas buscando valores absolutos mayores al umbral α=6, separar esas filas/columnas "outlier" en una submatriz que se queda en FP16, cuantizar el resto a INT8, ejecutar dos multiplicaciones por separado (INT8 y FP16) y sumar los resultados. Ese trabajo extra de detección y recomposición añade overhead frente a operar todo en un único formato, aunque el modelo cargado con AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", load_in_8bit=True) ocupe bastante menos memoria según model.get_memory_footprint().

¿Cómo decide llm.int8() qué columnas de una matriz de pesos se cuantizan en INT8 y cuáles se dejan en FP16?

Aplica cuantización vectorial (vector-wise): fija un umbral α=6 y, si algún elemento de una columna de la matriz de activaciones supera ese valor absoluto, esa columna (y la fila equivalente en la segunda matriz) se excluye de la cuantización y se mantiene en FP16; el resto de filas/columnas se cuantiza a INT8. Después se multiplican por separado las submatrices INT8 y FP16 y se suman los resultados, dando el mismo valor que multiplicar las matrices originales en precisión completa, sin que los outliers que aparecen a partir de los 2.7B de parámetros degraden el modelo.

Seguir leyendo

Últimos posts -->

¿Has visto estos proyectos?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

App de fitness con dos agentes que se ejecutan íntegramente en el dispositivo, sin backend, de forma que los datos del usuario nunca salen del móvil. Un coach conversacional BYOK con adaptadores para OpenAI, Anthropic y Google, 12 tools locales y system prompt remoto con fallback offline, y un subagente de visión que estima macronutrientes a partir de fotos de comida, con lectura de códigos de barras contra OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Sistema multiagente de investigación construido con LangGraph. Un supervisor descompone tu pregunta en temas y lanza subagentes de búsqueda en paralelo; cada uno comprime sus hallazgos antes de pasarlos a un agente redactor que escribe el informe final en markdown con sus fuentes. Streaming en vivo por WebSockets, modelo configurable por rol y claves de API propias que nunca se guardan en el servidor.

Tau

Tau Tau
Python
LangChain

Sistema multiagente de tutoría para estudiantes de secundaria, con un agente por asignatura y material de curso elaborado y validado por un equipo de profesores. Llegó a usarse con alumnos reales en un colegio privado en España y en un instituto en Colombia.

Ver todos los proyectos -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres ver alguna charla?

Últimas charlas -->

¿Quieres mejorar con estos tips?

Últimos tips -->

Usa esto en local

Los espacios de Hugging Face nos permite ejecutar modelos con demos muy sencillas, pero ¿qué pasa si la demo se rompe? O si el usuario la elimina? Por ello he creado contenedores docker con algunos espacios interesantes, para poder usarlos de manera local, pase lo que pase. De hecho, es posible que si pinchas en alún botón de ver proyecto te lleve a un espacio que no funciona.

Flow edit

Flow edit Flow edit

Edita imágenes con este modelo de Flow. Basándose en SD3 o FLUX puedes editar cualquier imagen y generar nuevas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos los contenedores -->
>_ Disponible para proyectos

¿Tienes un proyecto con IA?

Hablemos.

maximofn@gmail.com

Especialista en Machine Learning e Inteligencia Artificial. Desarrollo soluciones con IA generativa, agentes inteligentes y modelos personalizados.

¿Quieres entrenar tu modelo con estos datasets?

short-jokes-dataset

HuggingFace

Dataset de chistes en inglés

Uso: Fine-tuning de modelos de generación de texto humorístico

231K filas 2 columnas 45 MB
Ver en HuggingFace →

opus100

HuggingFace

Dataset con traducciones de inglés a español

Uso: Entrenamiento de modelos de traducción inglés-español

1M filas 2 columnas 210 MB
Ver en HuggingFace →

netflix_titles

HuggingFace

Dataset con películas y series de Netflix

Uso: Análisis de catálogo de Netflix y sistemas de recomendación

8.8K filas 12 columnas 3.5 MB
Ver en HuggingFace →
Ver más datasets -->