LLM 의 비용 대부분은 학습이 아니라 추론에 있습니다. 학습은 한 번이지만 추론은 사용자가 메시지를 보낼 때마다 매번 일어나기 때문입니다. 이 글은 Red Hat 의 Cedric Clyburn 이 진행한 DeepLearning.AI 강의 "Fast & Efficient LLM Inference with vLLM" 을 학습용으로 정리한 것입니다. open source LLM 을 가져와 quantization 으로 줄이고, vLLM 으로 효율적으로 서빙하고, 벤치마크로 검증하는 전체 워크플로를 다룹니다.
전체 그림은 단순합니다. GPU 메모리에는 두 가지가 들어갑니다. 고정 크기인 model weights, 그리고 요청마다 토큰 단위로 자라는 KV cache. 최적화도 두 갈래입니다. 배포 전에 모델 자체를 줄이는 model optimization (quantization, sparsification), 그리고 런타임에 추론 엔진이 처리하는 inference optimization (continuous batching, PagedAttention, prefix caching). 강의는 이 두 축을 따라갑니다.

왜 효율적 배포가 중요한가
2023 년 초만 해도 쓸만한 open source 모델이 거의 없었지만, 지금은 Hugging Face 에 수천, 수만 개가 올라와 있습니다. 문제가 "좋은 모델을 구할 수 있는가" 에서 "그 모델을 효율적으로 돌릴 수 있는가" 로 옮겨갔습니다.
API 를 호출하는 대신 직접 모델을 돌리는 이유는 네 가지입니다.
- Cost: 토큰당 과금 대신, 작업 난이도에 모델 크기를 맞출 수 있습니다.
- Security: 데이터가 환경 밖으로 나가지 않습니다. 의료, 금융에서 중요합니다.
- Control: 업그레이드/폐기 시점을 직접 정하고, rate limit 도 서드파티 장애도 없습니다.
- Customization: fine-tune 으로 정확도와 비용을 조절합니다.
배포에는 측정 가능한 목표가 필요합니다. 이를 SLO (service level objective) 로 잡습니다. 추적할 차원은 두 가지입니다.
첫째는 정확도입니다. 틀리는 모델은 쓸모가 없습니다. 정확도가 사용 가능한 기준선을 넘어야 하고, 그 기준선은 use case 마다 다릅니다. 여기서 model card 가 등장합니다. model card 의 각 행은 표준 벤치마크 (general knowledge 는 MMLU, math 는 GSM8K 등) 이고, recovery 컬럼은 최적화 버전이 원본 정확도를 얼마나 유지하는지 보여줍니다 (예: 평균 99.88%). 배포 전에 정확도 SLO 충족 여부를 이걸로 확인합니다.

둘째는 추론 성능입니다. 핵심 latency 지표 셋과 throughput 하나를 봅니다.
- TTFT (time to first token): 첫 토큰이 나오기까지의 시간. 사용자가 응답을 보기 전까지 기다리는 시간.
- ITL (inter-token latency): 첫 토큰을 제외하고 연속된 토큰 사이의 평균 간격. 생성의 매끄러움.
- Request latency: 처음부터 끝까지 전체 시간.
- Throughput: 모든 요청에 걸쳐 초당 생성되는 output 토큰 수. 프로덕션 규모를 감당할 수 있는가.
배포는 항상 performance, accuracy, cost 사이의 삼각형 안에서 균형을 잡습니다. 보통 둘을 고르면 셋째가 대가를 치릅니다. 높은 정확도 + 낮은 latency 는 높은 비용, 낮은 비용 + 높은 정확도는 높은 latency, 낮은 비용 + 낮은 latency 는 정확도 희생. 다만 제대로 된 도구와 기법이 그 한계를 밀어낼 수 있다는 게 이 강의의 요지입니다.
비용 감각을 잡아두면, Llama 를 full precision weights 로 한 번에 한 요청씩 처리하는 naive 배포는 월 수십만에서 수백만 달러까지 갈 수 있습니다. 여기에 vLLM 의 continuous batching 과 PagedAttention 을 더하면 throughput 이 극적으로 올라 비용이 약 10 배 줄고, 거기에 quantized 모델까지 얹으면 메모리 footprint 가 더 줄어듭니다.
추론과 메모리 기초
추론을 돌리려면 디스크의 모델 파일만으로는 부족합니다. 세 층이 같이 동작합니다. 위에는 모델 (학습된 파라미터 파일, Llama 나 Qwen), 가운데는 inference server (vLLM 같은 소프트웨어. 모델을 로드하고 요청을 관리하며 최적화를 담당), 아래는 hardware accelerator (보통 GPU). PyTorch 로 GPU 에 모델을 직접 올려 돌릴 수도 있지만 그건 노트북이나 단일 사용자용입니다. 다수 사용자를 효율적으로 받는 순간 inference server 가 필수가 됩니다.
autoregressive generation
LLM 은 문장을 한 번에 만들지 않고 토큰을 하나씩 생성합니다. "The quick brown" 이 들어오면 다음 토큰 "fox" 를 예측하고, 그걸 입력에 붙여 "The quick brown fox" 로 다시 돌려 "jumps" 를 예측합니다. end of sequence 토큰이 나올 때까지 반복합니다. 각 토큰은 그 앞의 모든 토큰 (방금 생성한 것 포함) 에 의존합니다. 토큰 하나마다 모델 전체를 한 번 통과 (forward pass) 하므로, 500 토큰 답변은 모델을 500 번 돌립니다.
transformer 한 층 안
forward pass 한 번을 들여다보면, 토큰은 먼저 token embedding (숫자 벡터) 으로 변환되고, transformer layer 스택을 통과합니다. 각 layer 는 두 부분입니다. 토큰끼리 정보를 주고받는 Self-Attention block, 그리고 각 토큰 표현을 더 가공하는 Feed-Forward Network. 이 쌍이 N 번 쌓입니다. 마지막 layer 뒤에는 LM Head 가 내부 표현을 가능한 다음 토큰 각각의 점수로 바꾸고, 최고 점수 토큰이 예측값입니다 (예: fox 90%).
Self-Attention 과 Feed-Forward 는 모두 linear layer 로 만들어집니다. linear layer 는 그냥 행렬 곱입니다. 벡터를 받아 weight matrix 와 곱해 새 벡터를 냅니다. 단순하지만 모델 파라미터 대부분이 여기 있고 계산 대부분도 여기서 일어납니다.
- Self-Attention 안에는 linear layer 네 개: Q, K, V, O projection. 토큰들이 서로를 보며 관계를 파악합니다.
- Feed-Forward 안에는 linear layer 세 개: gate, up, down projection. 토큰 간 상호작용 없이 각 토큰을 독립적으로 가공합니다.

attention 과 KV cache
attention 에서 각 토큰은 다른 토큰들에 주목합니다. fox 라는 토큰을 위해 세 벡터를 만듭니다. Q (query, 문맥에서 무엇을 알고 싶은가), K (key, 내 라벨과 내가 담은 정보의 종류), V (value, 라벨이 맞으면 내 실제 내용). 셋은 토큰의 벡터 표현을 각각 Q, K, V projection 에 통과시켜 얻습니다.
fox 의 attention 을 계산하려면, fox 의 Q 를 지금까지의 모든 토큰 (자기 자신 포함) 의 K 와 dot product 합니다. dot product 가 크면 관련성이 높고 작으면 낮습니다. 그 점수를 key dimension 의 제곱근으로 나눠 수치를 안정시키고 softmax 로 합이 1 인 가중치로 바꿉니다. 마지막으로 그 가중치로 모든 V 벡터의 가중합을 구하면, 문맥이 더해진 하나의 벡터가 나옵니다. 이 벡터가 네 번째 linear layer 인 O projection 을 통과해 attention block 의 최종 출력이 됩니다.

여기서 핵심 관찰 두 가지가 나옵니다. 새 토큰을 만들려면 그 앞 모든 토큰의 K, V 가 필요합니다. Q 는 현재 토큰에만 필요하지만 K, V 는 전체 history 가 필요합니다. 이게 KV cache 의 토대입니다.
흐름은 이렇습니다. 네 번째 토큰 fox 를 생성하면 Q4, K4, V4 를 계산하고, attention 은 Q4 와 이전 모든 토큰의 K, V 를 씁니다. 다음 토큰 jumps 를 생성할 때는 새 Q5, K5, V5 만 필요하고 토큰 1-4 의 K, V 는 이전 단계에서 안 바뀌었습니다. 다시 계산하면 낭비이므로 cache 합니다. 매 추론 스텝마다 새 토큰의 K, V 만 계산해 cache 에 덧붙입니다. 이게 KV cache 이고, 이 절약이 N 개 layer 마다 일어나므로 N 배로 곱해집니다.

KV cache 크기 계산
토큰마다 모든 layer 에서 K, V 벡터를 저장합니다. 한 가지 디테일: 각 layer 는 토큰당 K, V 집합을 여러 개 병렬로 계산해 다양한 관계를 잡습니다. 이걸 KV head 라 하고 Llama 3 70B 는 8 개입니다. 토큰당 KV cache 공식:
2 x (number of layers) x (number of KV heads) x (head dimension) x (dtype bytes)
2 는 K 와 V 둘 다라서, Llama 3 70B 는 layer 80, KV head 8, head dimension 128, 출시 precision 에서 숫자당 2 byte 입니다. 대입하면 2 x 80 x 8 x 128 x 2 로 토큰당 약 320 KB. 이걸 실제 배포 context length 로 키우면:
- 2,000 토큰 (일반 chat turn): 약 640 MB
- 8,000 토큰 (표준 프로덕션 tier): 약 2.5 GB
- 32,000 토큰 (긴 문서/코드베이스): 약 10 GB
- 128,000 토큰 (Llama 3 최대): 약 40 GB
마지막 숫자가 중요합니다. 출시 시 모델 weights 가 약 140 GB 인데, 128,000 토큰 요청 하나가 그 위에 약 40 GB 의 KV cache 를 더 요구합니다. 모델 자체 크기의 거의 1/3 을 사용자 한 명의 대화에 씁니다. 게다가 이건 요청당입니다. 긴 context 사용자 10 명을 동시에 받으면 모델 위에 400 GB 넘는 KV cache 만 필요합니다. KV cache 가 현대 LLM 추론에서 메모리의 지배적 관심사인 이유입니다. GPU 메모리에 살고, sequence length 와 동시 요청 수에 선형으로 자라며, 이걸 효율적으로 관리하는 게 프로덕션 inference server 의 가장 큰 일입니다.

GPU 메모리 계층
여기서 용어 하나. 임의 차원의 숫자 배열을 통틀어 tensor 라 합니다. 스칼라는 0 차원, 벡터는 1 차원, 행렬은 2 차원 tensor. Q, K, V, KV cache, weights 가 전부 tensor 입니다.
GPU 메모리는 세 단계이고 크기와 속도가 극단적으로 다릅니다.
- CPU DRAM (host 메모리): GPU 는 독립 컴퓨터가 아니라 host 에 꽂혀 있습니다. 크기는 크지만 (서버는 1 TB 이상) GPU 에서 멀고, host 에서 GPU 로 옮기는 건 느립니다.
- HBM (high bandwidth memory): 흔히 말하는 GPU 메모리/VRAM. GPU 카드 위에 있어 compute 에 가깝고, host 보다 작지만 훨씬 빠릅니다.
- SRAM: GPU compute unit 바로 옆 on-chip 메모리. compute unit 은 tensor core 라 부르며 행렬 곱을 극도로 빠르게 합니다. SRAM 은 아주 작지만 엄청나게 빠릅니다.
NVIDIA A100 기준 숫자: SRAM 약 20 MB 에 약 19 TB/s, HBM 40 GB 에 1.5 TB/s, host 와 GPU 사이 전송은 약 12 GB/s. compute 에 가까울수록 빠르지만 그만큼 적습니다.

tensor 의 이동은 이렇습니다. model weights 는 시작 시 한 번 디스크/DRAM 에서 HBM 으로 올라와 서버 수명 내내 머뭅니다. KV cache 도 HBM 에 살며 요청이 토큰을 처리할수록 자랍니다. 매 forward pass 마다 weights 와 KV cache 의 작은 조각이 HBM 에서 SRAM 으로 당겨져 tensor core 가 계산합니다. Q, K, V 나 attention 출력 같은 transient tensor 는 SRAM 에서 계산되고 바로 쓰인 뒤 버려집니다.
추론 속도를 지배하는 건 두 가지입니다. HBM 에서 SRAM 으로 데이터를 얼마나 빨리 옮기는가, 그리고 tensor core 가 그 데이터를 얼마나 빨리 계산하는가. 이후 모든 최적화는 같은 원칙으로 돌아옵니다. 데이터를 덜 옮기거나, 더 효율적으로 옮기거나, 메모리를 더 잘 관리하거나.
Model optimization: quantization
모델은 매년 약 2 배씩 커지는데 GPU 메모리는 그만큼 빨리 안 커집니다. 모델이 할 수 있는 일과 하드웨어가 돌릴 수 있는 일 사이 격차가 벌어집니다. compression 이 필요한 이유입니다.

quantization 의 아이디어는 단순합니다. weight 를 pi 처럼 3.14159... 로 정밀하게 저장하는 대신 3.14 로 더 적은 bit 에 저장합니다. 요즘 LLM 은 대개 BF16 (brain float 16, 숫자당 16 bit) 으로 출시됩니다. quantization 은 이를 FP8, INT8, INT4 같은 더 낮은 bit 포맷으로 바꿉니다.
포맷 이름 정리:
- FP (floating-point): 3.14 같은 소수.
- BF (brain floating-point): BF16 은 Google 이 만든 16 bit 포맷으로 FP16 보다 표현 범위가 넓어 큰 모델에 안정적.
- INT (integer): 3 이나 -127 같은 정수.
FP32 는 넓은 범위에 촘촘한 정밀도, BF16 은 같은 범위에 덜 촘촘, FP16 / INT8 로 내려가면 범위가 줄고 표현 가능한 값 사이 간격이 커집니다. 정밀도를 크기와 맞바꾸는 셈입니다.
quantization 은 두 가지에 적용할 수 있습니다. weight (학습된 파라미터) 와 activation (forward pass 중 계산되는 중간값). 추가로 sparsification 도 있습니다. 예측에 가장 적게 기여하는 weight 를 0 으로 만들어 아예 건너뜁니다. 흔한 방식은 2:4 sparsity 로, weight tensor 의 매 4 개 값 중 2 개를 0 으로 둡니다.
메모리 절감 예시
Llama 4 Scout (1,090 억 파라미터) 로 보면, BFloat16 은 파라미터당 2 byte 라 109B x 2 로 weights 만 약 220 GB, 최소 80 GB GPU 세 개가 필요합니다. INT8 / FP8 (1 byte) 로 내리면 약 109 GB 로 절반, GPU 두 개. INT4 / FP4 로 더 내리면 약 55 GB 로 원본의 75% 감소, GPU 한 개. 같은 모델인데 메모리는 1/4.

모델 어디에 적용되나
quantization 은 transformer block 안의 linear layer 를 노립니다. embedding layer 와 LM head 같은 다른 부분은 정확도 보존을 위해 보통 제외합니다. linear layer 를 노리는 이유는 forward pass 시간 대부분이 거기서 쓰이고 (행렬 곱), weight 대부분도 거기 있어 효과가 가장 크기 때문입니다.
linear layer 안에서 quantize 할 대상은 둘입니다. weight, 그리고 거기 흘러드는 input activation. input activation 은 linear layer 에서 weight 와 곱해지는 tensor 입니다. 예를 들어 attention 에서 fox 의 표현이 Q, K, V 를 만들려고 linear weight 로 흘러드는 것, 또는 가중합 출력이 O projection 으로 흘러드는 것이 전부 input activation 입니다.
weight-only vs weight+activation
quantization 은 메모리만 줄이는 게 아니라 GPU 메모리 계층에 직접 매핑되는 두 효과가 있습니다.
- quantized weight -> 데이터 이동 latency 감소. 매 forward pass 마다 weight 를 HBM 에서 SRAM 으로 당기는데, 16 bit 대신 8 bit 면 옮길 데이터가 절반.
- quantized activation -> tensor core throughput 증가. tensor core 는 낮은 precision 에서 초당 더 많은 연산을 합니다. Hopper, Ada Lovelace 이후 GPU 는 FP8 tensor core 가, 이전 Ampere 는 INT8 tensor core 가 같은 역할을 합니다.
여기서 선택지가 갈립니다.
- Weight-Only (예: W8A16): weight 만 INT8 로, activation 은 BF16 유지. 추론 시 weight 를 압축된 형태로 HBM 에서 SRAM 으로 옮긴 뒤 곱셈 직전에 BF16 으로 dequantize. 데이터 이동만 이득이고 tensor core speedup 은 없습니다.
- Weight+Activation (예: W8A8): 둘 다 quantize. 계산 자체가 낮은 precision tensor core 에서 돕니다. HBM->SRAM 데이터도 줄고 tensor core 도 빨라져 메모리 비용과 compute 비용을 둘 다 줄입니다.
정리하면 weight quantization 은 추론의 데이터 이동 부분을, activation quantization 은 compute 부분을 빠르게 합니다. 둘 다 해야 완전한 speedup 이 풀립니다.
실전 이득은 다섯 가지입니다. GPU 자원 감소, 배포 비용 감소, latency 감소, throughput 과 context 길이 증가 (weight 가 작아져 KV cache 에 더 많은 동시 사용자), 에너지 소비 감소.
정확도는 떨어지지 않나
RAG 같은 long-input workload (system prompt 50 + question 20 + retrieved context 900 = 입력 1,024 토큰, 출력 약 128 토큰) 로 본 결과, Llama 3 70B 를 H100 두 개에서 FP16 vs FP8 (weight+activation quantize) 비교 시 FP16 throughput 은 약 158 tokens/s 에서 정체되는데 FP8 은 474 까지 올라 약 3 배 이상. TTFT 도 부하가 오르면 FP16 은 30,000 ms 넘게 터지는데 FP8 은 약 4,800 ms 수준으로, 고부하에서 약 67 배 감소.
그럼 정밀도를 버리니 모델이 나빠지나. 제대로 하면 사실상 아니오입니다. 단, "제대로" 가 중요합니다. 모든 숫자를 그냥 몇 bit 로 반올림하는 naive quantization 은 모델을 해칩니다. 실전에서 통하는 건 calibrated 기법 (GPTQ, AWQ, SmoothQuant) 입니다. 작은 대표 데이터셋으로 어떤 weight 와 값이 가장 중요한지 파악하고 그걸 보호하면서 quantize 합니다.
세 reasoning 벤치마크 (AIME 2024, MATH-500, GPQA-Diamond) 의 average pass@1 로 보면, Llama-8B 부터 Qwen-32B 까지 BF16 / FP W8A8 / INT W8A8 / INT W4A16 네 막대가 거의 같은 높이입니다. Qwen-14B 예시: 원본 BF16 은 73.6, 가장 공격적인 INT W4A16 (4-bit weight) 은 72.8 로 1 점 미만 하락에 weight 는 4 배 축소. 같은 모델의 FP W8A8 은 74.3 으로 오히려 약간 높은데, 이건 quantization 이 똑똑해진 게 아니라 run 간 random variation 일 뿐입니다. 요지는 명확합니다. 제대로 하면 quantization 은 속도와 메모리 이득을 모델 품질 손해 없이 줍니다.
quantization 알고리즘
LLM Compressor 가 제공하는 주요 알고리즘:
- Round-to-nearest (RTN): 가장 단순. 각 weight 를 목표 precision 의 가장 가까운 값으로 반올림. calibration 데이터 불필요, 매우 빠름. 단 INT4 같은 낮은 bit 에서 정확도 저하. 좋은 baseline 이지만 프로덕션용은 아님.
- AWQ (activation-aware weight quantization): 모든 weight 가 똑같이 중요하진 않다는 관찰에 기반. calibration 데이터를 한 번 통과시켜 activation 크기를 보고, 큰 activation 에 대응하는 weight 는 조심히 다루고 나머지는 공격적으로 압축. GPTQ 보다 가볍고 빠르며 VRAM 도 덜 씀. NVIDIA 하드웨어에서 정확도/속도 균형이 좋음.
- GPTQ: 수학적으로 더 엄밀. quantize 로 생기는 error 를 남은 weight 에서 보상해 전체 출력 변화를 최소화. loss 의 Hessian (각 weight 변화에 출력이 얼마나 민감한지, curvature 측정) 을 계산하고, layer 별로 weight 를 하나씩 quantize 하며 남은 weight 를 갱신해 보상. Hessian 계산/역행렬이 비싸 compute 와 메모리가 더 들지만 정확도가 매우 높고 (벤치마크에 따라 AWQ 보다 나음), 가장 널리 지원되어 모델 공유에 안전한 선택.
- Sparse-GPT: sparsification 담당. NVIDIA H100 같은 특정 하드웨어의 고유 케이스에서.
추가로 압축 전에 weight/activation 을 재배치해 정보 손실을 줄이는 transform 도 있습니다. smoothing 은 모델의 spike 를 평탄하게 하고, transformation (QuIP 의 Hadamard rotation, SpinQuant 의 rotation) 은 회전을 적용해 outlier 를 줄입니다. modifier 는 체이닝됩니다. 예를 들어 W8A8 에서 GPTQ 앞에 SmoothQuant 를 두면 정확도가 좋아집니다.
실습: LLM Compressor 로 모델 압축
llm-compressor 는 vLLM 프로젝트의 프로덕션 quantization 툴킷입니다. 학습된 모델을 한 번의 pass 로 precision 을 낮춥니다 (재학습 불필요). 핵심 API 는 oneshot 으로, 모델 + calibration dataset + recipe 를 받아 단일 pass 로 압축합니다. "oneshot" 이라는 이름이 그 single pass 를 뜻합니다. 결과물은 vLLM 이 바로 서빙할 수 있습니다.
압축 단계는 넷입니다. 모델 선택 (Hugging Face 등) -> 알고리즘 선택 (압축 속도와 정확도 유지의 tradeoff) -> quantization scheme 선택 (weight/activation precision) -> vLLM 으로 추론.
scheme 표:
| Scheme | Weights | Activations | quantized layer 감소 | 품질 영향 |
|---|---|---|---|---|
| W8A16 | 8-bit | 16-bit | 약 50% | 최소 |
| W4A16 | 4-bit | 16-bit | 약 75% | 낮음-중간 |
| W8A8 | 8-bit | 8-bit | 약 50% | 낮음 |
| W4A8 | 4-bit | 8-bit | 약 75% | 중간 |
강의 실습은 Qwen3-0.6B 에 GPTQ 로 W4A16 을 적용합니다. recipe 는 Linear layer 를 target 하고 lm_head 는 ignore 합니다 (출력 layer 는 정밀하게 유지). calibration 데이터는 WikiText-2 (Wikipedia 기사 표준 벤치마크. 나중 perplexity 평가에도 같은 데이터셋 사용). num_calibration_samples=256 이 기본으로 무난한데, 수백 개를 넘으면 정확도 이득은 작아지고 runtime 만 늘어납니다. max_seq_length 는 샘플당 최대 토큰 길이로, 길수록 quantizer 가 현실적 context 에서 weight 가 어떻게 동작하는지 봅니다.
from llmcompressor import oneshot from llmcompressor.modifiers.quantization import GPTQModifier recipe = GPTQModifier(scheme="W4A16", targets="Linear", ignore=["lm_head"]) oneshot( model="Qwen/Qwen3-0.6B", dataset="wikitext", dataset_config_name="wikitext-2-raw-v1", recipe=recipe, output_dir=OUTPUT_DIR, max_seq_length=4096, num_calibration_samples=256, )
42% 의 함정
16-bit 에서 4-bit 로 갔으니 75% 감소를 기대하지만, 실제 감소는 42% 입니다. linear layer weight 만 INT4 로 quantize 되고 LM head 와 normalization layer 는 높은 precision 으로 남기 때문입니다. 4 배 압축은 모델을 지배하는 linear layer 에만 적용되고, quantize 안 된 부분이 전체 감소율을 끌어내립니다. 이 비율은 큰 모델일수록 좋아집니다. linear weight 가 전체에서 더 큰 비중이라, 같은 방식으로 quantize 한 70B 모델은 이론적 4 배에 훨씬 가까워집니다.
perplexity 로 검증
작은 파일은 모델이 여전히 동작해야 의미가 있습니다. 같은 prompt ("Machine learning is a branch of") 로 두 모델을 비교하면 quantized 출력이 baseline 과 비슷합니다 (단어 하나하나 같진 않아도). 수치로는 perplexity 를 봅니다. 모델이 텍스트를 얼마나 잘 예측하는지 재는 표준 지표로, 낮을수록 좋습니다. quantization 이 모델을 망가뜨렸다면 perplexity 가 눈에 띄게 높아집니다.
sliding window 로 wikitext-2 의 held-out 부분을 통과시켜, 각 위치에서 예측 분포와 실제 다음 토큰 사이 cross-entropy loss 를 계산하고 평균 loss 를 exponentiate 하면 perplexity 가 나옵니다. 실습 결과: quantized 35.48, base 32.79 로 약 8% 증가. 대부분의 프로덕션 배포에서 몇 % 의 perplexity 증가는 모델 크기 감소와 인프라 절감 대비 충분히 값집니다.
Inference optimization: vLLM 의 세 기법
모델을 줄이는 건 절반입니다. 나머지 절반은 GPU 가 놀거나 메모리가 터지지 않게 다수 사용자에게 서빙하는 것입니다. vLLM 을 떠받치는 세 기법을 봅니다.
continuous batching
먼저 batching 이 왜 중요한가. 생성은 반복적이라 토큰마다 full forward pass 가 필요하고, 그건 모델 weight 전체를 HBM 에서 compute unit 으로 당기는 것을 뜻합니다. 한 번에 한 요청만 처리하면 GPU 가 극도로 저활용됩니다. 토큰 하나에 필요한 compute 는 작은데 모델 전체를 메모리로 옮기는 full cost 를 매번 치르기 때문입니다. tensor core 는 대부분 데이터를 기다리며 놉니다. 해법은 batching 입니다. weight 를 한 번 읽어 여러 사용자에게 동시에 씁니다. 메모리 비용은 같은데 읽기당 훨씬 많은 일을 합니다.
가장 단순한 static batching 은 고정 그룹을 모아 함께 처리하고 전부 끝날 때까지 기다린 뒤 다음 batch 를 시작합니다. BERT 나 YOLO 처럼 입출력 크기가 예측 가능한 모델엔 잘 맞습니다. 하지만 LLM 은 이 가정을 깹니다. 한 사용자는 "2 + 2" 로 5 토큰 답을, 다른 사용자는 2,000 단어 에세이를 요청합니다. context 길이가 예측 불가라, 같은 batch 의 짧은 요청이 끝나도 그 슬롯은 가장 긴 요청이 끝날 때까지 놀고, GPU 는 이미 완료된 요청에 capacity 를 낭비합니다.
continuous batching 은 scheduler 가 토큰 레벨에서 동작해 이를 해결합니다. 요청이 끝나는 순간 대기 중이던 새 요청이 즉시 그 슬롯을 차지합니다. batch 가 결코 놀지 않아 GPU 가 꽉 찬 상태로 유지됩니다.
PagedAttention
batching 으로 GPU 를 바쁘게 해도, 동시 요청 수를 제한하는 둘째 자원이 있습니다. GPU 메모리, 그 최대 소비자가 KV cache 입니다. KV cache 는 동적으로 자라고 줄며, 요청이 얼마나 길지 미리 모릅니다.
이전 시스템은 요청마다 최대 길이 (예: 2,048 슬롯) 의 연속 블록 하나를 미리 할당했습니다. 여기서 세 가지 낭비가 생깁니다.
- internal fragmentation: 할당 안에서 끝내 안 쓰이는 빈 슬롯.
- external fragmentation: 할당들 사이의 물리적으로 비었지만 새 요청의 사전할당 청크엔 너무 작은 공간.
- over-reservation: 요청 A 가 결국 쓸 슬롯조차 수명 대부분 동안 비어 예약된 채 다른 요청을 막음.
vLLM 으로 이어진 논문은 KV cache 메모리의 20-40% 만 실제 토큰 저장에 쓰였고 나머지는 fragmentation 과 over-reservation 으로 사라졌다고 보고합니다. 이론상 메모리는 충분한데 실제로는 대부분 잠겨 있습니다.
PagedAttention 은 KV cache 를 하나의 큰 연속 블록 대신 고정 크기 블록 (page) 으로 쪼갭니다. 각 블록은 소수 토큰의 K, V 를 담고, 시스템은 각 요청의 토큰을 물리 블록에 매핑하는 block table 을 유지합니다. OS 의 virtual memory 와 paging 에서 빌린 아이디어입니다. OS 가 메모리를 작은 page 로 쪼개 빈 곳에 흩고 page table 로 추적하듯, PagedAttention 도 작은 블록을 GPU 메모리 곳곳에 흩고 lookup table 로 꿰맵니다.
동작 예: "Artificial Intelligence is" 가 들어오면 빈 Block 3 을 잡아 세 토큰의 KV 를 저장하고 block table 에 기록합니다. 다음 토큰 "the" 는 Block 3 의 빈 슬롯에 들어갑니다. "future" 생성 시 Block 3 이 차면 새 Block 6 을 잡습니다. Block 3 과 Block 6 은 물리적으로 떨어져 있어도 됩니다. attention 시 현재 토큰의 query 로 이전 모든 토큰의 K, V 에 주목해야 하는데, block table 을 읽어 Block 3 을 가져와 계산하고 Block 6 도 같은 식으로 처리합니다. 비연속 블록에 저장돼도 block table 이 꿰매주므로 attention 이 가능합니다. 사전할당도, 낭비 슬롯도, fragmentation 도 없습니다. 두 요청이 같은 물리 메모리 풀을 공유하며 각자 필요한 만큼만 받습니다.
prefix caching
요청들이 같은 prefix (예: system prompt) 를 공유하면 KV cache 블록도 공유합니다. 한 번 계산해 사용자 간 재사용합니다. 두 가지 흔한 패턴:
- 사용자 간 공유 prompt: 세 사용자가 다른 질문을 보내도 같은 system prompt 를 거칩니다. prefix caching 없으면 그 prompt 의 KV cache 를 매번 다시 계산하지만, 있으면 한 번 계산해 모두에게 재사용. few-shot 예시나 공유 RAG context 도 같습니다.
- multi-turn 대화: 다음 prompt 는 round 1 전체 + 새 질문입니다. round 1 부분이 동일하므로 그 KV cache 를 다시 계산하지 않고 메모리에서 바로 가져오고, 모델은 새 토큰에만 새 작업을 합니다.
cache hit rate 가 오르면 throughput 도 따라 오릅니다. 75% hit rate 에서 throughput 이 약 4 배입니다. 다시 안 해도 되는 compute 입니다.
이 셋 (continuous batching, PagedAttention, prefix caching) 이 vLLM 에 모입니다. 2025 년 1 월 기준 일일 설치 10 만, 2024 년 사용량 10 배 성장, GitHub 상위 AI/ML 저장소. Llama, Qwen, DeepSeek, Gemma, Mistral, Granite 등 어떤 모델이든, NVIDIA GPU, AMD Instinct, Intel Gaudi, Google TPU, AWS Neuron, IBM Spyre 등 어떤 accelerator 든, edge / private cloud / public cloud 어디든 지원합니다.
실습: vLLM 서빙
모델을 서빙하는 명령은 한 줄입니다.
vllm serve Qwen/Qwen3-0.6B --dtype=bfloat16 --max-model-len 4096
vllm serve: 내장 inference server 실행. PagedAttention, continuous batching, prefix caching 가 기본 활성화된 채로 weight 를 엔진에 로드하고 port 8000 에 HTTP 로 노출.- 모델 식별자: Hugging Face Hub 의 ID. 첫 실행 시 weight, tokenizer, config 를 로컬 cache 로 받아 로드하고 이후엔 cache 재사용.
--dtype=bfloat16: BF16 으로 로드.--max-model-len 4096: context window (prompt + generation) 를 4,096 토큰으로 제한. vLLM 이 이걸로 KV cache 블록 풀 크기를 미리 잡으므로, 예상 workload 에 맞춰 합리적으로 두면 안 쓸 메모리 예약을 피합니다.
vLLM 은 모델을 OpenAI-compatible HTTP API 로 감쌉니다. /v1/models, /v1/chat/completions, /v1/completions, /v1/embeddings 등 OpenAI SDK 가 호출하는 같은 route 를 같은 요청/응답 형태로 구현합니다. 그래서 표준 openai 파이썬 client 를 base_url 만 localhost 로 바꿔 그대로 씁니다. hosted 모델로 프로토타이핑하다 self-hosted 로 갈아탈 때 애플리케이션을 다시 쓸 필요가 없습니다.
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
logprobs 로 모델 들여다보기
직접 vLLM 을 돌리면 모델의 결정 과정을 볼 수 있습니다. logprobs=True, top_logprobs=5 로 요청하면 각 토큰의 log probability 와 고려했던 대안들을 받습니다. "The capital of France is" 에 대해 모델이 Paris 를 고를 때 92.5% confidence 를 보였다면, 모델이 확신하는지 추측하는지를 알 수 있습니다.
metrics 로 최적화 관찰
vLLM 은 Prometheus 호환 /metrics endpoint 를 노출합니다. 주요 지표:
num_requests_running / waiting: active vs queued 요청 수.gpu_cache_usage_perc(또는cpu_cache_usage_perc): KV cache 메모리 압력.prompt_tokens_total / generation_tokens_total: 누적 토큰 수.
5 개 요청을 동시에 보내면 continuous batching 으로 함께 처리되어 하나씩 보내는 것보다 빠릅니다. PagedAttention 덕에 KV cache 가 고정 크기 블록으로 흩어져 있다가, 요청이 끝나면 그 블록이 즉시 재사용 가능해집니다. 같은 system prompt 로 5 개 질문을 보내면 첫 요청만 full prefill cost 를 치르고, 이후엔 vLLM 이 공유 prefix 를 인식해 재계산을 건너뜁니다. prefix_cache_queries_total 이 요청마다 증가하는 것으로 확인됩니다 (실습에서 235 에서 550 으로). 짧은 prompt 에선 절감이 작지만, 수천 토큰의 instruction 이나 few-shot 이 있는 프로덕션에선 막대한 compute 를 없앱니다.
참고로 Qwen3 은 thinking mode (<think>...</think> chain-of-thought) 를 지원합니다. 더 나은 답을 주지만 토큰을 훨씬 많이 써서 KV cache, compute, 응답 길이가 늘어납니다. 실습에선 enable_thinking: False 로 꺼서 응답을 짧게 유지합니다.
측정: 벤치마킹과 평가
모델을 최적화하고 서빙했어도, 요구사항을 충족하는지 어떻게 아는가. 배포가 충분히 빠른가, 응답이 충분히 좋은가. 두 가지 측정이 있습니다. model evaluation 은 정확도, 안전성, 작업 적합성을 아우르는 넓은 과정이고, model benchmarking 은 그 안에서 정해진 데이터셋/작업/모델 대비 객관적 지표로 비교하는 것입니다. evaluation 이 "이 모델이 내 필요에 충분한가" 라면 benchmarking 은 그 답을 얻는 도구입니다.
SLO 를 먼저 정한다
벤치마크 전에 무엇과 비교할지를 알아야 합니다. use case 마다 목표가 다릅니다.
- E-commerce 챗봇: 반응성이 생명. TTFT 200 ms 미만, ITL 50 ms 미만, 99th percentile 에서 유지 (요청의 99% 가 충족해야).
- RAG 시스템: 근거 있는 답을 위해 조금 더 기다릴 의향이 있음. TTFT 300 ms, ITL 100 ms, end-to-end 3 초 미만.
같은 지표라도 임계값이 다릅니다. SLO 를 벤치마크 전에 정의해야 숫자가 의미를 갖습니다.
GuideLLM 으로 성능 측정
GuideLLM 은 vLLM 프로젝트의 벤치마킹 툴로, inference server 에 통제된 부하를 걸고 돌아오는 걸 측정합니다. 범용 load tester 는 request latency 를 한 숫자로 재지만, GuideLLM 은 streaming 응답을 이해해 TTFT, ITL 등 SLO 에 실제로 중요한 지표를 잡습니다. CLI 로 ad hoc 실행하거나 CI 에 엮어 regression 을 자동 포착할 수 있습니다.
벤치마킹이 값진 네 시나리오:
- pre-deployment: 내 하드웨어에서 필요한 품질로 돌아가는가. H200 에서 챗봇에 Llama 3.1 8B vs 70B 중 뭘 쓸까.
- cost / capacity planning: 모델을 정했으면 하드웨어가 얼마나 필요한가. 서버당 throughput 이 프로비저닝 수와 비용으로 직결.
- regression / A/B testing: quantize, 버전 교체, 서빙 설정 튜닝이 성능을 바꿉니다. INT8 버전이 baseline 대비 얼마나 더 받나.
- hardware evaluation: 성능이 급격히 나빠지기 전 최대 RPS 는. autoscaling 과 정직한 capacity 한계 설정의 기준.
GuideLLM 의 다섯 트래픽 패턴:
- synchronous: 한 번에 한 요청, 끝나면 다음. queuing 없는 단일 요청 latency 의 깨끗한 baseline.
- concurrent: 고정 수의 병렬 스트림. 동시 사용자에 서버가 버티는지.
- constant: 지정한 고정 rate 로 비동기 전송. 안정적/예측 가능 트래픽 시뮬레이션.
- poisson: 지정 rate 에 Poisson 분포의 random 간격. 실제 사용자 트래픽에 가장 근접.
- sweep: 전 스펙트럼 자동 실행. synchronous 를 바닥, concurrent 를 천장으로 사이에 여러 constant rate. 한 번에 전체 성능 곡선, capacity planning 에 좋음.
벤치마크 숫자는 항상 스택 전체 (모델 아키텍처/크기, quantize 여부, 서빙 엔진, 하드웨어, batching 설정) 가 빚어냅니다. 벤치마크는 이 변수를 바꾸지 않고 그 결합 효과를 측정할 뿐입니다.
실습은 synchronous profile 로 10 요청 (환경상 일부러 작게. 실제론 수백-수천 또는 시간 기반 run). 입력 32 prompt token, 출력 16 token, 32 개 pre-generated 샘플. 샘플 수를 요청 수 이상으로 둬야 prompt 가 반복되지 않아 prefix cache hit 가 부풀지 않습니다.
guidellm benchmark \ --target http://localhost:8000 --model $MODEL \ --processor Qwen/Qwen3-0.6B --profile synchronous \ --max-requests 10 \ --data "prompt_tokens=32,output_tokens=16,samples=32" \ --output-dir ./outputs
결과는 JSON 과 CSV 로 저장되며 mean, percentile, min/max 가 미리 계산되어 있습니다. 여기서 percentile 이 중요합니다. 평균은 outlier 를 숨깁니다. 평균 100 ms 인데 p99 가 2 초면 100 명 중 1 명이 20 배 더 기다린다는 뜻입니다. 배포 평가 시 평균만 보지 말고 항상 p95, p99 를 봅니다. 평균과 p95 사이 간격이 크면 tail latency 문제가 있고 사용자가 체감합니다.
lm_eval 로 품질 측정
성능은 절반입니다. 빠르지만 틀린 답을 주는 모델은 쓸모없습니다. lm_eval (EleutherAI 의 LM evaluation harness) 은 task performance, 즉 knowledge / reasoning / math / coding 벤치마크에서 모델이 얼마나 잘 답하는지를 잽니다. MMLU, ARC, GSM8K, TruthfulQA, Hellaswag 등 수백 개 task 를 기본 제공하고, 직접 YAML 로 custom task 도 정의할 수 있습니다. 로컬 모델과 원격 API endpoint (이미 띄운 vLLM 서버 포함) 둘 다 동작합니다.
GuideLLM 이 "이 배포가 얼마나 잘 도는가" 라면 lm_eval 은 "이 모델이 얼마나 잘 답하는가" 입니다. 실습은 같은 vLLM 서버에 local-completions backend 로 붙어 (multiple-choice 벤치마크가 요구하는 log-probability scoring 을 지원하는 completions endpoint 사용) Hellaswag 를 20 예제만 돌립니다. 결과 정확도 약 30%, 표준오차 약 10% 로 noisy 한데, 20 예제만 zero-shot 으로 돌려서 그렇습니다.
model card 읽기
quantized 모델 publisher 는 보통 model card 에 accuracy table 을 넣어, 사용자가 직접 모든 벤치마크를 돌리지 않고도 tradeoff 를 평가하게 합니다. Red Hat AI 의 Qwen3-0.6B W4A16 model card 의 recovery 컬럼은 quantized 버전이 base 정확도를 얼마나 유지하는지 보여줍니다 (의미 있는 base score 를 가진 대부분 벤치마크에서 약 93-100%). 같은 Hellaswag 가 model card 에선 base 43.04, quantized 41.02 로 95.3% recovery 입니다. 실습의 30% 보다 높은 건, model card 가 약 10,000 예제 전체를 10-shot 으로 돌리기 때문입니다.
결정 내리기
이제 증거가 세 갈래입니다. GuideLLM (배포가 어떻게 도는가 - latency, throughput, consistency), lm_eval (모델이 어떻게 답하는가 - 직접 돌린 task 정확도), model card (publisher 가 여러 벤치마크로 평가한 결과). quantized 모델 배포 결정엔 두 차원이 다 필요합니다. throughput 을 2 배로 올려도 정확도가 15% 떨어지면 안 할 수도 있고, 정확한데 latency SLO 를 못 맞추면 배포 불가입니다. 이 W4A16 모델은 약 50% 크기 감소에 OpenLLM v1 평균 약 4% 정확도 손실인데, 값어치는 use case 에 달렸습니다. 중요한 task 에서 recovery 를 확인하세요.
종합
전체를 합치면 이득이 복리로 쌓입니다. naive 배포가 baseline 이고, vLLM 의 continuous batching 과 PagedAttention 을 더하면 비용이 급감하고, 그 위에 model optimization 을 얹으면 더 밀어붙입니다. 어떤 단일 기법보다 조합이 훨씬 강력합니다. performance, accuracy, cost 삼각형은 여전하지만, 제대로 된 도구와 기법은 속도를 위해 정확도를, 비용을 위해 속도를 희생하지 않고 셋 다 더 가질 수 있게 합니다.
실제 사례 둘:
- 한 데이터베이스 회사가 SQL 생성용 Llama 70B 를 고객에게 배포하려 했는데 GPU 8 개가 있었습니다. quantize 가 잘 안 돼 정확도가 나빴지만, LLM Compressor 의 W4A16 으로 baseline 99% 이상 정확도를 회복하고 GPU 요구를 8 개에서 2 개로 (75% 인프라 감소) 줄였습니다.
- 한 리테일 회사가 fine-tuned Llama-70B 로 매일 수백만 레코드의 JSON 추출을 했는데, workload 에 안 맞는 방법을 써서 처음엔 quantization 이득이 없었습니다. 올바른 최적화 (weight-only 가 아니라 batch size, concurrency 같은 hyperparameter 도 튜닝) 로 GPU 시간을 40% 줄였습니다.
요지는 쉬운 것부터 (model quantization) 시작해 workload 와 하드웨어에 맞춰 튜닝하라는 것입니다. 프로덕션에서 흔한 scheme:
- INT W4A16: 대부분의 accelerator 에서 동작. 메모리 사용과 대역폭을 줄여 일부 케이스에서 약 3.7 배 압축, latency 민감 workload 에서 최대 약 3 배 speedup.
- FP W8A8: Hopper 같은 최신 accelerator 필요. 메모리, 대역폭, compute 를 줄여 약 2 배 압축, 일반 server workload 에서 약 3 배 speedup.
하드웨어와 workload 프로파일에 따라 고릅니다. AI 생태계는 모델 패밀리, 하드웨어 벤더, 배포 타겟이 흩어져 있는데, vLLM 은 이 전부에 걸쳐 일관된 최적화 서빙 레이어를 줍니다.
다음으로 갈 곳: LLM Compressor 로 직접 quantize 해보기, vLLM 서버 띄워 최적화 실험하기, GuideLLM 으로 SLO 대비 성능 측정하기. 한 단계 더 나아가면 llm-d 가 있습니다. disaggregated AI serving 으로, LLM 추론의 prefill 과 decode 단계를 분리해 각각 독립적으로 최적화합니다. 다음 frontier 입니다.