GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers
En el paper GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers se expone la necesidad de crear un método de cuantización post entrenamiento que no degrade la calidad del modelo. En este post hemos visto el método llm.int8() que cuantiza a INT8 algunos vectores de las matrices de pesos, siempre y cuando ninguno de sus valores sobrepase un valor umbral, lo cual está muy bien, pero no cuantizan todos los pesos del modelo. En este paper proponen un método que cuantiza todos los pesos del modelo a 4 y 3 bits, sin degradar la calidad del modelo. Lo que supone un ahorro considerable de memoria, no solo porque se cuantizan todos los pesos, sino porque además se hace a 4, 3 bits (y hasta 1 y 2 bits en ciertas condiciones), en vez de a 8 bits.
Trabajos en los que se basa
Cuantización por capas
Por un lado se basan en los trabajos Nagel et al., 2020
; Wang et al., 2020
; Hubara et al., 2021
y Frantar et al., 2022
, que proponen cuantizar los pesos de las capas de una red neuronal a 4 y 3 bits, sin degradar la calidad del modelo.
Teniendo un conjunto de datos m
proveniente de un dataset, a cada capa l
se le meten los datos y se obtiene la salida de los pesos W
de dicha capa. Así que lo que se hace es buscar unos pesos nuevos Ŵ
cuantizados que minimicen el error cuadrático en relación con la salida de la capa de precisión total
argmin_Ŵ||WX− ŴX||^2
Los valores de Ŵ
se establecen antes de realizar el proceso de cuantización y durante el proceso, cada parámetro de Ŵ
puede cambiar de valor independientemente sin depender del valor de los demás parámetros de Ŵ
.
Optimal brain quantization (OBQ)
En el trabajo de OBQ
de Frantar et al., 2022
optimizan el proceso de cuantización por capas anterior, haciendo que llegue a ser hasta 3 veces más rápido. Esto ayuda con los modelos grandes, ya que cuantizar un modelo grande puede llevar mucho tiempo.
El método OBQ
es un enfoque para resolver el problema de cuantificación por capas en modelos de lenguaje. OBQ
parte de la idea de que el error cuadrático se puede descomponer en la suma de errores individuales para cada fila de la matriz de pesos. Luego, el método cuantifica cada peso de manera independiente, actualizando siempre los pesos no cuantificados para compensar el error incurrido por la cuantificación.
El método es capaz de cuantificar modelos de tamaño mediano en tiempos razonables, pero como es un algoritmo de complejidad cúbica hace que sea extremadamente costoso aplicarlo a modelos con miles de millones de parámetros.
Algoritmo de GPTQ
Paso 1: Información de orden arbitrario
En OBQ
se buscaba la fila de pesos que creaba menor error cuadrático medio para cuantizar, pero se dieron cuenta que al hacerlo de manera aleatoria no aumentaba mucho el error cuadrático medio final. Por lo que en vez de buscar la fila que minimiza el error cuadrático medio, que creaba una complejidad cúbica en el algoritmo, se hace siempre en el mismo orden. Gracias a esto se reduce mucho el tiempo de ejecución del algoritmo de cuantización.
Paso 2: Actualizaciones lazy batch
Como la actualización de los pesos se hace fila a fila, hace que sea un proceso lento y no se aproveche del todo el hardware. Por lo que proponen realizar las actualizaciones en lotes de B=128
filas. De esta manera se aprovecha mejor el hardware y se reduce el tiempo de ejecución del algoritmo.
Paso 3: Reformulación de Cholesky
El problema de hacer las actualizaciones por lotes es que, debido a la gran escala de los modelos, se pueden producir errores numéricos que afectan la precisión del algoritmo. Concretamente, se pueden obtener matrices indefinidas, lo que provoca que el algoritmo actualice los pesos restantes en direcciones incorrectas, lo que resulta en una cuantización muy mala.
Para solucionar esto, los autores del paper proponen utilizar una reformulación de Cholesky, que es un método más numéricamente estable.
Resultados de GPTQ
A continuación se muestran dos gráficas con la medida de la perplejidad (perplexity) en el dataset WikiText2
para todos los tamaños de los modelos OPT y BLOOM. Se puede ver que con la técnica de cuantización RTN, la perplejidad en algunos tamaños aumenta mucho, mientras que con GPTQ se mantiene similar a la que se obtinene con el modelo en FP16
A continuación se muestran otras gráficas, pero con la medida de del accuracy en el dataset LAMBADA
. Ocurre lo mismo, mientras que GPTQ se mantiene similar a lo obtenido con FP16, otros métodos de cuantización degradan mucho la calidad del modelo
Cuantización extrema
En las gráficas anteriores se han mostrado los resultados de cuantizar el modelo a 3 y 4 bits, pero podemos cuantizarlos a 2bits, e incluso a 1 solo bit.
Modificando el tamaño de los batches al utilizar el algoritmo podemos obtener buenos resultados cuantizando tanto el modelo
Modelo | FP16 | g128 | g64 | g32 | 3 bits |
---|---|---|---|---|---|
OPT-175B | 8.34 | 9.58 | 9.18 | 8.94 | 8.68 |
BLOOM | 8.11 | 9.55 | 9.17 | 8.83 | 8.64 |
En la tabla anterior se puede ver el resultado de la perplejidad en el dataset WikiText2
para los modelos OPT-175B
y BLOOM
cuantizados a 3 bits. Se puede ver que a medida que se usen batches más pequeños, la perplejidad disminuye, lo que significa que la calidad del modelo cuantizado es mejor. Pero tiene el problema de que el algoritmo tarda más en ejecutarse
Descuantificación dinámica en la inferencia
Durante la inferencia se raliza algo llamado descuantificación dinámica
(dynamic dequantization
) para poder realizar la inferencia. Se va descuantificando cada capa a medida que se va pasando por ellas.
Para ello desarrollaron un kernel de que descuantifica las matrices y realiza los productos matriciales. Si bien la descuantificación consume más cálculos, el kernel tiene que acceder a mucha menos memoria, lo que genera aceleraciones significativas
La inferencia se realiza en FP16 descuantizando los pesos a medida que se va pasando por las capas y la función de activación de cada capa también se realiza en FP16. Aunque esto hace que haya que hacer más cálculos, porque hay que descuantizar, esos cálculos hacen que el proceso total sea más rápido, porque hay que traer de la memoria menos datos. Hay que traer de la memoria los pesos en menos bits, por lo que al final, en matrices de muchos parámetros hace que se ahorren muchos datos. El cuello de botella normalmente está en traer los datos de la memoria, por lo que aunque haya que hacer más cálculos, al final la inferencia es más rápida
Velocidad de inferencia
Los autores del paper realizaron una prueba cuantizando el modelo BLOOM-175B a 3 bits, lo que ocupaba unos 63 GB de memoria VRAM, incluidos los embeddings y la capa de salida que se mantienen en FP16. Además mantener la ventana de contexto de 2048 tokens consume unos 9 GB de memoria, lo que hace en total unos 72 GB de memoria VRAM. Cuantizaron en 3 bits y no en 4 para poder realizar este experimento y poder meter el modelo en una sola GPU Nvidia A100 de 80 GB de memoria VRAM.
Para comparar, la inferencia normal en FP16 requiere unos 350 GB de memoria VRAM, lo que equivale a 5 GPUs Nvidia A100 de 80 GB de memoria VRAM. Y la inferencia cuantizando en 8 bits mediante llm.int8() requiere 3 de dichas GPUs.
A continuación, se muestra una tabla con la inferencia del modelo en FP16 y cuantizado a 3 bits e GPUs Nvidia A100 de 80 GB de memoria VRAM y Nvidia A6000 de 48 GB de memoria VRAM.
GPU (VRAM) | tiempo promedio por token en FP16 (ms) | tiempo promedio por token en 3 bit (ms) | Aceleración | Reducción de GPUs necesarias |
---|---|---|---|---|
A6000 (48GB) | 589 | 130 | ×4.53 | 8→ 2 |
A100 (80GB) | 230 | 71 | ×3.24 | 5→ 1 |
Por ejemplo, utilizando los kernels, el modelo OPT-175B de 3 bits se ejecuta en una sola A100 (en vez de en 5) y es aproximadamente 3,25 veces más rápido que la versión FP16 en términos de tiempo promedio por token.
La GPU NVIDIA A6000 tiene un ancho de banda de memoria mucho menor, por lo que esta estrategia es aún más efectiva: ejecutar el modelo OPT-175B de 3 bits en 2 GPUs A6000 (en vez de en 8) es aproximádamente 4.53 veces más rápido que la versión FP16.
Librerías
Los autores del paper implementaron la librería GPTQ. Otras librerías fueron creadas como GPTQ-for-LLaMa, exllama y llama.cpp. Sin embargo estas librerías se centran solo en la arquitectura llama, por lo que la librería AutoGPTQ fue la que ganó más popularidad porque tiene una cobertura más amplia de arquitecturas.
Por ello dicha librería AutoGPTQ se integró mediante una API dentro de la librería transformers. Para poder usarla es necesario instalarla como se indica en la seción Installation de su repositorio y tener instalada la librería optimun.
Además hacer lo que indican en la seción Installation de su repositorio también conviene hacer lo siguiente:
git clone https://github.com/PanQiWei/AutoGPTQ
cd AutoGPTQ
pip install .
Para que se instalen los kernels de cuantización en la GPU que desarrollaron los autores del paper.
Cuantización de un modelo
Vamos a ver cómo cuantizar un modelo con la librería optimun y la API de AutoGPTQ.
Inferencia del modelo no cuantizado
Vamos a cuantizar el modelo meta-llama/Meta-Llama-3-8B-Instruct que como su nombre indica es un modelo de 8B de parámetros, por lo que en FP16 necesitaríamos 16 GB de memoria VRAM. Primero ejecutamos el modelo para ver la memoria que ocupa y la salida que genera
Como para usar este modelo hay que pedirle permiso a Meta, nos logueamos en HuggingFace para poder bajar el tokenizador y el modelo
from huggingface_hub import notebook_loginnotebook_login()
Instanciamos el tokenizador y el modelo
from huggingface_hub import notebook_loginnotebook_login()from transformers import AutoModelForCausalLM, AutoTokenizerimport torchdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")checkpoint = "meta-llama/Meta-Llama-3-8B-Instruct"tokenizer = AutoTokenizer.from_pretrained(checkpoint)model = AutoModelForCausalLM.from_pretrained(checkpoint).half().to(device)
Vamos a ver la memoria que ocupa en FP16
from huggingface_hub import notebook_loginnotebook_login()from transformers import AutoModelForCausalLM, AutoTokenizerimport torchdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")checkpoint = "meta-llama/Meta-Llama-3-8B-Instruct"tokenizer = AutoTokenizer.from_pretrained(checkpoint)model = AutoModelForCausalLM.from_pretrained(checkpoint).half().to(device)model_memory = model.get_memory_footprint()/(1024**3)print(f"Model memory: {model_memory:.2f} GB")
Model memory: 14.96 GB
Vemos que ocupa casi 15 GB, más o menos los 16 GB que habíamos dicho que debería ocupar, pero por qué esta diferencia? Seguramente este modelo no tenga exactamente 8B de parámetros, sino que tenga un poco menos, pero a la hora de indicar el número de parámetros se redondea a 8B.
Hacemos una inferencia para ver cómo lo hace y el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model.device)
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(f"Inference time: {time.time() - t0:.2f} s")
Cuantización del modelo a 4 bits
Vamos a cuantizarlo a 4 bits. Reinico el notebook para no tener problemas de memoria, por lo que nos volvemos a loguear en Hugging Face
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model.device)t0 = time.time()max_new_tokens = 50outputs = 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(f"Inference time: {time.time() - t0:.2f} s")from huggingface_hub import notebook_loginnotebook_login()
Primero creo el tokenizador
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model.device)t0 = time.time()max_new_tokens = 50outputs = 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(f"Inference time: {time.time() - t0:.2f} s")from huggingface_hub import notebook_loginnotebook_login()from transformers import AutoTokenizercheckpoint = "meta-llama/Meta-Llama-3-8B-Instruct"tokenizer = AutoTokenizer.from_pretrained(checkpoint)
Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Ahora creamos la configuración de cuantización. Como hemos dicho, este algoritmo calcula el error de los pesos cuantizados sobre los originales en función de entradas de un dataset, por lo que en la configuración tenemos que pasarle con qué dataset queremos cuantizar el modelo.
Los disponibles por defecto son wikitext2
,c4
,c4-new
,ptb
y ptb-new
.
También podemos crear nosotros un dataset a partir de una lista de strings
dataset = ["auto-gptq is an easy-to-use model quantization library with user-friendly apis, based on GPTQ algorithm."]
Además le tenemos que decir el número de bits que tenga el modelo cuantizado mediante el parámetro bits
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=4, dataset = "c4", tokenizer=tokenizer)
Cuantizamos el modelo
from transformers import AutoModelForCausalLM
import time
t0 = time.time()
model_4bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)
t_quantization = time.time() - t0
print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")
Como el proceso de cuantiación calcula el menor error entre los pesos cuantizados con los originales haciéndo pasar entradas por cada capa, el proceso de cuantización tarda. En este caso ha tardado una media hora
Vamos a ver la memoria que ocupa ahora
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=4, dataset = "c4", tokenizer=tokenizer)from transformers import AutoModelForCausalLMimport timet0 = time.time()model_4bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)t_quantization = time.time() - t0print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")model_4bits_memory = model_4bits.get_memory_footprint()/(1024**3)print(f"Model memory: {model_4bits_memory:.2f} GB")
Loading checkpoint shards: 100%|██████████| 4/4 [00:00<?, ?it/s]Model memory: 5.34 GB
Aquí podemos ver un beneficio de la cuantización. Mientras que el modelo original ocupaba unos 15 GB de VRAM, ahora el modelo cuantizado ocupa unos 5 GB, casi un tercio del tamaño original
Hacemos la inferencia y vemos el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_4bits.device)
t0 = time.time()
max_new_tokens = 50
outputs = model_4bits.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(f"Inference time: {time.time() - t0:.2f} s")
El modelo sin cuantizar tardó 4.14 segundos, mientras que ahora cuantizado a 4 bits ha tardado 2.34 segundos y además ha generado bien el texto. Hemos conseguido reducir la inferencia casi a la mitad.
Como el tamaño del modelo cuantizado es casi un tercio del modelo en FP16, podríamos pensar que la velocidad de inferencia debería ser unas tres veces más rápida con el modelo cuantizado. Pero hay que recordar que en cada capa se descuantifican los pesos y se realizan los cálculos en FP16, por eso solo hemos conseguido reducir el tiempo de inferencia a la mitad y no a un tercio
Ahora guardamos el modelo
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_4bits.device)t0 = time.time()max_new_tokens = 50outputs = model_4bits.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(f"Inference time: {time.time() - t0:.2f} s")save_folder = "./model_4bits/"model_4bits.save_pretrained(save_folder)tokenizer.save_pretrained(save_folder)
Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.('./model_4bits/tokenizer_config.json','./model_4bits/special_tokens_map.json','./model_4bits/tokenizer.json')
Y lo subimos al hub
repo_id = "Llama-3-8B-Instruct-GPTQ-4bits"
commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"
model_4bits.push_to_hub(repo_id, commit_message=commit_message)
Subimos también el tokenizador. Aunque no hemos cambiado el tokenizador, lo subimos porque si una persona se baja nuestro modelo del hub no tiene por qué saber qué tokenizador hemos usado, por lo que seguramente querrá bajarse el modelo y el tokenizador juntos. Podemos indicar en la model card qué tokenizador hemos usado para que se lo baje, pero lo más probable es que no se lea la model card, se intente bajar el tokenizador, obtenga un error y no sepa qué hacer. Así que lo subimos para ahorrarnos ese problema
repo_id = "Llama-3-8B-Instruct-GPTQ-4bits"
commit_message = f"Tokenizers for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"
tokenizer.push_to_hub(repo_id, commit_message=commit_message)
Cuantización del modelo a 3 bits
Vamos a cuantizarlo a 3 bits. Reinico el notebook para no tener problemas de memoria y vuelvo a loguearme en Hugging Face
repo_id = "Llama-3-8B-Instruct-GPTQ-4bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_4bits.push_to_hub(repo_id, commit_message=commit_message)repo_id = "Llama-3-8B-Instruct-GPTQ-4bits"commit_message = f"Tokenizers for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"tokenizer.push_to_hub(repo_id, commit_message=commit_message)from huggingface_hub import notebook_loginnotebook_login()
Primero creo el tokenizador
repo_id = "Llama-3-8B-Instruct-GPTQ-4bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_4bits.push_to_hub(repo_id, commit_message=commit_message)repo_id = "Llama-3-8B-Instruct-GPTQ-4bits"commit_message = f"Tokenizers for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"tokenizer.push_to_hub(repo_id, commit_message=commit_message)from huggingface_hub import notebook_loginnotebook_login()from transformers import AutoTokenizercheckpoint = "meta-llama/Meta-Llama-3-8B-Instruct"tokenizer = AutoTokenizer.from_pretrained(checkpoint)
README.md: 100%|██████████| 5.17/5.17k [00:00<?, ?B/s]README.md: 100%|██████████| 0.00/5.17k [00:00<?, ?B/s]Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Creamos la configuración de cuantización, ahora le indicamos que queremos cuantiar a 3 bits
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=3, dataset = "c4", tokenizer=tokenizer)
Cuantizamos el modelo
from transformers import AutoModelForCausalLM
import time
t0 = time.time()
model_3bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)
t_quantization = time.time() - t0
print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")
Al igual que antes ha tardado una media hora
Vamos a ver la memoria que ocupa ahora
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=3, dataset = "c4", tokenizer=tokenizer)from transformers import AutoModelForCausalLMimport timet0 = time.time()model_3bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)t_quantization = time.time() - t0print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")model_3bits_memory = model_3bits.get_memory_footprint()/(1024**3)print(f"Model memory: {model_3bits_memory:.2f} GB")
Loading checkpoint shards: 100%|██████████| 4/4 [00:00<?, ?it/s]Model memory: 4.52 GB
La memoria que ocupa el modelo en 3 bits es también casi unos 5 GB. El modelo en 4 bits ocupaba 5.34 GB, mientras que ahora en 3 bits ocupa 4.52 GB, por lo que hemos conseguido reducir un poco más el tamaño del modelo.
Hacemos la inferencia y vemos el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_3bits.device)
t0 = time.time()
max_new_tokens = 50
outputs = model_3bits.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(f"Inference time: {time.time() - t0:.2f} s")
Aunque la salida en 3 bits es buena, ahora el tiempo de inferencia ha sido de 2.89 segundos, mientras que en 4 bits fue de 2.34 segundos. Habría que hacer más pruebas a ver si siempre tarda menos en 4 bits, o puede que la diferencia sea tan pequeña que a veces sea más rápido la inferencia en 3 bits y otras veces la inferencia en 4 bits.
Además aunque la salida tiene sentido, empieza a ser repetitiva
Guardamos el modelo
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_3bits.device)t0 = time.time()max_new_tokens = 50outputs = model_3bits.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(f"Inference time: {time.time() - t0:.2f} s")save_folder = "./model_3bits/"model_3bits.save_pretrained(save_folder)tokenizer.save_pretrained(save_folder)
Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.('./model_3bits/tokenizer_config.json','./model_3bits/special_tokens_map.json','./model_3bits/tokenizer.json')
Y lo subimos al hub
repo_id = "Llama-3-8B-Instruct-GPTQ-3bits"
commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"
model_3bits.push_to_hub(repo_id, commit_message=commit_message)
Subimos también el tokenizador
repo_id = "Llama-3-8B-Instruct-GPTQ-3bits"
commit_message = f"Tokenizers for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"
tokenizer.push_to_hub(repo_id, commit_message=commit_message)
Cuantización del modelo a 2 bits
Vamos a cuantizarlo a 2 bits. Reinico el notebook para no tener problemas de memoria y me vuelvo a loguear en Hugging Face
repo_id = "Llama-3-8B-Instruct-GPTQ-3bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_3bits.push_to_hub(repo_id, commit_message=commit_message)repo_id = "Llama-3-8B-Instruct-GPTQ-3bits"commit_message = f"Tokenizers for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"tokenizer.push_to_hub(repo_id, commit_message=commit_message)from huggingface_hub import notebook_loginnotebook_login()
Primero creo el tokenizador
repo_id = "Llama-3-8B-Instruct-GPTQ-3bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_3bits.push_to_hub(repo_id, commit_message=commit_message)repo_id = "Llama-3-8B-Instruct-GPTQ-3bits"commit_message = f"Tokenizers for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"tokenizer.push_to_hub(repo_id, commit_message=commit_message)from huggingface_hub import notebook_loginnotebook_login()from transformers import AutoTokenizercheckpoint = "meta-llama/Meta-Llama-3-8B-Instruct"tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model.safetensors: 100%|██████████| 4.85/4.85G [00:00<?, ?B/s]README.md: 100%|██████████| 0.00/5.17k [00:00<?, ?B/s]Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Creamos la configuración de cuantización. Ahora le decimos que cuanticemos a 2 bits. Además hay que indicarle cuantos vectores de la matriz de pesos cuantiza a la vez mediante el parámetro group_size
, antes por defecto tenía el valor 128 y no lo tocamos, pero ahora al cuantizar a 2 bits, para tener menos error le ponemos un valor más pequeño. Si lo dejamos a 128, el modelo cuantizado funcionaría muy mal, en este caso le voy a poner un valor de 16
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=2, dataset = "c4", tokenizer=tokenizer, group_size=16)
from transformers import AutoModelForCausalLM
import time
t0 = time.time()
model_2bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)
t_quantization = time.time() - t0
print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")
Vemos que ha tardado también una media hora
Vamos a ver la memoria que ocupa ahora
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=2, dataset = "c4", tokenizer=tokenizer, group_size=16)from transformers import AutoModelForCausalLMimport timet0 = time.time()model_2bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)t_quantization = time.time() - t0print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")model_2bits_memory = model_2bits.get_memory_footprint()/(1024**3)print(f"Model memory: {model_2bits_memory:.2f} GB")
Loading checkpoint shards: 100%|██████████| 4/4 [00:00<?, ?it/s]Model memory: 4.50 GB
Mientras que cuantizado a 4 bits ocupaba 5.34 GB y a 3 bits ocupaba 4.52 GB, ahora cuantizado a 2 bits ocupa 4.50 GB, por lo que hemos conseguido reducir un poco más el tamaño del modelo
Hacemos la inferencia y vemos el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_2bits.device)
t0 = time.time()
max_new_tokens = 50
outputs = model_2bits.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(f"Inference time: {time.time() - t0:.2f} s")
Vemos que ya la salida no es buena, además el tiempo de inferencia son 2.92 segundos, más o menos lo mismo que con 3 y 4 bits
Guardamos el modelo
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_2bits.device)t0 = time.time()max_new_tokens = 50outputs = model_2bits.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(f"Inference time: {time.time() - t0:.2f} s")save_folder = "./model_2bits/"model_2bits.save_pretrained(save_folder)tokenizer.save_pretrained(save_folder)
Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.('./model_2bits/tokenizer_config.json','./model_2bits/special_tokens_map.json','./model_2bits/tokenizer.json')
Lo subimos al hub
repo_id = "Llama-3-8B-Instruct-GPTQ-2bits"
commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"
model_2bits.push_to_hub(repo_id, commit_message=commit_message)
Cuantización del modelo a 1 bit
Vamos a cuantizarlo a 1 bit. Reinico el notebook para no tener problemas de memoria y me vuelvo a loguear en Hugging Face
repo_id = "Llama-3-8B-Instruct-GPTQ-2bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_2bits.push_to_hub(repo_id, commit_message=commit_message)from huggingface_hub import notebook_loginnotebook_login()
Primero creo el tokenizador
repo_id = "Llama-3-8B-Instruct-GPTQ-2bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_2bits.push_to_hub(repo_id, commit_message=commit_message)from huggingface_hub import notebook_loginnotebook_login()from transformers import AutoTokenizercheckpoint = "meta-llama/Meta-Llama-3-8B-Instruct"tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model.safetensors: 100%|██████████| 4.83/4.83G [00:00<?, ?B/s]Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Creamos la configuración de cuantización, ahora le decimos que cuantice a solo 1 bit y además que use un group_size
de 8
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=2, dataset = "c4", tokenizer=tokenizer, group_size=8)
from transformers import AutoModelForCausalLM
import time
t0 = time.time()
model_1bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)
t_quantization = time.time() - t0
print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")
Vemos que también tarda una media hora en cuantizar
Vamos a ver la memoria que ocupa ahora
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=2, dataset = "c4", tokenizer=tokenizer, group_size=8)from transformers import AutoModelForCausalLMimport timet0 = time.time()model_1bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto", quantization_config=quantization_config)t_quantization = time.time() - t0print(f"Quantization time: {t_quantization:.2f} s = {t_quantization/60:.2f} min")model_1bits_memory = model_1bits.get_memory_footprint()/(1024**3)print(f"Model memory: {model_1bits_memory:.2f} GB")
Loading checkpoint shards: 100%|██████████| 4/4 [00:00<?, ?it/s]Model memory: 5.42 GB
Vemos que en este caso ocupa incluso más que cuantizado a 2 bits, 4.52 GB.
Hacemos la inferencia y vemos el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_1bits.device)
t0 = time.time()
max_new_tokens = 50
outputs = model_1bits.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(f"Inference time: {time.time() - t0:.2f} s")
Vemos que la salida es muy mala y además tarda más que cuando hemos cuantizado a 2 bits
Guardamos el modelo
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(model_1bits.device)t0 = time.time()max_new_tokens = 50outputs = model_1bits.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(f"Inference time: {time.time() - t0:.2f} s")save_folder = "./model_1bits/"model_1bits.save_pretrained(save_folder)tokenizer.save_pretrained(save_folder)
Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.('./model_1bits/tokenizer_config.json','./model_1bits/special_tokens_map.json','./model_1bits/tokenizer.json')
Lo subimos al hub
repo_id = "Llama-3-8B-Instruct-GPTQ-1bits"
commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"
model_1bits.push_to_hub(repo_id, commit_message=commit_message)
Resumen de la cuantización
Vamos a comprar la cuantización a 4, 3, 2 y 1 bits
Bits | Tiempo de cuantización (min) | Memoria (GB) | Tiempo de inferencia (s) | Calidad de la salida |
---|---|---|---|---|
FP16 | 0 | 14.96 | 4.14 | Buena |
4 | 32.20 | 5.34 | 2.34 | Buena |
3 | 31.88 | 4.52 | 2.89 | Buena |
2 | 32.89 | 4.50 | 2.92 | Mala |
1 | 33.84 | 5.42 | 3.12 | Mala |
Viendo esta tabla vemos que no tiene sentido, en este ejemplo, cuantizar a menos de 4 bits.
Cuantizar a 1 y 2 bits claramente no tiene sentido porque la calidad de la salida es mala.
Pero aunque la salida cuando cuantizamos a 3 bits es buena, empezó a ser repetitiva, por lo que a largo plazo, seguramente no sería buena idea usar ese modelo. Además ni el ahorro en tiempo de cuantización, el ahorro en VRAM y el ahorro en tiempo de inferencia es significativo en comparación con cuantizar a 4 bits.
Carga del modelo guardado
Ahora que hemos comparado la cuantización de modelos, vamos a ver cómo se haría para cargar el modelo de 4 bits que hemos guardado, ya que como hemos visto, es la mejor opción
Primero cargamos el tokenizador que hemos usado
repo_id = "Llama-3-8B-Instruct-GPTQ-1bits"commit_message = f"AutoGPTQ model for {checkpoint}: {quantization_config.bits}bits, gr{quantization_config.group_size}, desc_act={quantization_config.desc_act}"model_1bits.push_to_hub(repo_id, commit_message=commit_message)from transformers import AutoTokenizerpath = "./model_4bits"tokenizer = AutoTokenizer.from_pretrained(path)
README.md: 100%|██████████| 0.00/5.17k [00:00<?, ?B/s]Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Ahora cargamos el modelo que hemos guardado
from transformers import AutoModelForCausalLMload_model_4bits = AutoModelForCausalLM.from_pretrained(path, device_map="auto")
Loading checkpoint shards: 100%|██████████| 2/2 [00:00<?, ?it/s]
Vemos la memoria que ocupa
load_model_4bits_memory = load_model_4bits.get_memory_footprint()/(1024**3)print(f"Model memory: {load_model_4bits_memory:.2f} GB")
Model memory: 5.34 GB
Vemos que ocupa la misma memoria que cuando lo habíamos cuantizado, lo cual es lógico
Hacemos la inferencia y vemos el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(load_model_4bits.device)
t0 = time.time()
max_new_tokens = 50
outputs = load_model_4bits.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(f"Inference time: {time.time() - t0:.2f} s")
Vemos que la inferencia es buena y ha tardado 3.82 segundos, un poco más que cuando lo cuantizamos. Pero al igual que he puesto antes, habría que hacer esta prueba muchas veces y sacar una media.
Carga del modelo subido al hub
Ahora vemos cómo cargar el modelo de 4 bits que hemos subido al hub
Primero cargamos el tokenizador que hemos subido
import timeinput_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(load_model_4bits.device)t0 = time.time()max_new_tokens = 50outputs = load_model_4bits.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(f"Inference time: {time.time() - t0:.2f} s")from transformers import AutoTokenizercheckpoint = "Maximofn/Llama-3-8B-Instruct-GPTQ-4bits"tokenizer = AutoTokenizer.from_pretrained(checkpoint)
Setting `pad_token_id` to `eos_token_id`:128001 for open-end generation.Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Ahora cargamos el modelo que hemos guardado
from transformers import AutoModelForCausalLMload_model_4bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto")
Vemos la memoria que ocupa
from transformers import AutoModelForCausalLMload_model_4bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto")load_model_4bits_memory = load_model_4bits.get_memory_footprint()/(1024**3)print(f"Model memory: {load_model_4bits_memory:.2f} GB")
Model memory: 5.34 GB
También ocupa la misma memoria
Hacemos la inferencia y vemos el tiempo que tarda
import time
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(load_model_4bits.device)
t0 = time.time()
max_new_tokens = 50
outputs = load_model_4bits.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(f"Inference time: {time.time() - t0:.2f} s")
Vemos que la inferencia también es buena y ha tardado 3.81 segundos.