KV 캐시 양자화는 가중치 양자화를 이해하고 있는 엔지니어도 쉽게 함정에 빠지는 영역이다. 가중치 양자화에서 배운 직관 — "INT8이면 메모리 절반, 품질 손실은 거의 없다" — 을 KV 캐시에도 그대로 적용했다가, 롱컨텍스트 품질 저하를 겪고서야 무언가 다르다는 걸 알게 된다.
KV 캐시 양자화가 별도 문제인 이유
가중치는 훈련이 끝나면 고정된다. INT8로 바꾸기 전에 캘리브레이션 데이터셋을 한 번 통과시켜 스케일 팩터를 계산해 두면 끝이다. KV 캐시는 다르다. 매 forward pass마다 새로운 토큰의 K, V 텐서가 생성·누적되고, 그 분포는 입력 시퀀스와 위치에 따라 달라진다. 모델이 prefill을 마친 뒤에도 decode 단계마다 새 텐서가 append되므로, 고정 스케일 팩터를 사전 계산해서 재사용한다는 전략이 원천적으로 맞지 않는다.
가중치 양자화는 PTQ 타이밍에 스케일을 정하고 서빙 내내 재사용한다. KV 캐시 양자화는 각 요청의 런타임에 스케일을 결정해야 하고, 그러지 않으면 동적 범위를 잃는다. 이 차이가 정밀도 선택지를 좁힌다.
오차 경로도 다르다. 가중치 양자화 오차는 레이어를 거치면서 퍼지지만 attention 메커니즘의 피드백 루프를 타지 않는다. KV 캐시 오차는 다르다. attention score가 양자화된 K를 참조하고, softmax 결과가 양자화된 V를 가중합하므로, 시퀀스 내 모든 이전 토큰의 오차가 현재 토큰의 출력에 영향을 미친다. 시퀀스가 길수록 이 경로는 더 길어진다.
Key와 Value 텐서의 분포는 왜 다른가
KIVI 논문(Liu et al., ICML 2024)은 Llama, Falcon, Mistral에 걸쳐 KV 캐시 원소 분포를 분석했다. 결론은 Key와 Value가 서로 다른 분포를 가진다는 것이다.
Key 텐서는 채널(head_dim) 방향으로 outlier가 집중된다. 특정 채널에 다른 채널보다 훨씬 큰 절댓값이 반복적으로 나타나는 패턴인데, 이 채널들은 평균 크기 자체가 크다. 전체 토큰을 묶어 스케일을 계산하면 이 outlier 채널이 나머지 채널의 정밀도 예산을 잠식한다. 그래서 Key에는 per-channel quantization — 채널마다 독립적인 스케일 팩터를 두는 방식 — 이 맞다.
Value 텐서는 상대적으로 분포가 고르다. 채널 간 크기 차이가 Key만큼 크지 않고, 대신 토큰 방향으로 변동이 생긴다. Value에는 per-token quantization이 더 효율적이다.
동일한 양자화 설정을 Key와 Value에 함께 적용하면 Key 쪽에서 불균형한 오차가 발생한다. KIVI가 비대칭(asymmetric) 전략을 제안하는 이유가 여기에 있다 — Key는 per-channel INT2, Value는 per-token INT2로 각각 처리해 2bit에서도 2.6배 피크 메모리 절감과 품질 유지를 동시에 달성했다. KVQuant는 여기에 per-channel Key를 RoPE 적용 전에 처리하는 방식을 더해 수백만 토큰 컨텍스트 추론까지 지원 범위를 넓혔다.
컨텍스트 길이와 오차 누적
KV 캐시 양자화의 정확도 저하는 컨텍스트 길이에 비례해 커진다. attention은 현재 위치에서 과거 모든 위치의 K, V를 참조한다. 4096 토큰 시퀀스면 4,096개의 양자화된 K, V를 weighted sum하고, 32K 시퀀스면 32,768개를 같은 방식으로 처리한다. 개별 오차가 작아도 참조 횟수가 오차를 증폭시킨다.
여러 연구에서 일관되게 관찰되는 패턴: 대부분의 LLM은 4K 이하에서 가중치 양자화보다 KV 캐시 양자화에 오히려 덜 민감하지만, 16K~32K 이상에서는 민감도가 역전된다. RAG는 관련 청크를 여러 개 이어붙이면서 context가 쉽게 16K를 넘는데, 그 상태에서 KV 캐시를 INT4로 돌리면 검색된 근거를 올바르게 참조하지 못하는 케이스가 생긴다.
vLLM FP8 KV 캐시 벤치마크는 이 패턴을 정밀도별로 정량화한다. 128K 토큰 기준 MRCR(long-context retrieval) 태스크에서 FP8가 BF16 대비 AUC 97~98% 수준을 회복한다. FP8가 이만큼 선방하는 이유는 E4M3 포맷이 지수 비트를 갖고 있어 outlier를 클리핑하지 않고 표현 범위를 동적으로 조정하기 때문이다. 고정 소수점인 INT8은 outlier가 튀면 클리핑 오차가 크다. 컨텍스트가 길수록 이 차이가 누적돼 FP8과 INT8의 정확도 격차가 벌어진다.
INT8, FP8, INT4: 실측 수치
Llama-3-70B는 GQA를 쓴다. 80 레이어, KV heads 8개, head_dim 128이다. FP16 기준 토큰당 KV 캐시:
2(K+V) × 80 layers × 8 KV heads × 128 head_dim × 2 bytes = 327,680 bytes ≈ 0.31 MB/token
컨텍스트 길이와 배치 크기에 따른 KV 캐시 총량(GB):
| 컨텍스트 | 배치 | FP16 | INT8 / FP8 | INT4 |
|---|---|---|---|---|
| 4K | 1 | 1.3 | 0.6 | 0.3 |
| 4K | 8 | 10.0 | 5.0 | 2.5 |
| 4K | 32 | 40.0 | 20.0 | 10.0 |
| 16K | 1 | 5.0 | 2.5 | 1.3 |
| 16K | 8 | 40.0 | 20.0 | 10.0 |
| 16K | 32 | 160.0 | 80.0 | 40.0 |
| 32K | 1 | 10.0 | 5.0 | 2.5 |
| 32K | 8 | 80.0 | 40.0 | 20.0 |
| 32K | 32 | 320.0 | 160.0 | 80.0 |
A100 80GB에서 Llama-3-70B(FP16 가중치 약 140GB)를 텐서 병렬로 돌린다면, 32K × batch 8 조합에서 FP16 KV 캐시만 80GB다. INT8/FP8으로 줄이면 KV 캐시가 40GB로 떨어지고, 그 여력으로 배치를 키우거나 컨텍스트를 연장할 수 있다.
vLLM FP8 실측 결과: Llama-3.1-8B를 concurrency 8 / 입력 약 20K 토큰 조건에서 FP8가 BF16 대비 처리량 14.9% 향상, decode ITL 14.8% 감소를 보였다. 같은 조건에서 수용 가능한 KV 캐시 토큰 수는 35,792 → 71,584으로 2배 늘었다. 단, SqueezeBits의 비교에 따르면 vLLM은 FP8 KV 캐시 활성화 시 FlashAttention-2 백엔드를 사용할 수 없어 상황에 따라 처리량이 오히려 줄 수도 있다. TensorRT-LLM은 INT8과 FP8를 모두 지원하고 FP8 attention 연산을 내부에서 더 완성도 높게 처리한다. MMLU 정확도는 두 프레임워크 모두 8비트 KV 캐시에서 FP16 대비 거의 변동이 없었다(~0.679 수준).
설정 예시
vLLM (FP8 KV cache, CUDA 11.8+):
vllm serve meta-llama/Llama-3-70b-Instruct \
--kv-cache-dtype fp8_e4m3 \
--tensor-parallel-size 4
기본 스케일은 1.0이다. 정확도를 더 챙기려면 llm-compressor로 사전 calibration을 수행한 뒤 생성된 스케일 JSON을 함께 지정한다. 특정 레이어를 양자화에서 제외하고 싶으면 --kv-cache-dtype-skip-layers sliding_window 같은 플래그로 제어한다.
TensorRT-LLM (INT8 KV cache):
trtllm-build \
--checkpoint_dir ./llama-3-70b-hf \
--int8_kv_cache \
--tp_size 4 \
--output_dir ./engines/
스케일 팩터는 빌드 시 캘리브레이션 데이터에서 자동 계산된다. --fp8_kv_cache 플래그로 FP8로 전환할 수 있고, FP8 모델 가중치와 함께 쓸 때 attention 연산 전체를 FP8로 처리해 이득이 더 크다.
언제 KV 캐시 양자화를 켜는가
메모리 압박의 원인부터 진단한다. 배치 크기 1, 짧은 컨텍스트로 GPU를 모니터링하면 가중치가 차지하는 기저 메모리가 나온다. 컨텍스트를 늘리면서 증가하는 분이 KV 캐시다:
# 토큰당 KV 캐시 메모리 추정 (FP16 기준)
kv_per_token_bytes = 2 * num_layers * num_kv_heads * head_dim * 2
kv_cache_gb = (kv_per_token_bytes * seq_len * batch_size) / (1024**3)
KV 캐시 양자화가 맞는 상황: 컨텍스트가 8K 이상이고 배치를 더 크게 가져가야 하는 서빙 워크로드, 가중치는 이미 충분히 압축했는데 KV 캐시가 병목인 경우. 이때 INT8보다 동적 범위가 넓어 롱컨텍스트에 더 안전한 FP8로 시작하고, 정확도가 충분하면 유지한다.
피해야 하는 상황: 컨텍스트가 4K 이하인 짧은 대화 서빙(절감량이 작고 오버헤드만 생긴다), 정밀한 롱컨텍스트 추론에서 INT4를 고려하는 경우(예상보다 오차가 크다). vLLM의 prefix caching이 활성화된 환경에서는 캐시 히트율이 높을수록 FP8 양자화의 용량 이득이 더 크게 작용한다.
스케일링 팩터 재사용 함정: prefill에서 계산한 스케일 팩터를 decode 전체에 재사용하면, 새 토큰이 append되면서 텐서의 동적 범위가 prefill 시점과 달라진다. vLLM의 기본 스케일 1.0이 FP8에서 그나마 버티는 이유는 E4M3의 표현 범위가 넓어 스케일 오차를 흡수하기 때문이다 — INT8 고정 소수점에서는 같은 접근이 통하지 않는다. INT8을 선택한다면 per-token 동적 스케일링을 쓰는 구성을 권장한다.