llm.int8() - 8-bit Matrix Multiplication for Transformers at Scale
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..
Na postagem LLMs quantization, explicamos a importância da quantização dos LLMs para economizar memória. Também explicamos que há uma forma de quantização que é a quantização de ponto zero, que consiste em transformar os valores dos parâmetros dos pesos linearmente, mas isso tem o problema da degradação dos modelos de linguagem a partir do momento em que eles ultrapassam 2,7 bilhões de parâmetros.
Quantização de vetores
Como a quantização de todos os parâmetros dos modelos produz erros nos modelos de idiomas grandes, o que eles propõem no artigo llm.int8() é realizar a quantização de vetores, ou seja, separar as matrizes dos pesos em vetores, de modo que alguns desses vetores possam ser quantizados em 8 bits, enquanto outros não. Assim, aqueles que podem ser quantizados em 8 bits são quantizados e as multiplicações de matriz são realizadas no formato INT8, enquanto os vetores que não podem ser quantizados são mantidos no formato FP16 e as multiplicações são realizadas no formato FP16.
Vamos dar uma olhada em um exemplo
Suponha que tenhamos a matriz
e queremos multiplicá-lo pela matriz
Definimos um valor limite e todas as colunas da primeira matriz que têm um valor maior do que esse limite são deixadas no formato FP16; as linhas equivalentes às linhas da primeira matriz na segunda matriz também são deixadas no formato FP16.
Explicando melhor, como a segunda e a quarta colunas da primeira matriz (colunas amarelas) têm valores maiores que um determinado limite, a segunda e a quarta linhas da segunda matriz (linhas amarelas) são deixadas no formato FP16.
No caso de haver valores limiares na segunda matriz, o mesmo seria feito, por exemplo, se na segunda matriz uma linha tivesse um valor maior que um limiar, ela seria deixada no formato FP16, e essa coluna na primeira matriz seria deixada no formato FP16.
As linhas e colunas restantes que não são deixadas no formato FP16 são quantizadas em 8 bits e as multiplicações são realizadas no formato INT8.
Portanto, dividimos a primeira matriz em duas matrizes
E a segunda matriz nas duas matrizes
Multiplicamos as matrizes em INT8 em um lado
e aqueles no formato FP16, por outro lado
Como pode ser visto, a multiplicação das matrizes no formato INT8 nos dá uma matriz de tamanho 3x2, e a multiplicação das matrizes no formato FP16 nos dá outra matriz de tamanho 3x2, de modo que, se as somarmos
É interessante notar que ele apresenta o mesmo resultado como se tivéssemos multiplicado as matrizes originais.
Para ver por que isso acontece, se desenvolvermos o produto vetorial das duas matrizes originais
Vemos que a separação que fizemos não traz problemas.
Portanto, podemos concluir que é possível separar linhas e colunas de matrizes para realizar multiplicações de matrizes. Essa separação será feita quando qualquer elemento da linha ou coluna for maior que um valor limite, de modo que as linhas ou colunas que não tiverem um valor maior que esse limite serão codificadas em INT8, ocupando apenas um byte, e as linhas ou colunas que tiverem um elemento maior que esse limite serão passadas para FP16, ocupando 2 bytes. Dessa forma, não teremos problemas de arredondamento, pois os cálculos que fizermos em INT8 serão feitos com valores que não ultrapassem o intervalo de 8 bits.
Valor limiar α
Como dissemos, vamos separar em linhas e colunas que tenham algum elemento maior que um valor limite, mas qual valor limite devemos escolher? Os autores do artigo fizeram experimentos com vários valores e determinaram que o valor limite deveria ser α=6. Acima desse valor, eles começaram a obter degradações nos modelos de linguagem.
Uso de llm.int8()
Vamos ver como quantificar um modelo com llm.int8() com a biblioteca de transformadores. Para fazer isso, você precisa ter o bitsandbytes
instalado.
pip install bitsandbytes
```
Carregamos um modelo de parâmetro 1B duas vezes, uma vez da maneira normal e a segunda vez quantificando-o com llm.int8().
from transformers import AutoModelForCausalLM, AutoTokenizerimport torchdevice = 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)
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 a quantidade de memória que cada um dos modelos ocupa
model.get_memory_footprint()/(1024**3), model_8bit.get_memory_footprint()/(1024**3)
(4.098002195358276, 1.1466586589813232)
Como pode ser visto, o modelo quantizado ocupa muito menos memória.
Vamos agora fazer um teste de geração de texto com os dois modelos.
input_tokens = tokenizer("Hello my name is Maximo and I am a Machine Learning Engineer", return_tensors="pt").to(device)input_tokens.input_ids
tensor([[ 1, 15043, 590, 1024, 338, 5918, 4200, 322, 306, 626,263, 6189, 29257, 10863, 261]], device='cuda:0')
Vemos o resultado com o modelo normal
import timet0 = 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(time.time() - t0)
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]. I1.7616662979125977
E agora com o modelo quantizado
t0 = time.time()max_new_tokens = 50outputs = 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)
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]. I9.100712776184082
Vemos duas coisas: por um lado, obtemos o mesmo texto na saída, portanto, com um modelo muito menor, podemos obter a mesma saída; no entanto, o modelo quantizado leva muito mais tempo para ser executado, portanto, se você precisar usar esse modelo em tempo real, isso não seria aconselhável.
Isso é contraditório, porque poderíamos pensar que um modelo menor teria que ser executado mais rapidamente, mas temos que pensar que, na realidade, os dois modelos, o normal e o quantizado, executam as mesmas operações, apenas um executa todas as operações em FP32 e o outro as executa em INT8 e FP16, mas o modelo quantizado tem que procurar linhas e colunas com valores maiores que o valor limite, separá-las, executar as operações em INT8 e FP16 e, em seguida, juntar os resultados novamente, de modo que o modelo quantizado leva mais tempo para ser executado.