GPTQ: Quantização pós-treinamento precisa para transformadores pré-treinados generativos
No artigo GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers, é exposta a necessidade de criar um método de quantização pós-treinamento que não prejudique a qualidade do modelo. Nesta postagem, vimos o método llm.int8() que quantiza para INT8 alguns vetores das matrizes de peso, desde que nenhum de seus valores exceda um valor limite, o que é bom, mas eles não quantizam todos os pesos do modelo. Neste artigo, eles propõem um método que quantiza todos os pesos do modelo para 4 e 3 bits, sem degradar a qualidade do modelo. Isso economiza bastante memória, não só porque todos os pesos são quantizados, mas também porque isso é feito em 4, 3 bits (e até mesmo 1 e 2 bits sob certas condições), em vez de 8 bits.
Este caderno foi traduzido automaticamente para torná-lo acessível a mais pessoas, por favor me avise se você vir algum erro de digitação..
Trabalhos nos quais se baseia
Quantificação em camadas
Por um lado, eles se baseiam nos trabalhos Nagel et al., 2020
; Wang et al., 2020
; Hubara et al., 2021
e Frantar et al., 2022
, que propõem quantizar os pesos das camadas de uma rede neural para 4 e 3 bits, sem degradar a qualidade do modelo.
Dado um conjunto de dados m
de um conjunto de dados, cada camada l
é alimentada com os dados e a saída dos pesos W
dessa camada é obtida. Portanto, o que você faz é procurar novos pesos quantizados Ŵ
que minimizem o erro quadrático em relação à saída da camada de precisão total.
argmin_Ŵ||WX- ŴX||^2
.
Os valores de Ŵ
são definidos antes da execução do processo de quantização e, durante o processo, cada parâmetro de Ŵ
pode mudar de valor independentemente, sem depender do valor dos outros parâmetros de Ŵ
.
Quantização ideal do cérebro (OBQ)
No trabalho OBQ
de Frantar et al., 2022
, eles otimizam o processo de quantização em camadas acima, tornando-o até três vezes mais rápido. Isso ajuda com modelos grandes, pois a quantização de um modelo grande pode levar muito tempo.
O método OBQ
é uma abordagem para resolver o problema de quantização em camadas em modelos de linguagem. O OBQ
parte da ideia de que o erro quadrático pode ser decomposto na soma de erros individuais para cada linha da matriz de peso. O método quantifica cada peso de forma independente, sempre atualizando os pesos não quantificados para compensar o erro incorrido pela quantização.
O método é capaz de quantificar modelos de tamanho médio em tempos razoáveis, mas, como é um algoritmo de complexidade cúbica, é extremamente caro para ser aplicado a modelos com bilhões de parâmetros.
Algoritmo GPTQ
Etapa 1: informações arbitrárias do pedido
No OBQ
, eles procuraram a linha de pesos que criava o menor erro quadrático médio para quantificar, mas perceberam que fazer isso aleatoriamente não aumentava muito o erro quadrático médio final. Assim, em vez de procurar a linha que minimizasse o erro quadrático médio, o que criava uma complexidade cúbica no algoritmo, isso é feito sempre na mesma ordem. Isso reduz bastante o tempo de execução do algoritmo de quantização.
Etapa 2: atualizações em lote preguiçosas
Como a atualização dos pesos é feita linha por linha, isso torna o processo lento e não utiliza totalmente o hardware. Portanto, eles propõem executar as atualizações em lotes de B=128
linhas. Isso faz melhor uso do hardware e reduz o tempo de execução do algoritmo.
Etapa 3: Reformulação de Cholesky
O problema com as atualizações em lote é que, devido à grande escala dos modelos, podem ocorrer erros numéricos que afetam a precisão do algoritmo. Em particular, matrizes indefinidas podem ser obtidas, fazendo com que o algoritmo atualize os pesos restantes nas direções erradas, resultando em uma quantização muito ruim.
Para resolver isso, os autores do artigo propõem o uso de uma reformulação Cholesky, que é um método numericamente mais estável.
Resultados do GPTQ
Abaixo estão dois gráficos com a medida de perplexidade no conjunto de dados WikiText2
para todos os tamanhos dos modelos OPT e BLOOM. É possível observar que, com a técnica de quantização RTN, a perplexidade em alguns tamanhos aumenta muito, enquanto com o GPTQ ela permanece semelhante à obtida com o modelo FP16.
Outros gráficos são mostrados abaixo, mas com a medida de precisão no conjunto de dados LAMBADA
. É a mesma coisa, enquanto o GPTQ permanece semelhante ao obtido com o FP16, outros métodos de quantização degradam muito a qualidade do modelo.
Quantificação extrema
Os gráficos anteriores mostraram os resultados da quantização do modelo em 3 e 4 bits, mas podemos quantizá-los em 2 bits ou até mesmo em 1 bit.
Modificando o tamanho dos lotes ao usar o algoritmo, podemos obter bons resultados quantificando tanto o modelo quanto os lotes.
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 |
Na tabela acima, você pode ver o resultado da perplexidade no conjunto de dados WikiText2
para os modelos OPT-175B
e BLOOM
quantizados em 3 bits. É possível observar que, à medida que lotes menores são usados, a perplexidade diminui, o que significa que a qualidade do modelo quantizado é melhor. Mas o problema é que o algoritmo leva mais tempo para ser executado.
Desconto dinâmico na inferência
Durante a inferência, algo chamado "dequantização dinâmica" é executado para realizar a inferência. Cada camada é dequantificada à medida que passa por ela.
Para fazer isso, eles desenvolveram um kernel que desquantifica as matrizes e executa produtos de matrizes. Embora a descompactação seja mais intensiva em termos de computação, o kernel precisa acessar muito menos memória, o que resulta em um aumento significativo da velocidade.
A inferência é realizada no FP16, descontando os pesos à medida que você passa pelas camadas, e a função de ativação de cada camada também é realizada no FP16. Embora isso signifique que mais cálculos tenham de ser feitos, porque os pesos têm de ser descontados, esses cálculos tornam o processo geral mais rápido, porque menos dados têm de ser buscados na memória. Os pesos precisam ser obtidos da memória em menos bits, portanto, no final, em matrizes com muitos parâmetros, isso economiza muitos dados. O gargalo geralmente está na obtenção dos dados da memória, portanto, mesmo que você tenha que fazer mais cálculos, no final a inferência é mais rápida.
Velocidade de inferência
Os autores do artigo testaram a quantização do modelo BLOOM-175B para 3 bits, o que ocupou cerca de 63 GB de memória VRAM, incluindo embeddings e a camada de saída que são mantidos em FP16. Além disso, a manutenção da janela de contexto de 2048 tokens consome cerca de 9 GB de memória, em um total de cerca de 72 GB de memória VRAM. Eles quantizaram em 3 bits e não em 4 bits para poder realizar esse experimento e ajustar o modelo em uma única GPU Nvidia A100 com 80 GB de memória VRAM.
Para fins de comparação, a inferência FP16 normal requer cerca de 350 GB de VRAM, o que equivale a 5 GPUs Nvidia A100 com 80 GB de VRAM. E a inferência quantizada de 8 bits usando llm.int8() requer 3 dessas GPUs.
Abaixo está uma tabela com inferência de modelo em FP16 e 3 bits quantizados em GPUs Nvidia A100 com 80 GB de VRAM e GPUs Nvidia A6000 com 48 GB de VRAM.
GPU (VRAM) | Tempo médio por token em FP16 (ms) | Tempo médio por token em 3 bits (ms) | Aceleração | Redução das GPUs necessárias |
---|---|---|---|---|
A6000 (48GB) | 589 | 130 | ×4.53 | 8→ 2 |
A100 (80GB) | 230 | 71 | ×3.24 | 5→ 1 |
Por exemplo, usando os kernels, o modelo OPT-175B de 3 bits é executado em um único A100 (em vez de 5) e é aproximadamente 3,25 vezes mais rápido do que a versão FP16 em termos de tempo médio por token.
A GPU NVIDIA A6000 tem uma largura de banda de memória muito menor, o que torna essa estratégia ainda mais eficaz: a execução do modelo OPT-175B de 3 bits em 2 GPUs A6000 (em vez de 8) é aproximadamente 4,53 vezes mais rápida do que a versão FP16.
Livrarias
Os autores do artigo implementaram a biblioteca GPTQ. Outras bibliotecas foram criadas, como GPTQ-for-LLaMa, exllama e llama.cpp. No entanto, essas bibliotecas se concentram apenas na arquitetura llama, de modo que a biblioteca AutoGPTQ ganhou mais popularidade porque tem uma cobertura mais ampla de arquiteturas.
Por esse motivo, essa biblioteca AutoGPTQ foi integrada por meio de uma API na biblioteca transformers. Para usá-la, é necessário instalá-la conforme indicado na seção Installation de seu repositório e ter a biblioteca optimun instalada.
Além da seção Installation do seu repositório, você também deve fazer o seguinte:
git clone https://github.com/PanQiWei/AutoGPTQ
cd AutoGPTQ
pip install .
```
Para que os kernels de quantização da GPU desenvolvidos pelos autores do artigo sejam instalados.
Quantização de um modelo
Vamos ver como quantificar um modelo com a biblioteca optimun e a API AutoGPTQ.
Inferência de modelo não quantificada
Vamos quantificar o modelo meta-call/Meta-Call-3-8B-Instruct que, como o nome indica, é um modelo de 8B parâmetros, portanto, no FP16, precisaríamos de 16 GB de memória VRAM. Primeiro, executamos o modelo para ver quanta memória ele ocupa e a saída que ele gera
Como para usar esse modelo temos que pedir permissão ao Meta, entramos no HuggingFace para baixar o tokenizador e o modelo.
from huggingface_hub import notebook_loginnotebook_login()
Instanciamos o tokenizador e o 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 ver a quantidade de memória que o FP16 ocupa.
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 ele ocupa quase 15 GB, mais ou menos os 16 GB que dissemos que deveria ocupar, mas por que essa diferença? Certamente esse modelo não tem exatamente 8B de parâmetros, mas tem um pouco menos, mas ao indicar o número de parâmetros, ele é arredondado para 8B.
Fazemos uma inferência para ver como ele faz isso e quanto tempo leva.
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")
Quantização do modelo para 4 bits
Vamos quantificar isso em 4 bits. Reinicio o notebook para evitar problemas de memória, então entramos no Hugging Face novamente.
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()
Primeiro, crio o 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.
Agora criamos a configuração de quantização. Como já dissemos, esse algoritmo calcula o erro dos pesos quantizados em relação aos pesos originais com base nas entradas de um conjunto de dados, portanto, na configuração, temos de informá-lo com qual conjunto de dados queremos quantificar o modelo.
Os padrões disponíveis são wikitext2
, c4
, c4-new
, ptb
e ptb-new
.
Também podemos criar um conjunto de dados a partir de uma lista de cadeias de caracteres.
dataset = ["auto-gptq é uma biblioteca de quantização de modelos fácil de usar com apis amigáveis, baseada no algoritmo GPTQ."].
Além disso, precisamos informar a ele o número de bits que o modelo quantizado tem por meio do parâmetro bits
.
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=4, dataset = "c4", tokenizer=tokenizer)
Quantificamos o 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 o processo de quantização calcula o menor erro entre os pesos quantizados e os pesos originais ao passar as entradas por cada camada, o processo de quantização leva tempo. Nesse caso, levou cerca de meia hora
Vamos dar uma olhada na memória que ele ocupa agora
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
Aqui podemos ver um benefício da quantização. Enquanto o modelo original ocupava cerca de 15 GB de VRAM, agora o modelo quantizado ocupa cerca de 5 GB, quase um terço do tamanho original.
Fazemos a inferência e observamos o tempo que leva
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")
O modelo não quantizado levou 4,14 segundos, enquanto agora quantizado para 4 bits levou 2,34 segundos e também gerou bem o texto. Conseguimos reduzir a inferência em quase metade.
Como o tamanho do modelo quantizado é quase um terço do modelo FP16, poderíamos pensar que a velocidade de inferência deveria ser cerca de três vezes mais rápida com o modelo quantizado. Mas lembre-se de que, em cada camada, os pesos são quantificados e os cálculos são realizados em FP16, portanto, só conseguimos reduzir o tempo de inferência pela metade e não em um terço.
Agora vamos salvar o 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')
E fazemos o upload para o 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)
Também fizemos o upload do tokenizador. Embora não tenhamos alterado o tokenizador, nós o carregamos porque, se alguém fizer download do nosso modelo a partir do hub, não precisará saber qual tokenizador usamos, portanto, provavelmente desejará fazer o download do modelo e do tokenizador juntos. Podemos indicar no cartão do modelo qual tokenizador usamos para fazer o download, mas é muito provável que a pessoa não leia o cartão do modelo, tente fazer o download do tokenizador, receba um erro e não saiba o que fazer. Por isso, fizemos o upload para evitar esse 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)
Quantização do modelo para 3 bits
Vamos quantificar isso em 3 bits. Reinicio o notebook para evitar problemas de memória e faço login novamente no 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()
Primeiro, crio o 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.
Criamos a configuração de quantização e agora indicamos que queremos quantizar para 3 bits.
from transformers import GPTQConfigquantization_config = GPTQConfig(bits=3, dataset = "c4", tokenizer=tokenizer)
Quantificamos o 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")
Como antes, demorou cerca de meia hora.
Vamos dar uma olhada na memória que ele ocupa agora
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
O espaço ocupado pela memória do modelo de 3 bits também é de quase 5 GB. O modelo de 4 bits ocupava 5,34 GB, enquanto o modelo de 3 bits agora ocupa 4,52 GB, portanto, conseguimos reduzir um pouco mais o tamanho do modelo.
Fazemos a inferência e observamos o tempo que leva
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")
Embora a saída de 3 bits seja boa, o tempo de inferência passou a ser de 2,89 segundos, enquanto a saída de 4 bits foi de 2,34 segundos. Mais testes devem ser feitos para verificar se sempre leva menos tempo em 4 bits ou se a diferença é tão pequena que às vezes a inferência de 3 bits é mais rápida e às vezes a inferência de 4 bits é mais rápida.
Além disso, embora o resultado faça sentido, ele começa a se tornar repetitivo.
Salvamos o 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')
E fazemos o upload para o 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)
Também carregamos o 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)
Quantização do modelo para 2 bits
Vamos quantificar isso em 2 bits. Reinicio o notebook para evitar problemas de memória e faço login novamente no 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()
Primeiro, crio o 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.
Criamos a configuração de quantização. Agora dizemos a ele para quantizar em 2 bits. Além disso, temos que indicar quantos vetores da matriz de peso ele quantiza ao mesmo tempo por meio do parâmetro group_size
, antes, por padrão, ele tinha o valor 128 e não mexemos nele, mas agora, ao quantizar para 2 bits, para ter menos erro, colocamos um valor menor. Se o deixarmos em 128, o modelo quantizado funcionará muito mal. Nesse caso, colocarei um 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 isso também levou cerca de meia hora.
Vamos dar uma olhada na memória que ele ocupa agora
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
Enquanto a quantização de 4 bits era de 5,34 GB e a de 3 bits era de 4,52 GB, agora a quantização de 2 bits é de 4,50 GB, portanto, conseguimos reduzir um pouco mais o tamanho do modelo.
Fazemos a inferência e observamos o tempo que leva
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 o resultado já não é bom, além disso, o tempo de inferência é de 2,92 segundos, praticamente o mesmo que com 3 e 4 bits.
Salvamos o 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')
Nós o carregamos no 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)
Quantização do modelo para 1 bit
Vamos quantificar isso em 1 bit. Reinicio o notebook para evitar problemas de memória e faço login novamente no 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()
Primeiro, crio o 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.
Criamos a configuração de quantização, agora dizemos a ela para quantizar em apenas 1 bit e também para usar um 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 também leva cerca de meia hora para quantificar.
Vamos dar uma olhada na memória que ele ocupa agora
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, nesse caso, ele ocupa ainda mais do que quantificado em 2 bits, 4,52 GB.
Fazemos a inferência e observamos o tempo que leva
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 a saída é muito ruim e também demora mais do que quando quantizamos para 2 bits.
Salvamos o 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')
Nós o carregamos no 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)
Resumo da quantificação
Vamos comprar quantização de 4, 3, 2 e 1 bits.
Bits | Tempo de quantização (min) | Memória (GB) | Tempo de inferência (s) | Qualidade da saída |
---|---|---|---|---|
FP16 | 0 | 14.96 | 4.14 | Bom |
4 | 32.20 | 5.34 | 2.34 | Bom |
3 | 31.88 | 4.52 | 2.89 | Bom |
2 | 32.89 | 4.50 | 2.92 | Ruim |
1 | 33.84 | 5.42 | 3.12 | Ruim |
Observando essa tabela, vemos que não faz sentido, neste exemplo, quantificar com menos de 4 bits.
A quantificação em 1 e 2 bits claramente não faz sentido porque a qualidade da saída é ruim.
Mas, embora a saída quando quantizamos para 3 bits seja boa, ela começou a se tornar repetitiva, portanto, a longo prazo, provavelmente não seria uma boa ideia usar esse modelo. Além disso, nem a economia no tempo de quantização, nem a economia de VRAM, nem a economia no tempo de inferência são significativas em comparação com a quantização para 4 bits.
Carregamento do modelo salvo
Agora que comparamos a quantização dos modelos, vamos ver como seria feito para carregar o modelo de 4 bits que salvamos, já que, como vimos, essa é a melhor opção.
Primeiro, carregamos o tokenizador que usamos.
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.
Agora, carregamos o modelo que salvamos
from transformers import AutoModelForCausalLMload_model_4bits = AutoModelForCausalLM.from_pretrained(path, device_map="auto")
Loading checkpoint shards: 100%|██████████| 2/2 [00:00<?, ?it/s]
Vemos a memória que ele 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 ele ocupa a mesma memória de quando o quantificamos, o que é lógico.
Fazemos a inferência e observamos o tempo que leva
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 a inferência é boa e levou 3,82 segundos, um pouco mais do que quando a quantificamos. Mas, como eu disse antes, teríamos que fazer esse teste várias vezes e tirar uma média.
Carregando o modelo carregado para o hub
Agora veremos como carregar o modelo de 4 bits que carregamos no hub.
Primeiro, carregamos o tokenizador que carregamos.
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.
Agora, carregamos o modelo que salvamos
from transformers import AutoModelForCausalLMload_model_4bits = AutoModelForCausalLM.from_pretrained(checkpoint, device_map="auto")
Vemos a memória que ele 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
Ele também ocupa a mesma memória
Fazemos a inferência e observamos o tempo que leva
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 a inferência também é boa e levou 3,81 segundos.