Transformers documentation

Como o cache funciona

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Como o cache funciona

Imagine que você está tendo uma conversa com alguém e, em vez de lembrar do que a pessoa disse anteriormente, ela precisa recomeçar do zero a cada vez que você responde. Isso seria lento e ineficiente, certo?

Você pode estender essa analogia para modelos transformer. A geração em modelos autoregressivos pode ser lenta porque faz uma previsão um token de cada vez. Cada nova previsão depende de todo o contexto anterior.

Para prever o 1000º token, o modelo requer informações dos 999 tokens anteriores. Essas informações são representadas como multiplicações de matrizes entre as representações dos tokens.

Para prever o 1001º token, você precisa das mesmas informações dos 999 tokens anteriores, além de qualquer informação do 1000º token. Isso representa muitas multiplicações de matriz que o modelo precisa calcular repetidamente para cada token!

Um cache de chave-valor (KV) elimina essa ineficiência armazenando pares de kv derivados das camadas de atenção dos tokens processados anteriormente. Os pares de kv armazenados são recuperados do cache e reutilizados para os tokens subsequentes, evitando a necessidade de recálculo.

O uso de cache deve ser restrito à inferência. Ele pode causar erros inesperados se estiver habilitado durante o treinamento.

Para entender melhor como e por que o cache funciona, vamos examinar de perto a estrutura das matrizes de atenção.

Matrizes de atenção

O atento de produto escalar com escala é calculado conforme mostrado abaixo para um lote de tamanho b, número de cabeças de atenção h, comprimento da sequência até o momento T, e dimensão por cabeça de atenção d_head. Attention(Q,K,V)=softmax(QK⊤dhead×mask)V \text{Attention}(Q, K, V) = \text{softmax}\left( \frac{Q K^\top}{\sqrt{d_{\text{head}}}} \times \text{mask} \right) V

As matrizes de consulta (Q), chave (K) e valor (V) são projeções dos embeddings de entrada, com forma (b, h, T, d_head).

Para a atenção causal, a máscara impede que o modelo preste atenção a tokens futuros. Uma vez que um token é processado, sua representação nunca muda em relação aos tokens futuros, o que significa que Kpast K_{\text{past}} e Vpast V_{\text{past}} podem ser armazenados em cache e reutilizados para calcular a representação do último token. Attention(qt,[k1,k2,…,kt−1⏟cached,kt],[v1,v2,…,vt−1⏟cached,vt]) \text{Attention}(q_t, [\underbrace{k_1, k_2, \dots, k_{t-1}}_{\text{cached}}, k_{t}], [\underbrace{v_1, v_2, \dots, v_{t-1}}_{\text{cached}}, v_{t}])

No momento da inferência, você só precisa da query do último token para calcular a representação xt x_t que prevê o próximo token $ t+1 $. A cada etapa, os novos vetores de chave e valor são armazenados no cache e adicionados às chaves e valores passados. Kcache←concat(Kpast,kt),Vcache←concat(Vpast,vt) K_{\text{cache}} \leftarrow \text{concat}(K_{\text{past}}, k_t), \quad V_{\text{cache}} \leftarrow \text{concat}(V_{\text{past}}, v_t)

A atenção é calculada de forma independente em cada camada do modelo, e o cache é feito por camada.

Consulte a tabela abaixo para comparar como o cache melhora a eficiência.

sem cachecom cache
para cada etapa, recalcular todos os K e V anteriorespara cada etapa, calcule apenas o K e o V atuais
o custo de atenção por passo é quadrático em relação ao comprimento da sequênciao custo de atenção por passo é linear em relação ao comprimento da sequência (a memória cresce linearmente, mas o custo de computação por token permanece baixo)

Classe de cache

Uma interface básica de cache de KV recebe um tensor de chave e um tensor de valor para o token atual e retorna os tensors K e V atualizados. Isso é gerenciado internamente pelo método forward de um modelo.

new_K, new_V = cache.update(k_t, v_t, layer_idx)
attn_output = attn_layer_idx_fn(q_t, new_K, new_V)

Ao usar a classe Cache do Transformers, o módulo de autoatenção executa várias etapas críticas para integrar informações passadas e atuais.

  1. O módulo de atenção concatena os pares kv atuais com os pares kv anteriores armazenados no cache. Isso cria pesos de atenção com a forma (new_tokens_length, past_kv_length + new_tokens_length). Os pares kv atuais e anteriores são essencialmente combinados para calcular as pontuações de atenção, garantindo que o modelo esteja ciente do contexto anterior e da entrada atual.

  2. Quando o método forward é chamado iterativamente, é crucial que a forma da attention mask corresponda ao comprimento combinado dos pares kv passados e atuais. A attention mask deve ter a forma (batch_size, past_kv_length + new_tokens_length). Isso geralmente é tratado internamente em generate(), mas, se você quiser implementar seu próprio loop de geração com Cache, tenha isso em mente! A attention mask deve conter os valores dos tokens passados e atuais.

Implementação de armazenamento em cache

Os caches são estruturados como uma lista de camadas, em que cada camada contém um cache de chave e um cache de valor. Os caches de chave e de valor são tensores com a forma [batch_size, num_heads, seq_len, head_dim].

As camadas podem ser de diferentes tipos (por exemplo, DynamicLayer, StaticLayer, StaticSlidingWindowLayer), o que altera principalmente o modo como o comprimento da sequência é tratado e como o cache é atualizado.

A mais simples é uma DynamicLayer que cresce à medida que mais tokens são processados. A dimensão do comprimento da sequência (seq_len) aumenta a cada novo token:

cache.layers[idx].keys = torch.cat([cache.layers[idx].keys, key_states], dim=-2)
cache.layers[idx].values = torch.cat([cache.layers[idx].values, value_states], dim=-2)

Outros tipos de camada, como StaticLayer e StaticSlidingWindowLayer, possuem um comprimento de sequência fixo que é definido quando o cache é criado. Isso os torna compatíveis com torch.compile. No caso de StaticSlidingWindowLayer, os tokens existentes são deslocados para fora do cache quando um novo token é adicionado.

O exemplo abaixo demonstra como criar um loop de geração com DynamicCache. Conforme discutido, a máscara de atenção é uma concatenação dos valores de tokens passados e atuais.

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, DynamicCache
from accelerate import Accelerator

device = Accelerator().device

model_id = "meta-llama/Llama-2-7b-chat-hf"
model = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.bfloat16, device_map=device)
tokenizer = AutoTokenizer.from_pretrained(model_id)

past_key_values = DynamicCache(config=model.config)
messages = [{"role": "user", "content": "Hello, what's your name."}]
inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt", return_dict=True).to(model.device)

generated_ids = inputs.input_ids
max_new_tokens = 10

for _ in range(max_new_tokens):
    outputs = model(**inputs, past_key_values=past_key_values, use_cache=True)
    # Greedily sample one next token
    next_token_ids = outputs.logits[:, -1:].argmax(-1)
    generated_ids = torch.cat([generated_ids, next_token_ids], dim=-1)
    # Prepare inputs for the next generation step by leaving unprocessed tokens, in our case we have only one new token
    # and expanding attn mask for the new token, as explained above
    attention_mask = inputs["attention_mask"]
    attention_mask = torch.cat([attention_mask, attention_mask.new_ones((attention_mask.shape[0], 1))], dim=-1)
    inputs = {"input_ids": next_token_ids, "attention_mask": attention_mask}

print(tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0])
"[INST] Hello, what's your name. [/INST]  Hello! My name is LLaMA,"
Update on GitHub