Transformers documentation
캐시 전략
캐시 전략
Key-value(KV) 벡터는 어텐션 점수를 계산하는 데 사용됩니다. 자기 회귀 모델의 경우, 모델이 한 번에 하나의 토큰만 예측하기 때문에 매번 KV 점수가 계산됩니다. 각 예측은 이전 토큰에 의존하므로, 모델은 매번 동일한 계산을 수행합니다.
KV cache는 이러한 계산 결과를 저장하여 다시 계산하지 않고 재사용할 수 있게 합니다. 캐싱의 효율성은 계산 시간을 줄이고 응답 속도를 향상시켜 모델 성능을 최적화하는 데 필수적입니다. 캐시가 작동하는 방식에 대한 자세한 설명은 Caching 문서를 참고하세요.
Transformers는 다양한 캐싱 메커니즘을 구현한 여러 Cache 클래스를 제공합니다. 이 Cache 클래스 중 일부는 메모리 절약을 최적화하도록 설계된 반면, 다른 일부는 생성 속도를 극대화하도록 설계되었습니다. 아래 표를 참고하여 캐시 유형을 비교하고, 사용 사례에 가장 적합한 캐시를 선택하는 데 활용하세요.
| 캐시 유형 | 슬라이딩 레이어 지원 | 오프로딩 지원 | torch.compile() 지원 | 예상 메모리 사용량 |
|---|---|---|---|---|
| Dynamic Cache | 예 | 예 | 아니요 | 중간 |
| Static Cache | 예 | 예 | 예 | 높음 |
| 양자화 캐시 | 아니요 | 아니요 | 아니요 | 낮음 |
이 가이드에서는 다양한 Cache 클래스를 소개하고, 생성 과정에서 이를 사용하는 방법을 보여줍니다.
기본 캐시
DynamicCache는 모든 모델의 기본 캐시 클래스입니다. 생성이 진행됨에 따라 키와 값의 수가 증가하는 것을 저장하기 위해 캐시 크기가 동적으로 확장되도록 허용합니다.
슬라이딩 윈도우 어텐션(Mistral, Gemma2,…) 또는 청크드 어텐션(Llama4)을 사용하는 모델의 경우, 이러한 유형의 어텐션을 사용하는 레이어가 최대 크기(슬라이딩 윈도우 또는 청크 크기)에 도달하면 캐시의 크기가 더 이상 증가하지 않습니다.
generate()에서 use_cache=False를 설정하여 캐시를 비활성화합니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", dtype=torch.float16, device_map="auto")
inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)
model.generate(**inputs, do_sample=False, max_new_tokens=20, use_cache=False)Cache 클래스는 먼저 초기화한 후, 모델의 past_key_values 매개변수에 전달하여 사용할 수도 있습니다. 이는 더 세밀한 제어나 컨텍스트 캐싱과 같은 고급 사용 사례에 유용할 수 있습니다.
대부분의 경우, cache_implementation 매개변수에 캐시 전략을 정의하는 것이 더 쉽습니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, DynamicCache
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", dtype=torch.float16, device_map="auto")
inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)
past_key_values = DynamicCache(config=model.config)
out = model.generate(**inputs, do_sample=False, max_new_tokens=20, past_key_values=past_key_values)고정 크기 캐시
기본 DynamicCache는 캐시 크기가 고정되어 있지 않아 대부분의 just-in-time (JIT) 최적화를 활용할 수 없게 합니다. JIT 최적화는 메모리 사용량을 대가로 지연 시간을 최소화할 수 있게 합니다. 다음에 나열된 모든 캐시 유형은 생성을 가속화하기 위한 torch.compile과 같은 JIT 최적화와 호환됩니다.
고정 크기 캐시(StaticCache)는 kv 쌍에 대해 특정 최대 캐시 크기를 사전에 할당합니다. 캐시를 수정할 필요 없이 최대 캐시 크기까지 생성할 수 있습니다. 그러나 key/value 상태에 고정된(보통 큰) 크기를 사용하면, 생성 과정에서 실제로는 어텐션에 참여하지 않아야 하는 많은 토큰이 마스킹됩니다. 따라서 이 기법은 디코딩 단계를 compile하는 것을 쉽게 만들어 주지만, 어텐션 계산에서 토큰 낭비를 초래합니다. 모든 것과 마찬가지로 이는 트레이드오프이며, 길이가 대략 비슷한 여러 시퀀스로 생성하는 경우 매우 유리할 수 있지만, 예를 들어 매우 큰 시퀀스 하나와 짧은 시퀀스들만 있는 경우(고정 캐시 크기가 크므로 짧은 시퀀스에서 많은 부분이 낭비됨)에는 최적이지 않을 수 있습니다. 이 기능을 사용한다면 그 영향을 반드시 이해하시기 바랍니다!
DynamicCache의 경우, 슬라이딩 윈도우 어텐션(Mistral, Gemma2,…) 또는 청크드 어텐션(Llama4)을 사용하는 모델에서는 최대 길이가 더 크더라도 이러한 유형의 어텐션을 사용하는 레이어에서 캐시는 절대 슬라이딩 윈도우/청크 크기보다 커지지 않습니다.
generate()에서 cache_implementation="static"을 설정하여 StaticCache를 활성화할 수 있습니다. 이를 통해 greedy 및 sample 디코딩 전략의 디코딩 단계에 대한 자동 compilation도 켜집니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", dtype=torch.float16, device_map="auto")
inputs = tokenizer("Hello, my name is", return_tensors="pt").to(model.device)
out = model.generate(**inputs, do_sample=False, max_new_tokens=20, cache_implementation="static")
tokenizer.batch_decode(out, skip_special_tokens=True)[0]
"Hello, my name is [Your Name], and I am a [Your Profession] with [Number of Years] of"생성 텐서를 CPU에 유지
Neuron, TPU와 같은 Compiler backends는 모델을 고정된 computation graph로 trace합니다. Generation loop는 매 단계마다 토큰 하나씩 커지는 텐서(출력 시퀀스, attention_mask, position_ids)를 유지합니다. Accelerator에서 이러한 텐서의 shape가 변경될 때마다 컴파일러가 graph를 다시 trace하므로 generation이 느려집니다.
generate()는 각 forward 호출 직전에 forward가 소비하는 텐서만 모델 장치로 이동합니다. 출력은 입력의 장치와 일치하도록 다시 이동됩니다. 루프에서 증가하는 텐서 관리 작업을 가속기에서 제외하려면 입력을 CPU로 전달하세요. 컴파일된 그래프는 안정적으로 유지되며, 출력이 입력의 장치를 따르기 때문에 생성된 출력도 CPU에 유지됩니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B", device_map="auto")
# leave the inputs on CPU instead of calling .to(model.device).
inputs = tokenizer("The French Bread Law states", return_tensors="pt")
# generate runs forward on the model device but returns the output on the input_ids device.
output = model.generate(**inputs, do_sample=False, max_new_tokens=20)
print(output.device)
cpuCache 오프로딩
KV 캐시는 상당한 메모리 공간을 차지할 수 있으며, 긴 컨텍스트 생성 시 병목이 될 수 있습니다. 메모리 효율적인 캐시는 속도를 희생하여 메모리 사용량을 줄이는 데 초점을 맞춥니다. 이는 대규모 언어 모델(LLM)을 사용할 때 또는 하드웨어의 메모리가 제한적인 경우 특히 중요합니다.
캐시 오프로딩은 모델 레이어 중 하나를 제외한 KV 캐시를 CPU로 이동시켜 GPU 메모리를 절약합니다. 모델의 forward가 레이어를 순회하는 동안 GPU에는 현재 레이어의 캐시만 유지됩니다. 다음 레이어의 캐시를 비동기로 사전 가져오고, 어텐션 계산이 완료된 후 현재 레이어의 캐시를 CPU로 반환합니다.
You may want to consider offloading if you have a small GPU and you’re getting out-of-memory (OOM) errors.
모델 및 생성 옵션(컨텍스트 크기, 생성 토큰 수, 빔 수 등)에 따라 온디바이스 전체 캐시 대비 생성 처리량이 약간 저하될 수 있습니다. 이는 키/밸류 상태를 오가며 이동하는 데 일부 작업이 필요하기 때문입니다.
Offloading은 DynamicCache와 StaticCache 모두에서 사용할 수 있습니다. 동적 버전의 경우 cache_implementation="offloaded", 정적 버전의 경우 cache_implementation="offloaded_static"을 GenerationConfig 또는 generate()에서 설정하여 활성화할 수 있습니다.
또한, offloading=True 옵션으로 DynamicCache 또는 StaticCache를 직접 인스턴스화한 뒤, 이 캐시를 generate 또는 모델의 forward에 전달할 수도 있습니다(예: 동적 캐시의 경우 past_key_values=DynamicCache(config=model.config, offloading=True)).
위에서 언급한 두 Cache 클래스를 직접 인스턴스화할 때 offload_only_non_sliding이라는 추가 옵션이 있습니다.
이 추가 인수는 슬라이딩 윈도우/청크 어텐션을 사용하는 레이어(있는 경우)도 오프로드할지 여부를 결정합니다. 이러한 레이어는 보통 길이가 짧으므로, 오프로딩이 속도 저하를 유발할 수 있어 오프로딩을 피하는 것이 더 나은 경우가 있습니다. 기본적으로 이 옵션은 DynamicCache의 경우 False, StaticCache의 경우 True입니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
ckpt = "microsoft/Phi-3-mini-4k-instruct"
tokenizer = AutoTokenizer.from_pretrained(ckpt)
model = AutoModelForCausalLM.from_pretrained(ckpt, dtype=torch.float16, device_map="auto")
inputs = tokenizer("Fun fact: The shortest", return_tensors="pt").to(model.device)
out = model.generate(**inputs, do_sample=False, max_new_tokens=23, cache_implementation="offloaded")
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
Fun fact: The shortest war in history was between Britain and Zanzibar on August 27, 1896.아래 예제는 메모리가 부족할 때 오프로드된 캐시로 폴백하는 방법을 보여줍니다:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from accelerate import Accelerator
def resilient_generate(model, *args, **kwargs):
oom = False
device = Accelerator().device
torch_device_module = getattr(torch, device, torch.cuda)
try:
return model.generate(*args, **kwargs)
except torch.OutOfMemoryError as e:
print(e)
print("retrying with cache_implementation='offloaded'")
oom = True
if oom:
torch_device_module.empty_cache()
kwargs["cache_implementation"] = "offloaded"
return model.generate(*args, **kwargs)
ckpt = "microsoft/Phi-3-mini-4k-instruct"
tokenizer = AutoTokenizer.from_pretrained(ckpt)
model = AutoModelForCausalLM.from_pretrained(ckpt, dtype=torch.float16, device_map="auto")
prompt = ["okay "*1000 + "Fun fact: The most"]
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
beams = { "num_beams": 40, "num_return_sequences": 20, "max_new_tokens": 23, "early_stopping": True, }
out = resilient_generate(model, **inputs, **beams)
responses = tokenizer.batch_decode(out[:,-28:], skip_special_tokens=True)양자화 캐시
QuantizedCache는 KV 값을 더 낮은 정밀도로 양자화하여 메모리 요구량을 줄입니다. QuantizedCache는 현재 세 가지 양자화 백엔드를 지원합니다:
hqq는 int1, int2, int3, int4, int8 데이터 유형을 지원합니다.quanto는 int2 및 int4 데이터형을 지원합니다. 이는 기본 양자화 백엔드입니다.fp8은 KV 값을 8비트 부동 소수점(float8_e4m3fn)으로 저장합니다. 추가적인 의존성이 필요하지 않으며, 그룹별이 아닌 텐서별로 양자화하므로nbits,axis_key,axis_value,q_group_size,residual_length를 거부합니다.
컨텍스트 길이가 짧고 캐시 양자화 없이 생성에 충분한 GPU 메모리가 사용 가능한 경우, 캐시를 양자화하면 지연 시간이 증가할 수 있습니다. 메모리 효율성과 지연 시간 사이의 균형을 찾으시기 바랍니다.
GenerationConfig에서 cache_implementation="quantized"를 설정하여 QuantizedCache를 활성화합니다. 양자화 백엔드와 추가 양자화 매개변수는 generate()에 전달되는 cache_config 딕셔너리에 지정합니다. 메모리 부족(out-of-memory) 문제가 발생하는 경우를 제외하고는 이러한 추가 매개변수에 기본값을 사용해야 합니다. 메모리 부족 문제가 발생하면 residual length를 줄이는 것을 고려해 보세요.
hqq 백엔드의 경우, axis_key 및 axis_value 매개변수를 1으로 설정하는 것을 권장합니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", dtype=torch.float16, device_map="auto")
inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)
out = model.generate(
**inputs,
do_sample=False,
max_new_tokens=20,
cache_implementation="quantized",
cache_config={
"nbits": 4,
"backend": "hqq",
"axis_key": 1,
"axis_value": 1
}
)
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
I like rock music because it's loud and energetic. It's a great way to express myself and relquanto 백엔드의 경우, axis_key 및 axis_value 매개변수를 0으로 설정하는 것을 권장합니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf", dtype=torch.float16, device_map="auto")
inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)
out = model.generate(
**inputs,
do_sample=False,
max_new_tokens=20,
cache_implementation="quantized",
cache_config={
"nbits": 4,
"backend": "quanto",
"axis_key": 0,
"axis_value": 0
}
)
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
I like rock music because it's loud and energetic. It's a great way to express myself and relfp8 백엔드는 추가 매개변수를 받지 않으며, 그룹 단위 매개변수가 전달되면 예외를 발생시킵니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B-Instruct-2507")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-4B-Instruct-2507", dtype=torch.float16, device_map="auto")
inputs = tokenizer("I like rock music because", return_tensors="pt").to(model.device)
out = model.generate(**inputs, do_sample=False, max_new_tokens=20, cache_implementation="quantized", cache_config={"backend": "fp8"})
print(tokenizer.batch_decode(out, skip_special_tokens=True)[0])
I like rock music because it is powerful and emotional. I also like classical music because it is elegant and sophisticated. I enjoy인코더-디코더 캐시
EncoderDecoderCache는 인코더-디코더 모델용으로 설계되었습니다. 이전 kv 쌍의 저장 및 검색을 보장하기 위해 셀프 어텐션과 크로스 어텐션 캐시를 모두 관리합니다. 인코더와 디코더에 서로 다른 캐시 유형을 개별적으로 설정할 수 있습니다.
이 캐시 유형은 설정이 필요하지 않습니다. 위에서 설명한 2개의 Cache를 감싸는 단순 래퍼이며, 모델이 독립적으로 직접 사용합니다.
모델별 캐시
일부 모델은 다른 캐시 클래스와 호환되지 않는 고유한 방식으로 과거 kv 쌍 또는 상태를 저장합니다.
Mamba와 같은 Mamba 모델은 어텐션 메커니즘이나 kv 상태가 없기 때문에 특정 캐시가 필요합니다. 따라서 위의 Cache 클래스와 호환되지 않습니다.
반복적 생성
캐시는 모델과의 왕복 상호작용이 있는 반복 생성 설정(챗봇)에서도 작동할 수 있습니다. 일반 생성과 마찬가지로, 캐시를 사용한 반복 생성은 모델이 각 단계마다 전체 컨텍스트를 다시 계산하지 않고도 진행 중인 대화를 효율적으로 처리할 수 있게 합니다.
캐시를 사용한 반복 생성의 경우, 먼저 빈 캐시 클래스를 초기화한 후 새로운 프롬프트를 입력할 수 있습니다. chat template를 사용하여 대화 이력을 추적하세요.
다음 예제는 Llama-2-7b-chat-hf를 보여줍니다. 다른 대화형 모델을 사용하는 경우 apply_chat_template()이 메시지를 다르게 처리할 수 있습니다. Jinja 템플릿 작성 방식에 따라 중요한 토큰이 잘려 나갈 수 있습니다. 멀티모달 대화 모델의 경우 이미지나 오디오를 처리하는 방법은 iterative chatting with cache 가이드를 참고하세요.
예를 들어, 일부 모델은 추론 과정에서 <think> ... </think>와 같은 특수 토큰을 사용합니다. 이러한 토큰은 재인코딩 과정에서 유실될 수 있으며, 이로 인해 인덱싱 문제가 발생할 수 있습니다. 안정성을 유지하려면 completions에서 추가된 토큰을 수동으로 제거하거나 조정해야 할 수 있습니다.
import torch
from transformers import AutoTokenizer,AutoModelForCausalLM, DynamicCache, StaticCache
model_id = "meta-llama/Llama-2-7b-chat-hf"
model = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.bfloat16, device_map='auto')
tokenizer = AutoTokenizer.from_pretrained(model_id)
user_prompts = ["Hello, what's your name?", "Btw, yesterday I was on a rock concert."]
past_key_values = DynamicCache(config=model.config)
messages = []
for prompt in user_prompts:
messages.append({"role": "user", "content": prompt})
inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt", return_dict=True).to(model.device)
input_length = inputs["input_ids"].shape[1]
outputs = model.generate(**inputs, do_sample=False, max_new_tokens=256, past_key_values=past_key_values)
completion = tokenizer.decode(outputs[0, input_length: ], skip_special_tokens=True)
messages.append({"role": "assistant", "content": completion})캐시 자르기
반복 생성 과정에서 후보 연속문이 거부되는 경우와 같이 캐시에서 토큰을 되돌리려면 crop()를 사용합니다. 제거할 토큰 수를 음의 정수로 전달합니다.
# Remove the last three tokens from the cache.
past_key_values.crop(-3)Sliding-window 및 linear-attention 레이어는 진행하면서 과거 상태를 폐기하므로 기본적으로 롤백할 대상이 없습니다. 이러한 레이어가 해당 상태를 유지하도록 하려면 forward pass 전에 activate_past_recording()을 호출하세요. 이를 하지 않으면 sliding-window 레이어가 윈도우를 채우는 시점에, linear-attention 레이어의 경우 즉시 crop이 RuntimeError를 발생시킵니다.
# Enable this before the forward pass if you need to roll back tokens.
past_key_values.activate_past_recording()crop(0)는 캐시를 지우지 않습니다. 캐시된 시퀀스에서 토큰을 제거하지 않으며, full-attention 레이어에 대해서는 no-op입니다. sliding-window 및 linear-attention 레이어의 경우, 다음 모델 호출에서 사용되지 않는 캐시된 정보를 폐기할 수 있습니다. 이를 통해 토큰을 롤백하지 않고도 메모리 사용량을 줄일 수 있습니다.
cached positions
┌───┬───┬───┬───┬───┬───┐
before crop(-3) │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │
└───┴───┴───┴───┴───┴───┘
╰─────┬─────╯
rolled back
after crop(-3)
┌───┬───┬───┐
full attention │ 0 │ 1 │ 2 │ keeps the whole prefix
└───┴───┴───┘
┌───┐
sliding_window=2 │ 2 │ keeps sliding_window - 1
└───┘두 레이어 모두 시퀀스 길이가 3으로 보고되며, 생성은 위치 3에서 재개됩니다.
0을 전달하면 시퀀스에서 토큰이 제거되지 않습니다. full-attention 레이어에는 아무런 효과가 없지만, sliding-window 및 linear-attention 레이어에서는 다음 forward pass에서 읽지 않을 기록된 상태를 폐기하여 롤백 없이 메모리를 해제합니다.
캐시 사전 채우기(접두사 캐싱)
일부 상황에서는 특정 프롬프트 접두사에 대한 kv 쌍으로 Cache를 채운 후 이를 재사용하여 서로 다른 시퀀스를 생성하려는 경우가 있습니다.
아래 예시에서는 StaticCache를 초기화한 후 초기 프롬프트를 캐시에 저장합니다. 이제 미리 채워진 프롬프트에서 여러 시퀀스를 생성할 수 있습니다.
import copy
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, DynamicCache, StaticCache
model_id = "meta-llama/Llama-2-7b-chat-hf"
model = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.bfloat16, device_map={"": 0})
tokenizer = AutoTokenizer.from_pretrained(model_id)
# Init StaticCache with big enough max-length (1024 tokens for the below example)
# You can also init a DynamicCache, if that suits you better
prompt_cache = StaticCache(config=model.config, max_cache_len=1024)
INITIAL_PROMPT = "You are a helpful assistant. "
inputs_initial_prompt = tokenizer(INITIAL_PROMPT, return_tensors="pt").to(model.device.type)
# This is the common prompt cached, we need to run forward without grad to be able to copy
with torch.no_grad():
prompt_cache = model(**inputs_initial_prompt, past_key_values = prompt_cache).past_key_values
prompts = ["Help me to write a blogpost about travelling.", "What is the capital of France?"]
responses = []
for prompt in prompts:
new_inputs = tokenizer(INITIAL_PROMPT + prompt, return_tensors="pt").to(model.device.type)
past_key_values = copy.deepcopy(prompt_cache)
outputs = model.generate(**new_inputs, past_key_values=past_key_values,max_new_tokens=20)
response = tokenizer.batch_decode(outputs)[0]
responses.append(response)
print(responses)