Transformers KV 캐시 구현 선택, Dynamic·Static·Offloaded를 추론 조건에 맞추는 법

그래픽 카드와 RAM 모듈이 놓인 작업대

AI를 활용하여 생성한 이미지입니다.

긴 문장을 생성하는 LLM 추론에서는 가중치만 GPU 메모리를 차지하지 않아요. 토큰을 한 개씩 생성할 때 앞서 계산한 key와 value를 캐시에 쌓아 두고 다음 토큰 계산에 재사용합니다. 그래서 컨텍스트가 길어지거나 동시 요청이 늘면 모델 파일은 그대로인데 메모리 사용량이 먼저 불어날 수 있어요.[2]

Hugging Face Transformers에는 상황에 따라 고를 수 있는 여러 캐시 구현이 있어요. 기본값처럼 쓰기 쉬운 DynamicCache, 컴파일과 고정 크기 버퍼에 유리한 StaticCache, GPU 메모리가 부족할 때 CPU로 일부를 넘기는 OffloadedCache를 같은 방식으로 보면 안 됩니다.[1] 선택 기준은 “어떤 캐시가 가장 빠른가”가 아니라 현재 작업이 긴 컨텍스트인지, 반복 실행인지, GPU 메모리가 부족한지에 있어요.

먼저 캐시가 하는 일을 구분하기

Self-attention에서 query, key, value는 입력 토큰에서 만들어지는 행렬이에요. 새 토큰을 생성할 때는 마지막 토큰의 query를 중심으로 계산하고, 새 key와 value를 과거 값에 이어 붙입니다. 이 과거 key·value가 KV 캐시예요. 캐시를 쓰면 매 단계마다 이전 토큰 전체를 다시 계산하지 않아도 되지만, 캐시에 저장할 데이터가 계속 늘어납니다.[2]

이 구조 때문에 두 가지 시간이 달라져요. 처음 프롬프트를 읽는 구간은 프리필이고, 이후 토큰을 생성하는 구간은 디코드예요. 짧은 답변에서는 차이가 작아 보여도 긴 문서나 대화 기록을 반복해서 넣으면 캐시 크기와 메모리 이동이 병목이 될 수 있습니다.

상황 먼저 보는 문제 어울리는 선택
일반적인 단일 생성 구현 복잡도와 메모리 증가 DynamicCache
고정된 최대 길이로 반복 실행 컴파일·고정 버퍼·패딩 비용 StaticCache
GPU 메모리가 부족하지만 CPU 메모리는 여유 있음 GPU와 CPU 사이 이동 비용 OffloadedCache
여러 요청이 같은 접두사를 공유함 캐시 재사용 범위와 요청 스케줄링 서버 프레임워크의 prefix caching 검토

DynamicCache를 기본값으로 보는 이유

DynamicCache는 생성 과정에서 필요한 만큼 캐시를 늘려 가는 방식이에요. 입력 길이나 생성 길이가 매번 달라지는 실험, 대화형 애플리케이션, 모델별로 최대 길이가 다른 환경에서 다루기 편합니다.[1] 먼저 파이프라인을 맞추고 오류 원인을 좁힐 때는 이 방식이 가장 덜 까다로워요.

다만 “동적으로 늘어난다”는 말은 메모리 사용량이 고정되지 않는다는 뜻이기도 해요. 긴 요청이 들어오면 KV 캐시가 커지고, 여러 요청을 동시에 처리하면 각 요청의 캐시가 합쳐져 GPU 여유 공간이 빠르게 줄어듭니다. 모델을 4비트로 줄였는데도 긴 문장에서 CUDA out of memory가 난다면 가중치만 의심하지 말고 캐시와 배치 크기를 함께 보세요.

첫 진단에서는 다음을 기록하면 좋아요.

  1. 입력 토큰 수와 max_new_tokens를 분리해 기록합니다.
  2. 동시 요청 수와 배치 크기를 고정합니다.
  3. 생성 전후의 GPU 메모리와 첫 토큰까지 걸린 시간을 따로 봅니다.
  4. 같은 조건에서 짧은 입력과 긴 입력을 비교합니다.

이렇게 해야 모델 크기 때문에 늘어난 메모리와 컨텍스트 때문에 늘어난 메모리를 구분할 수 있어요.

StaticCache는 속도보다 실행 형태가 먼저예요

StaticCache는 최대 캐시 크기를 미리 정하고 고정된 버퍼를 사용하는 방식이에요. Transformers 문서는 이 구현이 torch.compile 같은 컴파일 경로와 함께 쓰일 수 있다고 설명합니다.[1] 매번 텐서 모양이 바뀌는 동적 캐시보다 컴파일러가 예상하기 쉬운 형태를 만들 수 있다는 뜻이에요.

대신 실제 입력이 최대 길이보다 짧아도 정해 둔 크기와 패딩을 고려해야 합니다. 최대 길이를 크게 잡으면 메모리를 미리 예약하거나 불필요한 계산이 늘 수 있고, 너무 작게 잡으면 긴 요청을 처리하지 못해요. 따라서 StaticCache는 “무조건 더 빠른 캐시”가 아니라 입력 길이와 실행 모양이 어느 정도 안정된 서비스에 맞습니다.

StaticCache를 시험할 때는 단순히 생성 시간 한 번만 비교하지 마세요. 같은 모델·정밀도·입력·생성 토큰 수로 워밍업을 포함한 측정을 여러 번 하고, 컴파일 초기 비용과 두 번째 실행 이후의 시간을 나눠 보세요. 첫 실행이 느려도 반복 실행에서 이득이 생길 수 있고, 반대로 요청 길이가 매번 달라지면 패딩 비용 때문에 차이가 사라질 수 있어요.

OffloadedCache는 메모리와 지연시간의 교환

OffloadedCache는 캐시를 GPU에만 두지 않고 일부를 CPU 메모리로 옮기는 선택지예요. Hugging Face 문서는 GPU 메모리가 부족한 상황에서 캐시 오프로딩을 사용할 수 있도록 예시와 전략을 제공합니다.[1] 모델을 더 작은 양자화로 바꾸기 어려운 상황에서 실행 자체를 가능하게 하는 데 의미가 있어요.

하지만 CPU 메모리를 쓰면 공짜가 아닙니다. 디코드 단계마다 GPU와 CPU 사이에서 필요한 캐시를 옮겨야 하므로 지연시간이 늘 수 있고, PCIe·메모리 대역폭·CPU 부하에 따라 결과가 달라져요. 응답 시간이 중요한 대화형 서비스라면 오프로딩으로 실행된다는 사실만 보고 품질이 좋아졌다고 판단하면 안 됩니다. 먼저 요청당 지연시간과 초당 생성 토큰을 측정하고, 동시 요청이 늘어날 때 CPU 사용량도 확인해야 해요.

오프로딩은 다음과 같은 순서로 판단하면 됩니다.

  • GPU 메모리 부족이 실제 원인인지 확인합니다.
  • 컨텍스트 길이와 동시 요청을 줄였을 때 실행되는지 봅니다.
  • 줄일 수 없는 조건이라면 CPU 메모리 여유와 이동 경로를 확인합니다.
  • 오프로딩 전후의 TTFT와 디코드 속도를 따로 측정합니다.
  • 지연시간이 허용 범위를 넘으면 더 작은 모델, 낮은 컨텍스트, 요청 큐 조정으로 돌아갑니다.

캐시 구현을 바꾸기 전에 확인할 것

캐시 구현 변경은 모델 가중치 교체와 달라서 결과 품질 자체를 바꾸는 설정은 아니지만, 지원되는 모델과 생성 경로에 따라 호환성 문제가 생길 수 있어요. 현재 사용 중인 Transformers 버전과 모델 클래스가 해당 캐시 구현을 받는지 먼저 확인하세요. 문서의 예시를 그대로 복사하기보다 모델이 기대하는 past_key_values 형식과 use_cache 설정을 함께 봐야 합니다.[2]

또한 캐시를 지운다는 말도 두 가지로 나눠야 해요. 한 요청의 과거 토큰 캐시를 비우는 것과, 서버 프로세스에 남은 모델·CUDA 메모리를 정리하는 것은 다른 문제입니다. 전자는 생성 대화의 상태를 초기화하는 작업이고, 후자는 런타임 메모리 관리 문제예요. 둘을 섞으면 캐시를 바꿨는데도 메모리가 줄지 않는 것처럼 보일 수 있습니다.

문제가 생겼을 때의 좁히는 순서

  1. 같은 모델과 정밀도로 캐시 구현만 바꿉니다.
  2. 입력 토큰 수, 생성 토큰 수, 배치 크기를 고정합니다.
  3. DynamicCache로 기준선을 만듭니다.
  4. StaticCache는 반복 실행과 컴파일 후 시간을 별도로 비교합니다.
  5. OffloadedCache는 GPU 메모리와 CPU 사용량, TTFT, 디코드 속도를 함께 기록합니다.
  6. 특정 길이에서만 실패하면 최대 캐시 길이와 패딩 조건을 먼저 확인합니다.
  7. 여러 요청에서만 실패하면 요청별 캐시가 동시에 살아 있는지 확인합니다.

공식 문서에 없는 특정 모델의 메모리 절감률이나 속도 배수는 일반화하면 안 돼요. 캐시의 효과는 레이어 수, 헤드 수, 헤드 차원, 데이터 타입, 컨텍스트 길이, 배치 크기, 하드웨어에 따라 달라집니다. 숫자를 제시하려면 같은 조건에서 직접 측정하고, 그렇지 않다면 “실행 가능성을 높일 수 있지만 지연시간 비용이 생길 수 있다” 정도로 범위를 좁혀 쓰는 편이 정확해요.

선택 기준 한 줄 정리

처음 구현하거나 요청 길이가 들쭉날쭉하면 DynamicCache로 기준선을 만들고, 입력 모양이 안정된 반복 추론에서 컴파일을 활용하려면 StaticCache를 검토하세요. GPU 메모리가 부족하지만 CPU 메모리와 지연시간을 일부 양보할 수 있을 때만 OffloadedCache를 고려하면 됩니다. 캐시 구현을 바꾸는 일보다 먼저 입력 길이와 동시 요청을 통제하는 것이 더 큰 효과를 낼 때도 많아요.

관련 글

공식 출처

  1. Hugging Face Transformers: Cache strategies [1]
  2. Hugging Face Transformers: How caching works [2]

함께 읽을 글