- Published on
LLM 추론 최적화 완전 가이드 2025: vLLM, TensorRT-LLM, KV Cache, Speculative Decoding
- Authors

- Name
- Youngju Kim
- @fjvbn20031
목차
1. LLM 추론의 병목 이해: Compute-Bound vs Memory-Bound
LLM 추론 최적화를 논하기 전에, 먼저 병목이 어디서 발생하는지 정확히 이해해야 합니다.
1.1 Arithmetic Intensity와 Roofline Model
GPU 연산의 성능은 두 가지 리소스에 의해 결정됩니다.
| 리소스 | 단위 | A100 80GB | H100 80GB | H200 141GB |
|---|---|---|---|---|
| 연산 능력 (FP16) | TFLOPS | 312 | 989 | 989 |
| 메모리 대역폭 | TB/s | 2.0 | 3.35 | 4.8 |
| Arithmetic Intensity 경계 | FLOP/byte | 156 | 295 | 206 |
Arithmetic Intensity = 총 연산량(FLOPs) / 총 메모리 전송량(Bytes)
- Compute-Bound: Arithmetic Intensity가 경계값보다 높을 때. 행렬 곱셈이 대표적
- Memory-Bound: Arithmetic Intensity가 경계값보다 낮을 때. Attention, Decoding이 대표적
1.2 Prefill vs Decode 단계
LLM 추론은 크게 두 단계로 나뉩니다.
┌──────────────────────────────────────────────────────┐
│ LLM 추론 파이프라인 │
├──────────────────┬───────────────────────────────────┤
│ Prefill 단계 │ Decode 단계 │
│ (프롬프트 처리) │ (토큰 생성) │
├──────────────────┼───────────────────────────────────┤
│ - 입력 토큰 병렬 │ - 토큰 1개씩 순차 생성 │
│ - Compute-Bound │ - Memory-Bound │
│ - 높은 GPU 활용률 │ - 낮은 GPU 활용률 (보통 5-15%) │
│ - 한 번 실행 │ - 출력 길이만큼 반복 │
│ - KV Cache 생성 │ - KV Cache 읽기 + 추가 │
└──────────────────┴───────────────────────────────────┘
Prefill 단계: 전체 프롬프트를 한 번에 처리합니다. 행렬-행렬 곱셈(GEMM)이 주를 이루어 compute-bound입니다.
Decode 단계: 토큰을 하나씩 생성합니다. 행렬-벡터 곱셈(GEMV)이 주를 이루어 memory-bound입니다. 매 스텝마다 전체 모델 가중치를 읽어야 하지만 실제 연산량은 적습니다.
1.3 왜 Decode가 느린가
Llama-2 70B 모델 기준:
- 모델 가중치: 약 140GB (FP16)
- Decode 한 스텝당: 140GB를 메모리에서 읽어야 함
- A100 대역폭 2TB/s 기준: 140GB / 2TB/s = 70ms per token
- 실제 연산에 필요한 시간: 약 1ms
메모리 읽기가 70배 더 오래 걸립니다. 이것이 LLM 추론 최적화의 핵심 동기입니다.
2. KV Cache: LLM 추론의 핵심 자료구조
2.1 KV Cache란 무엇인가
Transformer의 Self-Attention은 모든 이전 토큰의 Key(K)와 Value(V)를 필요로 합니다. KV Cache는 이미 계산된 K, V 텐서를 저장하여 재계산을 방지합니다.
# KV Cache 없는 경우 (매 스텝 전체 재계산)
# 토큰 n개 생성 시 총 연산: O(n^2 * d)
# KV Cache 있는 경우 (이전 결과 재사용)
# 토큰 n개 생성 시 총 연산: O(n * d)
# 단, KV Cache 메모리: O(n * d) 추가 필요
2.2 KV Cache 메모리 계산
KV Cache 크기 = 2 * num_layers * num_kv_heads * head_dim * seq_len * batch_size * dtype_size
예시: Llama-2 70B, seq_len=4096, batch_size=1, FP16
= 2 * 80 * 8 * 128 * 4096 * 1 * 2 bytes
= 1.34 GB (시퀀스 하나에!)
batch_size=32면: 1.34 * 32 = 42.9 GB
| 모델 | 파라미터 | KV Cache/token (FP16) | 4K 시퀀스 1개 | 4K 시퀀스 32개 |
|---|---|---|---|---|
| Llama-2 7B | 7B | 800 KB | 3.2 GB | 102 GB |
| Llama-2 70B | 70B | 320 KB | 1.34 GB | 42.9 GB |
| Mixtral 8x7B | 46.7B | 640 KB | 2.56 GB | 81.9 GB |
| Llama-3 405B | 405B | 1.6 MB | 6.4 GB | 204 GB |
2.3 PagedAttention (vLLM의 핵심)
기존 방식의 문제: 시퀀스마다 최대 길이만큼 연속 메모리를 미리 할당. 실제로는 60-80%가 낭비됩니다.
┌─────────────────────────────────────────────┐
│ 기존 KV Cache 할당 방식 │
│ │
│ Request 1: [████████░░░░░░░░░░░░] 40% 사용 │
│ Request 2: [████████████░░░░░░░░] 60% 사용 │
│ Request 3: [██░░░░░░░░░░░░░░░░░░] 10% 사용 │
│ ^^^^^^^^ 낭비되는 메모리 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ PagedAttention KV Cache 할당 │
│ │
│ 물리 블록: [B0][B1][B2][B3][B4][B5][B6][B7] │
│ │
│ Request 1 → 페이지 테이블: [B0, B3, B5] │
│ Request 2 → 페이지 테이블: [B1, B4, B6, B7] │
│ Request 3 → 페이지 테이블: [B2] │
│ │
│ ✅ 내부 단편화 거의 제로 │
│ ✅ 비연속 메모리 블록 활용 │
│ ✅ Copy-on-Write로 프롬프트 공유 │
└─────────────────────────────────────────────┘
PagedAttention의 핵심 아이디어:
- KV Cache를 고정 크기 **블록(페이지)**으로 분할
- OS의 가상 메모리처럼 페이지 테이블로 비연속 블록을 논리적으로 연결
- 필요할 때만 블록을 할당하여 내부 단편화 제거
- Copy-on-Write: 같은 프롬프트를 공유하는 요청들이 KV Cache를 공유
2.4 Prefix Caching
반복되는 시스템 프롬프트나 공통 프리픽스의 KV Cache를 재사용합니다.
# vLLM에서 Prefix Caching 활성화
from vllm import LLM
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
enable_prefix_caching=True, # Prefix Caching 활성화
max_model_len=8192,
)
# 같은 시스템 프롬프트를 사용하는 요청들은
# 시스템 프롬프트 부분의 KV Cache를 공유합니다
3. Attention 최적화: FlashAttention과 MQA/GQA
3.1 FlashAttention: IO-Aware Attention
표준 Attention의 문제점:
- Q, K, V 행렬을 HBM(High Bandwidth Memory)에서 읽기
- S = Q @ K^T 계산 후 HBM에 쓰기
- P = softmax(S) 계산 후 HBM에 쓰기
- O = P @ V 계산 후 HBM에 쓰기
총 4번의 HBM 읽기/쓰기 - 이것이 병목입니다.
┌──────────────────────────────────────────────┐
│ FlashAttention 핵심 아이디어 │
│ │
│ GPU 메모리 계층: │
│ ┌─────────┐ 19 TB/s ┌─────────────────┐ │
│ │ SRAM │◄──────────►│ Compute Units │ │
│ │ (20 MB) │ └─────────────────┘ │
│ └────┬────┘ │
│ │ 2-4.8 TB/s │
│ ┌────▼────────────────┐ │
│ │ HBM (80-141 GB) │ │
│ └─────────────────────┘ │
│ │
│ 전략: Q,K,V를 타일(블록)로 나누어 │
│ SRAM에서 모든 계산을 수행하고 │
│ 최종 결과만 HBM에 기록 │
└──────────────────────────────────────────────┘
3.2 FlashAttention 버전별 비교
| 특성 | FlashAttention-1 | FlashAttention-2 | FlashAttention-3 |
|---|---|---|---|
| 출시 | 2022 | 2023 | 2024 |
| 속도 향상 | 2-4x | 추가 2x | 추가 1.5-2x |
| GPU 지원 | A100 | A100, H100 | H100 (Hopper 최적화) |
| 주요 최적화 | 타일링, 재계산 | 병렬화 개선, warp 분할 | FP8, 비동기 복사, 파이프라이닝 |
| MHA 대비 FLOPS | 50-70% | 70-80% | 최대 740 TFLOPS (75%) |
3.3 Multi-Query Attention (MQA) vs Grouped-Query Attention (GQA)
KV Cache 크기를 줄이는 아키텍처 수준 최적화입니다.
┌─────────────────────────────────────────────────────┐
│ Multi-Head Attention (MHA) │
│ Q heads: [H1][H2][H3][H4][H5][H6][H7][H8] │
│ K heads: [H1][H2][H3][H4][H5][H6][H7][H8] │
│ V heads: [H1][H2][H3][H4][H5][H6][H7][H8] │
│ KV Cache: 8x │
├─────────────────────────────────────────────────────┤
│ Multi-Query Attention (MQA) │
│ Q heads: [H1][H2][H3][H4][H5][H6][H7][H8] │
│ K heads: [ H_shared ] │
│ V heads: [ H_shared ] │
│ KV Cache: 1x (8배 절감) │
├─────────────────────────────────────────────────────┤
│ Grouped-Query Attention (GQA, 2 groups) │
│ Q heads: [H1][H2][H3][H4] | [H5][H6][H7][H8] │
│ K heads: [ K_group1 ] | [ K_group2 ] │
│ V heads: [ V_group1 ] | [ V_group2 ] │
│ KV Cache: 2x (4배 절감) │
└─────────────────────────────────────────────────────┘
| 모델 | Attention 유형 | KV Heads | Q Heads | KV Cache 절감 |
|---|---|---|---|---|
| GPT-J 6B | MHA | 16 | 16 | 1x |
| Falcon-40B | MQA | 1 | 64 | 64x |
| Llama-2 70B | GQA | 8 | 64 | 8x |
| Llama-3 70B | GQA | 8 | 64 | 8x |
| Mistral 7B | GQA | 8 | 32 | 4x |
4. Batching 전략: Static vs Continuous
4.1 Static Batching의 한계
Static Batching (기존 방식):
시간 ──────────────────────────────────►
Req 1: [████████████████████████████████] (긴 응답)
Req 2: [████████░░░░░░░░░░░░░░░░░░░░░░] (짧은 응답)
Req 3: [██████████████░░░░░░░░░░░░░░░░] (중간 응답)
Req 4: [WAIT WAIT WAIT WAIT WAIT WAIT ] (대기 중)
░ = GPU 유휴 (패딩), WAIT = 배치 완료까지 대기
전체 배치가 끝나야 다음 배치 시작 → 처리량 매우 낮음
4.2 Continuous Batching (In-Flight Batching)
Continuous Batching:
시간 ──────────────────────────────────►
Req 1: [████████████████████████████████]
Req 2: [████████]
Req 3: [██████████████]
Req 4: [████████████████]
Req 5: [████████]
완료된 요청 즉시 제거 → 새 요청 즉시 투입
GPU 유휴 시간 최소화 → 처리량 10-20배 향상
Continuous Batching의 핵심 원리:
- 매 iteration마다 완료된 요청을 배치에서 제거
- 대기 중인 요청을 즉시 배치에 추가
- GPU가 항상 최대 부하로 동작
- 개별 요청의 레이턴시도 개선 (대기 시간 감소)
4.3 Chunked Prefill
긴 프롬프트의 Prefill 단계가 Decode 요청을 블로킹하는 문제를 해결합니다.
# vLLM chunked prefill 설정
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
enable_chunked_prefill=True,
max_num_batched_tokens=2048, # 한 번에 처리할 최대 토큰 수
)
# 긴 프롬프트(예: 32K 토큰)를 2048 토큰 청크로 나누어 처리
# 청크 사이사이에 Decode 요청도 처리 가능
# TTFT는 약간 증가하지만, 전체 시스템 처리량과 ITL 개선
5. Speculative Decoding: 추론 속도의 게임 체인저
5.1 핵심 아이디어
작은 **드래프트 모델(Draft Model)**이 여러 토큰을 빠르게 예측하고, 큰 **타겟 모델(Target Model)**이 한 번의 forward pass로 모두 검증합니다.
┌────────────────────────────────────────────────────┐
│ Speculative Decoding 흐름 │
│ │
│ Step 1: Draft Model (작고 빠른 모델) │
│ "The capital of France is" → [Paris][,][a][city] │
│ 4개 토큰을 매우 빠르게 예측 (4ms) │
│ │
│ Step 2: Target Model (크고 정확한 모델) │
│ 한 번의 forward pass로 4개 토큰 동시 검증 │
│ [Paris ✅] [, ✅] [a ❌→ "known"] [city ❌] │
│ │
│ 결과: "Paris, known" (2개 수락 + 1개 수정) │
│ 기존: 3 forward pass 필요 → 이제 1 forward pass │
│ 속도 향상: 약 2-3x │
└────────────────────────────────────────────────────┘
5.2 수학적 보장: 출력 품질 유지
Speculative Decoding의 핵심 장점은 타겟 모델의 출력 분포를 정확히 유지한다는 것입니다.
수락/거절 확률:
- 드래프트 토큰 x에 대해, 수락 확률 = min(1, p_target(x) / p_draft(x))
- 거절 시: (p_target(x) - p_draft(x)) 분포에서 재샘플링
이 과정을 통해 최종 출력은 타겟 모델만 사용한 것과 수학적으로 동일한 분포를 가집니다.
5.3 다양한 Speculative Decoding 변형
# 1. 별도 Draft Model 사용
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-70B-Instruct",
speculative_model="meta-llama/Llama-3.1-8B-Instruct",
num_speculative_tokens=5,
use_v2_block_manager=True,
)
# 2. Medusa Heads (추가 MLP 헤드로 여러 위치 동시 예측)
# Draft 모델 없이 타겟 모델 자체에 경량 헤드를 추가
# 학습 필요하지만 메모리 오버헤드 최소
# 3. EAGLE (Extrapolation Algorithm for Greater Language-model Efficiency)
# 드래프트 모델이 타겟 모델의 hidden state를 재사용
# 별도 드래프트 모델보다 높은 수락률
5.4 Tree Attention
여러 후보 시퀀스를 트리 구조로 동시에 검증합니다.
토큰 위치: 1 2 3
┌── Paris ─┬── is ── ...
The ────────┤ └── was ── ...
├── Lyon ── is ── ...
└── capital ── of ── ...
트리의 모든 경로를 한 번의 forward pass로 검증
→ 수락 확률 극대화, 처리량 향상
6. 양자화(Quantization)로 추론 가속
6.1 데이터 타입별 비교
| 데이터 타입 | 비트 수 | 범위 | 메모리 절감 | 품질 영향 |
|---|---|---|---|---|
| FP32 | 32 | 매우 넓음 | 기준 | 기준 |
| FP16 | 16 | 넓음 | 2x | 무시 가능 |
| BF16 | 16 | FP32와 동일 | 2x | 무시 가능 |
| FP8 (E4M3) | 8 | 중간 | 4x | 매우 적음 |
| INT8 | 8 | -128~127 | 4x | 적음 |
| INT4 | 4 | -8~7 | 8x | 중간 |
| NF4 | 4 | 정규분포 최적화 | 8x | INT4보다 적음 |
6.2 양자화 기법 비교
┌───────────────────────────────────────────────────────┐
│ 양자화 기법 분류 │
├─────────────────────┬─────────────────────────────────┤
│ Post-Training │ Training-Aware │
│ Quantization(PTQ) │ Quantization │
├─────────────────────┼─────────────────────────────────┤
│ - GPTQ (INT4) │ - QLoRA + Merge │
│ - AWQ (INT4) │ - QAT (Quantization-Aware │
│ - GGUF (다양한) │ Training) │
│ - bitsandbytes │ │
│ - SmoothQuant │ │
│ - FP8 Dynamic │ │
└─────────────────────┴─────────────────────────────────┘
6.3 주요 양자화 포맷 상세
# GPTQ: 레이어별 최적 양자화 (OBQ 기반)
# 장점: INT4에서도 좋은 품질, GPU 추론 최적화
# 단점: 캘리브레이션 데이터 필요, 양자화 시간 오래 걸림
from transformers import AutoModelForCausalLM, GPTQConfig
gptq_config = GPTQConfig(
bits=4,
group_size=128,
dataset="c4",
desc_act=True,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-70B-Instruct",
quantization_config=gptq_config,
device_map="auto",
)
# AWQ: Activation-aware Weight Quantization
# 핵심: 중요한 가중치 채널을 찾아 보호 (활성화 크기 기준)
# GPTQ보다 빠른 양자화, 비슷한 품질
from awq import AutoAWQForCausalLM
model = AutoAWQForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-70B-Instruct"
)
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM"
}
model.quantize(tokenizer, quant_config=quant_config)
# bitsandbytes: 간편한 INT8/NF4 양자화
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype="bfloat16",
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
quantization_config=bnb_config,
)
6.4 GGUF: CPU/Metal 추론용 포맷
llama.cpp에서 사용하는 양자화 포맷입니다. 다양한 양자화 레벨을 지원합니다.
| GGUF 양자화 | 비트 | 방법 | 품질 | 속도 |
|---|---|---|---|---|
| Q2_K | 2-3 | K-quant 혼합 | 낮음 | 매우 빠름 |
| Q4_K_M | 4-5 | K-quant 중간 | 좋음 | 빠름 |
| Q5_K_M | 5-6 | K-quant 중간 | 매우 좋음 | 보통 |
| Q6_K | 6 | K-quant | 거의 원본 | 느림 |
| Q8_0 | 8 | 균일 양자화 | 원본과 동일 | 느림 |
| F16 | 16 | 양자화 없음 | 원본 | 가장 느림 |
7. 서빙 프레임워크 비교: vLLM vs TensorRT-LLM vs TGI
7.1 종합 비교 표
| 기능 | vLLM | TensorRT-LLM | TGI | Ollama | llama.cpp |
|---|---|---|---|---|---|
| 개발사 | UC Berkeley | NVIDIA | Hugging Face | Ollama | ggerganov |
| 언어 | Python/C++ | C++/Python | Rust/Python | Go | C/C++ |
| PagedAttention | O | O | O | X | X |
| Continuous Batching | O | O | O | X | X |
| Tensor Parallelism | O | O | O | X | X |
| FP8 지원 | O | O (최적) | O | X | X |
| Speculative Decoding | O | O | 제한적 | X | O |
| LoRA 서빙 | O (다중) | O | O | O | O |
| Vision 모델 | O | O | O | O | O (일부) |
| CPU 추론 | 제한적 | X | X | O | O (최적) |
| Metal (Apple) | X | X | X | O | O |
| 설치 난이도 | 쉬움 | 어려움 | 쉬움 | 매우 쉬움 | 보통 |
| 프로덕션 적합도 | 높음 | 높음 | 높음 | 낮음 | 중간 |
7.2 처리량 벤치마크 (Llama-3.1 8B, A100 80GB)
| 프레임워크 | 처리량 (tok/s) | TTFT (ms) | ITL (ms) | 메모리 사용 |
|---|---|---|---|---|
| vLLM (FP16) | 4,200 | 45 | 12 | 18 GB |
| vLLM (AWQ-4bit) | 6,800 | 32 | 8 | 7 GB |
| TensorRT-LLM (FP16) | 4,800 | 38 | 10 | 17 GB |
| TensorRT-LLM (FP8) | 7,500 | 28 | 7 | 10 GB |
| TGI (FP16) | 3,600 | 52 | 14 | 18 GB |
| llama.cpp (Q4_K_M) | 120 | 200 | 35 | 5 GB |
8. vLLM 심화: 아키텍처부터 LoRA 서빙까지
8.1 vLLM 아키텍처
┌──────────────────────────────────────────┐
│ vLLM Architecture │
│ │
│ ┌─────────┐ ┌──────────────────┐ │
│ │ FastAPI │────►│ LLM Engine │ │
│ │ Server │ │ │ │
│ └─────────┘ │ ┌────────────┐ │ │
│ │ │ Scheduler │ │ │
│ ┌─────────┐ │ │ (요청 배치) │ │ │
│ │ OpenAI │────►│ └─────┬──────┘ │ │
│ │ compat │ │ │ │ │
│ └─────────┘ │ ┌─────▼──────┐ │ │
│ │ │ Block Mgr │ │ │
│ │ │ (PagedAttn) │ │ │
│ │ └─────┬──────┘ │ │
│ │ │ │ │
│ │ ┌─────▼──────┐ │ │
│ │ │ Worker(s) │ │ │
│ │ │ (GPU 실행) │ │ │
│ │ └────────────┘ │ │
│ └──────────────────┘ │
└──────────────────────────────────────────┘
8.2 vLLM 실전 배포
# vLLM 서버 시작 (OpenAI API 호환)
# vllm serve meta-llama/Llama-3.1-70B-Instruct \
# --tensor-parallel-size 4 \
# --max-model-len 32768 \
# --gpu-memory-utilization 0.90 \
# --enable-prefix-caching \
# --enable-chunked-prefill \
# --max-num-batched-tokens 4096 \
# --port 8000
# Python으로 API 호출
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="token-abc123",
)
response = client.chat.completions.create(
model="meta-llama/Llama-3.1-70B-Instruct",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum computing"},
],
max_tokens=512,
temperature=0.7,
)
8.3 vLLM LoRA 다중 서빙
하나의 베이스 모델로 여러 LoRA 어댑터를 동시에 서빙할 수 있습니다.
# vllm serve meta-llama/Llama-3.1-8B-Instruct \
# --enable-lora \
# --lora-modules \
# sql-lora=./adapters/sql-lora \
# code-lora=./adapters/code-lora \
# chat-lora=./adapters/chat-lora \
# --max-loras 3 \
# --max-lora-rank 64
# API 호출 시 모델 이름으로 LoRA 어댑터 선택
response = client.chat.completions.create(
model="sql-lora", # LoRA 어댑터 이름
messages=[{"role": "user", "content": "SELECT ..."}],
)
8.4 vLLM Vision 모델 서빙
# 멀티모달 모델 서빙
# vllm serve Qwen/Qwen2-VL-7B-Instruct \
# --max-model-len 8192 \
# --limit-mm-per-prompt image=4
from openai import OpenAI
import base64
client = OpenAI(base_url="http://localhost:8000/v1", api_key="key")
response = client.chat.completions.create(
model="Qwen/Qwen2-VL-7B-Instruct",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
]
}],
)
9. TensorRT-LLM 심화: 최적 성능을 위한 선택
9.1 TensorRT-LLM 빌드 파이프라인
┌────────┐ ┌───────────┐ ┌──────────┐ ┌──────────┐
│HF Model│────►│ Convert │────►│ TRT-LLM │────►│ Triton │
│(원본) │ │ Checkpoint│ │ Engine │ │ 서빙 │
└────────┘ └───────────┘ └──────────┘ └──────────┘
양자화 적용 컴파일 최적화 API 서버
# Step 1: 체크포인트 변환 + FP8 양자화
python convert_checkpoint.py \
--model_dir meta-llama/Llama-3.1-70B-Instruct \
--output_dir ./checkpoint_fp8 \
--dtype bfloat16 \
--tp_size 4 \
--pp_size 1 \
--use_fp8
# Step 2: TensorRT 엔진 빌드
trtllm-build \
--checkpoint_dir ./checkpoint_fp8 \
--output_dir ./engine_fp8 \
--gemm_plugin auto \
--max_batch_size 64 \
--max_input_len 4096 \
--max_seq_len 8192 \
--paged_kv_cache enable \
--use_paged_context_fmha enable \
--workers 4
9.2 TensorRT-LLM FP8 최적화
H100 GPU의 FP8 Tensor Core를 최대한 활용합니다.
| 설정 | 처리량 (Llama-3.1 70B, 4xH100) | 레이턴시 |
|---|---|---|
| FP16, TP=4 | 2,400 tok/s | 16ms ITL |
| FP8, TP=4 | 4,200 tok/s | 9ms ITL |
| FP8 + Speculative | 5,800 tok/s | 6ms ITL |
| INT4 AWQ, TP=2 | 3,800 tok/s | 11ms ITL |
9.3 Inflight Batching (TensorRT-LLM)
TensorRT-LLM의 Continuous Batching 구현입니다.
# Triton Inference Server + TensorRT-LLM 백엔드
# model_config.pbtxt 설정
"""
backend: "tensorrtllm"
max_batch_size: 64
model_transaction_policy {
decoupled: True # 스트리밍 응답 지원
}
parameters: {
key: "batching_type"
value: {string_value: "inflight"} # Inflight Batching 활성화
}
parameters: {
key: "max_tokens_in_paged_kv_cache"
value: {string_value: "131072"} # KV Cache 토큰 수 제한
}
"""
10. 모델 병렬화: Multi-GPU 전략
10.1 Tensor Parallelism (TP)
하나의 레이어를 여러 GPU에 분할합니다.
Tensor Parallelism (TP=4):
레이어 N의 가중치 행렬 W
┌──────┬──────┬──────┬──────┐
│ W_1 │ W_2 │ W_3 │ W_4 │
│GPU 0 │GPU 1 │GPU 2 │GPU 3 │
└──┬───┴──┬───┴──┬───┴──┬───┘
│ │ │ │
▼ ▼ ▼ ▼
[부분1] [부분2] [부분3] [부분4]
│ │ │ │
└──────┴──────┴──────┘
All-Reduce
(결과 합산)
장점: 레이턴시 감소 (모든 GPU 동시 계산)
단점: GPU 간 통신 필요 (NVLink 권장)
적합: 같은 노드 내 GPU (낮은 레이턴시 필요)
10.2 Pipeline Parallelism (PP)
레이어를 순차적으로 여러 GPU에 분배합니다.
Pipeline Parallelism (PP=4, 80 layers):
GPU 0: [Layer 0-19] → GPU 1: [Layer 20-39]
→ GPU 2: [Layer 40-59]
→ GPU 3: [Layer 60-79]
장점: GPU 간 통신 최소 (한 방향)
단점: 파이프라인 버블 (GPU 유휴 시간)
적합: 노드 간 분산 (높은 레이턴시 허용)
10.3 Expert Parallelism (EP) - MoE 모델용
Mixture of Experts 모델에서 Expert를 분산합니다.
Expert Parallelism (Mixtral 8x7B, EP=4):
GPU 0: Expert 0, 1 + Shared Layers
GPU 1: Expert 2, 3 + Shared Layers
GPU 2: Expert 4, 5 + Shared Layers
GPU 3: Expert 6, 7 + Shared Layers
토큰 라우팅: 각 토큰은 Top-2 Expert로 전송
→ GPU 간 All-to-All 통신 필요
10.4 실전 병렬화 조합
# Llama-3.1 405B 서빙 (8x H100 80GB)
# 모델 크기: ~810 GB (FP16) → FP8로 ~405 GB
# 옵션 1: TP=8 (모든 GPU에 모든 레이어 분할)
vllm serve meta-llama/Llama-3.1-405B-Instruct-FP8 \
--tensor-parallel-size 8 \
--max-model-len 16384
# 옵션 2: TP=4, PP=2 (4 GPU씩 2 파이프라인 스테이지)
vllm serve meta-llama/Llama-3.1-405B-Instruct-FP8 \
--tensor-parallel-size 4 \
--pipeline-parallel-size 2 \
--max-model-len 16384
11. GPU 메모리 최적화 심화
11.1 KV Cache 양자화
# vLLM에서 KV Cache FP8 양자화
vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 \
--kv-cache-dtype fp8 \
--quantization fp8
# KV Cache 메모리 절감 효과
# FP16 KV Cache: 1.34 GB / sequence (Llama-2 70B, 4K)
# FP8 KV Cache: 0.67 GB / sequence (50% 절감)
# 같은 GPU에서 2배 많은 동시 요청 처리 가능
11.2 메모리 할당 전략
GPU 메모리 분배 (A100 80GB, Llama-3.1 70B FP16):
┌─────────────────────────────────┐
│ 모델 가중치: ~35 GB (TP=2) │ 43.75%
├─────────────────────────────────┤
│ KV Cache: ~35 GB │ 43.75%
│ (gpu_memory_utilization=0.90) │
├─────────────────────────────────┤
│ 활성화 메모리: ~2 GB │ 2.5%
├─────────────────────────────────┤
│ 시스템 예약: ~8 GB │ 10%
└─────────────────────────────────┘
KV Cache가 처리 가능한 최대 동시 요청 수를 결정합니다.
11.3 메모리 부족 시 대응 전략
| 전략 | 구현 | 효과 | 부작용 |
|---|---|---|---|
| 양자화 | FP16→INT4 | 가중치 4배 감소 | 미세한 품질 저하 |
| KV Cache 양자화 | FP16→FP8 | KV Cache 2배 감소 | 무시할 수준 |
| max_model_len 축소 | 32K→8K | 해당 비율만큼 KV Cache 감소 | 긴 컨텍스트 불가 |
| TP 증가 | TP=2→TP=4 | GPU당 메모리 절반 | GPU 추가 비용 |
| Prefix Caching | 시스템 프롬프트 공유 | 반복 요청 시 큰 절감 | 유니크 요청에는 효과 없음 |
12. 비용 분석: 플랫폼별 tokens/dollar
12.1 셀프 호스팅 비용 비교
| GPU | 클라우드 시간당 비용 | Llama-3.1 70B 처리량 | tokens/dollar |
|---|---|---|---|
| A100 80GB x1 | 약 3.0 USD | 800 tok/s (FP16) | 960K |
| A100 80GB x4 (TP=4) | 약 12.0 USD | 2,800 tok/s | 840K |
| H100 80GB x1 | 약 4.5 USD | 1,500 tok/s (FP8) | 1,200K |
| H100 80GB x4 (TP=4) | 약 18.0 USD | 5,000 tok/s (FP8) | 1,000K |
| L40S x1 | 약 1.5 USD | 600 tok/s (INT4) | 1,440K |
| 4090 x1 (자체 서버) | 약 0.3 USD (전기) | 400 tok/s (INT4) | 4,800K |
12.2 API vs 셀프 호스팅 손익분기점
월간 토큰 사용량별 비용 비교 (Llama-3.1 70B급):
┌─────────────────────────────────────────────────┐
│ 비용 │
│ ($) │
│ 5000│ /API │
│ │ / │
│ 3000│ ────────── Self-Hosted │
│ │ / (H100x4 월 고정비) │
│ 1000│ / API │
│ │ / │
│ 0├──┬────┬────┬────┬────┬────┬────► │
│ 0 2B 5B 10B 20B 50B 100B 토큰/월 │
│ │
│ 손익분기점: 약 10B tokens/month │
└─────────────────────────────────────────────────┘
13. 벤치마킹: 올바른 측정 방법
13.1 핵심 메트릭
| 메트릭 | 정의 | 중요한 이유 |
|---|---|---|
| TTFT (Time To First Token) | 첫 토큰 생성까지 시간 | 사용자 체감 응답 시작 시간 |
| ITL (Inter-Token Latency) | 토큰 간 생성 시간 | 스트리밍 시 체감 속도 |
| E2E Latency | 전체 요청 완료 시간 | 총 대기 시간 |
| Throughput | 초당 생성 토큰 수 | 시스템 전체 처리 능력 |
| TPS/User | 사용자당 초당 토큰 | 개인 체감 속도 |
13.2 벤치마킹 도구와 방법
# vLLM 내장 벤치마크 (추천)
# python -m vllm.entrypoints.openai.api_server 실행 후:
# python benchmarks/benchmark_serving.py \
# --backend vllm \
# --model meta-llama/Llama-3.1-8B-Instruct \
# --dataset-name sharegpt \
# --dataset-path ShareGPT_V3_unfiltered.json \
# --num-prompts 1000 \
# --request-rate 10 \
# --endpoint /v1/completions
# 결과 예시:
# Successful requests: 1000
# Benchmark duration (s): 105.23
# Total input tokens: 215000
# Total generated tokens: 180000
# Request throughput (req/s): 9.50
# Output token throughput (tok/s): 1710.5
# Mean TTFT (ms): 48.2
# Median TTFT (ms): 42.1
# P99 TTFT (ms): 125.3
# Mean ITL (ms): 11.8
# Median ITL (ms): 10.2
# P99 ITL (ms): 35.7
13.3 부하별 성능 특성
처리량과 레이턴시의 관계 (concurrency 증가 시):
처리량 레이턴시
(tok/s) (ms)
│ ┌────────── │ /
│ / │ /
│ / │ /
│ / │ /
│ / │ /
│ / │ /
│ / │ /
│ / │/
├──────────────► concurrency ├──────────────► concurrency
최적 운영점: 처리량이 포화되기 직전 (Knee point)
보통 GPU 활용률 70-80% 지점
14. 실전 배포 아키텍처
14.1 프로덕션 서빙 아키텍처
┌──────────────────────────────────────────────────┐
│ Production Architecture │
│ │
│ Client → Load Balancer → API Gateway │
│ │ │
│ ┌─────────┼─────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐┌────────┐┌────────┐ │
│ │ vLLM ││ vLLM ││ vLLM │ │
│ │ Pod 1 ││ Pod 2 ││ Pod 3 │ │
│ │ (4xH100)││(4xH100)││(4xH100)│ │
│ └────┬────┘└───┬────┘└───┬────┘ │
│ │ │ │ │
│ ┌────▼─────────▼─────────▼────┐ │
│ │ Prometheus + Grafana │ │
│ │ (메트릭 수집/시각화) │ │
│ └─────────────────────────────┘ │
│ │
│ Autoscaling: 큐 길이/GPU 활용률 기반 │
│ Health Check: /health 엔드포인트 │
│ Graceful Shutdown: 진행 중인 요청 완료 후 종료 │
└──────────────────────────────────────────────────┘
14.2 Kubernetes 배포 예시
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama3-70b
spec:
replicas: 3
selector:
matchLabels:
app: vllm-llama3
template:
metadata:
labels:
app: vllm-llama3
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
command: ["python", "-m", "vllm.entrypoints.openai.api_server"]
args:
- "--model=meta-llama/Llama-3.1-70B-Instruct"
- "--tensor-parallel-size=4"
- "--max-model-len=16384"
- "--gpu-memory-utilization=0.90"
- "--enable-prefix-caching"
- "--enable-chunked-prefill"
resources:
limits:
nvidia.com/gpu: "4"
memory: "64Gi"
requests:
nvidia.com/gpu: "4"
memory: "32Gi"
ports:
- containerPort: 8000
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 180
periodSeconds: 30
nodeSelector:
gpu-type: h100
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
15. 퀴즈
Q1. vLLM의 PagedAttention이 해결하는 핵심 문제는 무엇인가요?
정답: KV Cache의 메모리 단편화 문제를 해결합니다.
기존 방식은 시퀀스마다 최대 길이만큼 연속 메모리를 미리 할당하여 60-80%가 낭비되었습니다. PagedAttention은 OS의 가상 메모리처럼 KV Cache를 고정 크기 블록(페이지)으로 나누고, 페이지 테이블로 비연속 블록을 논리적으로 연결합니다. 이를 통해:
- 내부 단편화 거의 제거
- 비연속 메모리 블록 활용 가능
- Copy-on-Write로 공통 프리픽스 KV Cache 공유
결과적으로 같은 GPU 메모리에서 2-4배 많은 동시 요청을 처리할 수 있습니다.
Q2. Continuous Batching이 Static Batching보다 처리량이 높은 이유는?
정답: Static Batching은 배치 내 모든 요청이 끝날 때까지 기다려야 다음 배치를 시작합니다. 짧은 응답이 먼저 끝나도 GPU는 유휴 상태로 대기합니다.
Continuous Batching은:
- 매 iteration마다 완료된 요청을 즉시 제거
- 대기 큐의 새 요청을 즉시 투입
- GPU가 항상 최대 부하로 동작
이를 통해 Static Batching 대비 10-20배 높은 처리량을 달성합니다. 개별 요청의 레이턴시도 대기 시간 감소로 개선됩니다.
Q3. Speculative Decoding이 출력 품질을 저하시키지 않는 이유는?
정답: 수학적으로 타겟 모델의 출력 분포를 정확히 보존하기 때문입니다.
드래프트 모델이 예측한 토큰 x에 대해:
- 수락 확률 = min(1, p_target(x) / p_draft(x))
- 거절 시: (p_target - p_draft) 분포에서 재샘플링
이 과정을 통해 최종 출력은 타겟 모델만 사용한 것과 수학적으로 동일한 분포를 가집니다. 속도만 향상되고 품질 손실은 제로입니다.
Q4. LLM Decode 단계가 Memory-Bound인 이유는?
정답: Decode 단계에서는 한 번에 한 토큰만 생성합니다. 이때 전체 모델 가중치를 메모리에서 읽어야 하지만(행렬-벡터 곱셈), 실제 연산량은 매우 적습니다.
Llama-2 70B 예시:
- 모델 가중치 140GB를 매 스텝 읽어야 함
- A100 대역폭 2TB/s 기준: 70ms (메모리 읽기)
- 실제 연산 시간: 약 1ms
메모리 대역폭이 병목이므로 Memory-Bound입니다. 이것이 양자화(가중치 크기 축소)와 배칭(가중치 읽기 1회로 여러 요청 처리)이 효과적인 이유입니다.
Q5. FP8 양자화가 INT8보다 LLM 추론에 더 적합한 이유는?
정답: FP8은 부동소수점 형식이라 넓은 동적 범위를 가집니다. LLM 가중치와 활성화의 분포는 매우 다양한 크기를 가지므로, 고정소수점인 INT8보다 FP8이 더 적합합니다.
구체적으로:
- FP8 E4M3: 지수부 4비트, 가수부 3비트 → 넓은 범위, 적당한 정밀도
- INT8: -128~127 고정 범위 → 이상치(outlier)에 취약
- H100 GPU는 FP8 전용 Tensor Core를 탑재하여 FP16 대비 2배 연산 성능
- FP8은 별도 캘리브레이션 없이 동적 양자화 가능
결과적으로 FP8은 INT4에 가까운 성능 향상을 제공하면서 FP16에 가까운 품질을 유지합니다.
16. 참고 자료
- vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention - Kwon et al., 2023
- FlashAttention: Fast and Memory-Efficient Exact Attention - Dao et al., 2022
- FlashAttention-2: Faster Attention with Better Parallelism - Dao, 2023
- Efficient Memory Management for Large Language Model Serving with PagedAttention - Kwon et al., 2023
- Fast Inference from Transformers via Speculative Decoding - Leviathan et al., 2023
- GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers - Frantar et al., 2023
- AWQ: Activation-aware Weight Quantization - Lin et al., 2024
- TensorRT-LLM - NVIDIA Official Documentation
- Orca: A Distributed Serving System for Transformer-Based Generative Models - Yu et al., 2022
- GQA: Training Generalized Multi-Query Transformer Models - Ainslie et al., 2023
- Medusa: Simple LLM Inference Acceleration Framework - Cai et al., 2024
- EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty - Li et al., 2024
- SmoothQuant: Accurate and Efficient Post-Training Quantization - Xiao et al., 2023