LLM 추론 최적화 - 양자화와 KV 캐시
LLM을 직접 서빙해보면 학습보다 추론(inference) 이 더 골치 아프다는 걸 금방 알게 됩니다. GPU 메모리는 순식간에 꽉 차고, 토큰은 느리게 나오고, 비용은 요청량에 비례해 불어납니다. 다행히 추론을 가볍게 만드는 두 가지 핵심 기술이 있습니다. 바로 양자화(Quantization) 와 KV 캐시(KV Cache) 입니다.
이 글에서는 추론이 왜 무거운지부터 시작해, 이 둘이 각각 무엇을 해결하는지, 그리고 배치·연속 배치 같은 서빙 기법과 실무 선택 기준까지 정리합니다.
1. LLM 추론은 왜 무거운가
LLM이 답을 만드는 과정은 토큰을 하나씩 순차적으로 생성하는 일입니다. "안녕하세요"를 만들려면 "안" → "녕" → ... 이렇게 한 토큰씩, 매번 모델 전체를 다시 통과시켜야 합니다.
여기서 두 가지 병목이 생깁니다.
- 메모리 병목: 70B 모델을 fp16으로 올리면 가중치만 약 140GB입니다. 여기에 실행 중 상태까지 더해져 GPU VRAM을 압박합니다.
- 대역폭 병목: 토큰 하나를 만들 때마다 수십~수백 GB의 가중치를 메모리에서 읽어와야 합니다. 연산보다 메모리에서 데이터를 옮기는 시간이 더 큽니다. 그래서 추론은 흔히 memory-bound 라고 부릅니다.
비유하자면, 요리 실력(연산)이 아니라 냉장고에서 재료를 꺼내오는 왕복 시간(메모리 이동) 이 전체 속도를 결정하는 상황입니다.
이 두 병목을 각각 공략하는 것이 양자화(메모리 크기)와 KV 캐시(중복 연산 제거)입니다.
2. 양자화 - 숫자를 더 적은 비트로
모델 가중치는 기본적으로 실수(float)입니다. 이 실수를 더 적은 비트로 표현해 용량과 대역폭을 줄이는 것이 양자화입니다.
| 정밀도 | 비트 | 상대 크기 | 특징 |
|---|---|---|---|
| fp32 | 32bit | 4배 | 학습 기본, 서빙엔 과함 |
| fp16 / bf16 | 16bit | 2배 | 추론 표준 |
| int8 | 8bit | 1배 | 품질 거의 유지, 흔한 선택 |
| int4 (4bit) | 4bit | 0.5배 | 메모리 최소, 품질 저하 감수 |
핵심 감각은 이렇습니다. fp16 → int8 로 가면 가중치 용량이 절반이 되고, 그만큼 메모리에서 읽어올 양도 줄어 속도도 빨라집니다. int4까지 내리면 소비자용 GPU에서도 큰 모델을 굴릴 수 있게 됩니다.
2-1. 품질 트레이드오프
공짜는 아닙니다. 비트를 줄이면 숫자를 표현하는 해상도가 낮아져 미세한 정보가 뭉개집니다.
- int8: 대부분의 작업에서 품질 손실이 거의 체감되지 않습니다. 실무 기본값으로 안전합니다.
- int4: 용량은 매력적이지만, 복잡한 추론·코딩에서 미묘하게 품질이 떨어질 수 있습니다.
그래서 실무에서는 정밀도를 무작정 낮추지 않고, 자신의 태스크로 직접 평가해서 허용 가능한 지점을 찾습니다. 요약 챗봇은 int4로 충분해도, 코드 생성은 int8을 유지하는 식입니다.
참고로 GPTQ, AWQ 같은 기법은 "품질에 덜 중요한 가중치는 과감히, 중요한 가중치는 보수적으로" 양자화해 4bit에서도 손실을 최소화합니다.
3. KV 캐시 - 이미 계산한 건 다시 안 한다
토큰을 하나 만들 때마다 모델은 지금까지의 모든 토큰을 다시 본다고 했습니다. 그런데 이전 토큰들에 대한 내부 계산 결과(어텐션의 Key와 Value)는 매번 똑같습니다. 매번 다시 계산하면 낭비죠.
KV 캐시는 이 Key/Value를 계산해두고 재사용하는 것입니다.
[캐시 없음] 5번째 토큰 생성 → 1~4번 토큰 K/V 전부 재계산 (매번 O(n²)) [KV 캐시] 5번째 토큰 생성 → 1~4번 K/V는 캐시에서 꺼냄, 5번째만 계산
덕분에 각 토큰 생성이 선형(O(n)) 에 가까워지고, 긴 답변일수록 효과가 큽니다. 오늘날 거의 모든 LLM 서빙이 KV 캐시를 기본으로 씁니다.
3-1. 공짜는 아니다 - KV 캐시도 메모리를 먹는다
KV 캐시는 연산을 줄이는 대신 메모리를 소비합니다. 캐시 크기는 대략 이렇게 정해집니다.
KV 캐시 크기 ≈ (레이어 수) × (시퀀스 길이) × (히든 차원) × 2(K,V) × (배치) × 정밀도
즉 문맥이 길수록, 동시 요청이 많을수록 캐시가 커집니다. 롱 컨텍스트를 다룰 때 VRAM이 폭발하는 주범이 바로 이 KV 캐시입니다. 그래서 PagedAttention(vLLM) 같은 기법이 캐시 메모리를 페이지 단위로 잘게 관리해 낭비를 줄입니다.
4. 배치와 연속 배치 - 처리량을 끌어올리기
추론이 memory-bound 라고 했죠. 가중치를 한 번 읽어온 김에 여러 요청을 한꺼번에 처리하면 그 비용을 나눠 쓸 수 있습니다. 이것이 배치(Batching) 입니다.
문제는 요청마다 답변 길이가 다르다는 점입니다. 단순 배치는 배치 안의 가장 긴 요청이 끝날 때까지 나머지가 기다립니다.
연속 배치(Continuous Batching) 는 이 낭비를 없앱니다.
- 한 요청이 끝나면 그 자리에 대기 중인 새 요청을 즉시 끼워 넣습니다.
- 토큰 생성 스텝 단위로 배치를 동적으로 재구성합니다.
비유하면, 정해진 인원이 다 탈 때까지 기다렸다 출발하는 버스(정적 배치)가 아니라, 자리가 비는 대로 다음 손님을 태우는 택시 정류장(연속 배치) 입니다. 실제로 vLLM, TGI 같은 서빙 엔진이 이 방식으로 처리량(throughput)을 몇 배씩 끌어올립니다.
여기서 두 지표를 구분해야 합니다.
- 지연 시간(Latency): 한 사용자가 답을 받기까지의 시간.
- 처리량(Throughput): 시간당 처리하는 전체 요청/토큰 수.
배치는 처리량을 키우지만 개별 지연은 늘 수 있어, 둘 사이의 균형을 잡아야 합니다.
5. 실무에서는 어떻게 고를까
직접 서빙한다면 대략 이 순서로 판단하면 편합니다.
- 엔진부터 고른다. 대부분은 직접 구현하지 말고 vLLM이나 TGI 같은 검증된 서빙 엔진을 씁니다. KV 캐시, PagedAttention, 연속 배치가 기본 내장입니다.
- 정밀도를 태스크로 결정한다. 기본은 fp16/bf16, VRAM이 부족하면 int8부터. int4는 품질 평가를 통과했을 때만.
- VRAM 예산을 계산한다.
가중치 + KV 캐시 + 여유로 잡습니다. 롱 컨텍스트·고동시성이면 KV 캐시를 넉넉히 봐야 합니다. - 목표 지표를 정한다. 실시간 챗봇이면 지연 우선, 대량 배치 처리면 처리량 우선으로 배치 크기를 조정합니다.
| 상황 | 권장 방향 |
|---|---|
| VRAM이 빠듯하다 | int8/int4 양자화 우선 |
| 답변이 길고 문맥이 크다 | KV 캐시/PagedAttention 최적화 |
| 동시 요청이 많다 | 연속 배치로 처리량 확보 |
| 실시간 응답이 중요하다 | 배치 크기를 낮춰 지연 최소화 |
물론 자체 서빙이 부담스러우면 상용 API를 쓰는 게 정답일 때도 많습니다. 이 최적화는 모델을 직접 굴릴 때 의미가 있습니다.
마무리
LLM 추론 최적화는 결국 두 문장으로 요약됩니다.
- 양자화: 가중치를 더 적은 비트로 담아 메모리와 대역폭을 아낀다. 대가는 미세한 품질 손실.
- KV 캐시: 이미 계산한 Key/Value를 재사용해 중복 연산을 없앤다. 대가는 추가 메모리.
그리고 이 둘 위에 배치·연속 배치를 얹어 GPU를 알뜰하게 굴립니다.
처음 시작한다면 vLLM으로 fp16 서빙을 띄워 baseline을 잡고, VRAM이 부족하면 int8 양자화를, 처리량이 필요하면 연속 배치 설정을 만지는 순서를 추천합니다. "더 큰 GPU" 가 아니라 "같은 GPU를 더 알뜰하게" 가 추론 최적화의 본질입니다.
