본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: 모델 최적화 - 양자화, 배칭, KV 캐시 전략
2026년 1월 22일·AI / ML·

3장: 모델 최적화 - 양자화, 배칭, KV 캐시 전략

LLM 추론 성능을 극대화하기 위한 양자화 기법, 배칭 전략, KV 캐시 튜닝 방법을 실전 예제와 함께 체계적으로 다룹니다.

22분708자7개 섹션
mlopskubernetesinfrastructureperformance
공유
ai-deployment3 / 10
12345678910
이전2장: 모델 서빙 프레임워크 - vLLM과 TGI 심층 비교다음4장: 컨테이너화 - Docker로 AI 서비스 패키징

모델 최적화가 중요한 이유

LLM 추론의 비용과 성능은 모델 최적화에 의해 결정됩니다. 동일한 모델이라도 최적화 수준에 따라 처리량이 2~4배 차이나고, 필요한 GPU 수가 절반으로 줄어들 수 있습니다. 이는 곧 운영 비용의 차이로 직결됩니다.

모델 최적화는 크게 세 가지 축으로 나뉩니다. 첫째, 양자화(Quantization)를 통해 모델의 메모리 사용량을 줄이는 것입니다. 둘째, 배칭(Batching) 전략을 통해 GPU 연산 자원의 활용률을 높이는 것입니다. 셋째, KV 캐시 관리를 통해 메모리를 효율적으로 사용하는 것입니다. 이 세 가지는 독립적이 아니라 서로 밀접하게 연관되어 있습니다.

양자화의 원리와 종류

양자화란 무엇인가

양자화는 모델의 가중치(Weight)와 활성화(Activation)를 더 낮은 정밀도의 숫자로 표현하는 기법입니다. 예를 들어 FP16(16비트 부동소수점)으로 저장된 가중치를 INT8(8비트 정수)이나 INT4(4비트 정수)로 변환하면, 메모리 사용량이 각각 절반, 1/4로 줄어듭니다.

text
모델 메모리 사용량 계산 (파라미터 수 x 비트 / 8):
 
  Llama-3.1-8B:
    FP16: 8B x 2 bytes = 16 GB
    INT8: 8B x 1 byte  = 8 GB
    INT4: 8B x 0.5 byte = 4 GB
 
  Llama-3.1-70B:
    FP16: 70B x 2 bytes = 140 GB (A100 80GB 2장 필요)
    INT8: 70B x 1 byte  = 70 GB  (A100 80GB 1장 가능)
    INT4: 70B x 0.5 byte = 35 GB (A100 40GB 1장 가능)

양자화의 핵심 트레이드오프는 정밀도와 효율성입니다. 비트 수를 줄일수록 메모리가 절약되고 추론 속도가 빨라지지만, 모델의 출력 품질이 저하될 수 있습니다. 현대의 양자화 기법들은 이 품질 손실을 최소화하면서 최대한의 압축을 달성하는 데 초점을 맞추고 있습니다.

GPTQ (GPT Quantization)

GPTQ는 가장 널리 사용되는 사후 학습 양자화(Post-Training Quantization, PTQ) 기법 중 하나입니다. 보정 데이터셋(Calibration Dataset)을 사용하여 양자화 오차를 최소화하는 방식으로 동작합니다.

GPTQ의 핵심 아이디어는 Optimal Brain Quantization(OBQ)에 기반합니다. 각 가중치를 양자화할 때 발생하는 오차를 나머지 가중치를 조정하여 보상합니다. 이를 통해 INT4 양자화에서도 원본 FP16 모델과 거의 동등한 품질을 유지할 수 있습니다.

GPTQ 양자화 실행
python
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
 
model_id = "meta-llama/Llama-3.1-8B-Instruct"
quantize_config = BaseQuantizeConfig(
    bits=4,
    group_size=128,
    desc_act=True,
    damp_percent=0.1,
)
 
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoGPTQForCausalLM.from_pretrained(
    model_id,
    quantize_config=quantize_config,
)
 
# 보정 데이터셋 준비
calibration_data = [
    tokenizer(text, return_tensors="pt")
    for text in calibration_texts[:128]
]
 
# 양자화 실행
model.quantize(calibration_data)
 
# 저장
model.save_quantized("./llama-3.1-8b-gptq-4bit")

vLLM에서 GPTQ 모델을 서빙하려면 다음과 같이 실행합니다.

GPTQ 모델 서빙
bash
vllm serve ./llama-3.1-8b-gptq-4bit \
  --quantization gptq \
  --dtype float16 \
  --max-model-len 4096

AWQ (Activation-aware Weight Quantization)

AWQ는 활성화(Activation) 분포를 고려하여 가중치를 양자화하는 기법입니다. 모든 가중치가 동등하게 중요한 것이 아니라, 활성화 크기가 큰 채널에 연결된 가중치가 더 중요하다는 관찰에 기반합니다.

AWQ는 중요한 가중치에 대해 스케일링 팩터를 적용하여 양자화 오차를 줄입니다. GPTQ보다 양자화 속도가 빠르고, 유사하거나 더 나은 품질을 제공하는 경우가 많습니다.

AWQ 양자화 실행
python
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
 
model_id = "meta-llama/Llama-3.1-8B-Instruct"
 
model = AutoAWQForCausalLM.from_pretrained(model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)
 
quant_config = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM",
}
 
model.quantize(tokenizer, quant_config=quant_config)
model.save_quantized("./llama-3.1-8b-awq-4bit")
AWQ 모델 서빙
bash
vllm serve ./llama-3.1-8b-awq-4bit \
  --quantization awq \
  --dtype float16 \
  --max-model-len 4096

FP8 양자화

FP8(8비트 부동소수점)은 NVIDIA Hopper 아키텍처(H100) 이상에서 하드웨어 수준으로 지원되는 양자화 포맷입니다. INT8과 동일한 메모리 절감을 제공하면서, 부동소수점 특성 덕분에 품질 손실이 거의 없습니다.

FP8에는 E4M3(지수부 4비트, 가수부 3비트)와 E5M2(지수부 5비트, 가수부 2비트) 두 가지 포맷이 있습니다. 일반적으로 가중치에는 E4M3을, 기울기에는 E5M2를 사용합니다.

FP8 모델 서빙 (H100 필요)
bash
vllm serve meta-llama/Llama-3.1-70B-Instruct \
  --quantization fp8 \
  --dtype float16 \
  --tensor-parallel-size 2 \
  --max-model-len 8192
Info

FP8 양자화는 H100 이상의 GPU에서만 하드웨어 가속을 받을 수 있습니다. A100에서는 소프트웨어 에뮬레이션으로 동작하며, 이 경우 성능 이점이 제한적입니다.

양자화 기법 비교

text
양자화 기법 비교표:
 
기법     비트  품질 손실  양자화 속도  추론 속도  하드웨어 요구
------  ----  --------  ----------  --------  -----------
FP16    16    없음       해당 없음    기준선     모든 GPU
FP8     8     매우 적음  빠름        1.3-1.5x  H100+
GPTQ    4     적음       느림(시간)   1.5-2.0x  모든 GPU
AWQ     4     적음       보통(분)     1.5-2.0x  모든 GPU
GGUF    2-8   가변       빠름        가변       CPU/GPU
 
* 추론 속도는 FP16 대비 상대 속도입니다.

배칭 전략

정적 배칭의 한계

가장 단순한 배칭 방식은 정적 배칭(Static Batching)입니다. 일정 시간 동안 도착한 요청을 모아서 한번에 처리하는 방식입니다. 그러나 LLM 추론에서는 이 방식이 심각한 비효율을 초래합니다.

LLM은 자기회귀(Autoregressive) 방식으로 토큰을 하나씩 생성합니다. 배치 내 요청들의 출력 길이가 다를 때, 짧은 출력을 가진 요청은 이미 완료되었음에도 가장 긴 요청이 끝날 때까지 GPU 자원을 점유합니다.

text
정적 배칭 예시 (배치 크기 4):
 
시간 -->
요청1: [####]________________  (4 토큰 생성 후 완료, 16 토큰 동안 대기)
요청2: [############]________  (12 토큰 생성 후 완료, 8 토큰 동안 대기)
요청3: [####################] (20 토큰 생성, 전체 시간 사용)
요청4: [########]____________  (8 토큰 생성 후 완료, 12 토큰 동안 대기)
 
GPU 활용률: (4+12+20+8) / (20*4) = 55%

연속 배칭

연속 배칭(Continuous Batching, 또는 Iteration-level Batching)은 이 문제를 해결합니다. 각 생성 스텝(Iteration)마다 완료된 요청을 배치에서 제거하고, 대기 큐에서 새 요청을 즉시 투입합니다.

text
연속 배칭 예시:
 
시간 -->
슬롯1: [요청1####][요청5######][요청8##########]
슬롯2: [요청2############][요청6########]______
슬롯3: [요청3####################]______________
슬롯4: [요청4########][요청7##############]_____
 
GPU 활용률: 약 85-95%

vLLM과 TGI 모두 연속 배칭을 기본으로 사용합니다. 별도의 설정 없이 자동으로 적용됩니다.

프리필과 디코딩 분리

LLM 추론은 두 단계로 나뉩니다. 프리필(Prefill) 단계에서는 입력 토큰 전체를 한번에 처리하여 초기 KV 캐시를 생성합니다. 디코딩(Decode) 단계에서는 한 번에 하나의 토큰을 생성합니다.

이 두 단계는 연산 특성이 근본적으로 다릅니다. 프리필은 연산 집약적(Compute-bound)이며, 디코딩은 메모리 대역폭 집약적(Memory-bound)입니다. 이들을 같은 배치에서 처리하면 서로 간섭이 발생합니다.

text
프리필 vs 디코딩 특성:
 
프리필:
  - 입력 토큰 전체를 병렬 처리
  - GPU 연산 유닛을 집중 사용
  - 배치 크기 증가 시 처리량 선형 증가
 
디코딩:
  - 한 번에 1 토큰 생성
  - KV 캐시 읽기/쓰기가 병목
  - 배치 크기 증가 시 메모리 대역폭이 병목

일부 고급 배포에서는 프리필과 디코딩을 별도의 GPU 또는 인스턴스에서 처리하는 분해(Disaggregation) 전략을 사용합니다. 이를 통해 각 단계에 최적화된 자원 할당이 가능합니다.

Tip

프리필-디코딩 분리는 대규모 서비스에서만 의미가 있습니다. 소규모 배포에서는 vLLM의 기본 연속 배칭만으로도 충분한 성능을 달성할 수 있습니다.

KV 캐시 관리 전략

KV 캐시의 역할

Transformer 모델에서 어텐션(Attention) 연산은 이전에 생성된 모든 토큰의 Key와 Value 벡터를 참조합니다. 매 스텝마다 이를 재계산하면 엄청난 연산 낭비가 발생하므로, 이미 계산된 Key와 Value를 캐시에 저장하여 재사용합니다. 이것이 KV 캐시입니다.

KV 캐시의 메모리 사용량은 다음과 같이 계산됩니다.

text
KV 캐시 메모리 = 2 x 레이어 수 x 헤드 수 x 헤드 차원 x 시퀀스 길이 x 배치 크기 x 바이트
 
Llama-3.1-8B (FP16, 시퀀스 4096, 배치 32):
  = 2 x 32 x 8 x 128 x 4096 x 32 x 2 bytes
  = 약 17 GB
 
Llama-3.1-70B (FP16, 시퀀스 4096, 배치 32):
  = 2 x 80 x 8 x 128 x 4096 x 32 x 2 bytes
  = 약 42 GB

모델 가중치 자체보다 KV 캐시가 더 많은 메모리를 차지할 수 있습니다. 따라서 KV 캐시 관리는 서빙 성능의 핵심 요소입니다.

GPU 메모리 활용률 설정

vLLM에서 --gpu-memory-utilization 옵션은 GPU 메모리의 몇 퍼센트를 사용할지 결정합니다. 모델 가중치를 로드한 후 남은 메모리가 KV 캐시에 할당됩니다.

메모리 활용률 설정
bash
# 보수적 설정 (안정성 우선)
vllm serve model-name --gpu-memory-utilization 0.85
 
# 공격적 설정 (처리량 우선)
vllm serve model-name --gpu-memory-utilization 0.95
 
# 기본값
vllm serve model-name --gpu-memory-utilization 0.90

활용률을 높이면 더 많은 동시 요청을 처리할 수 있지만, OOM 위험이 증가합니다. 프로덕션 환경에서는 0.88~0.92 사이의 값을 권장합니다.

프리픽스 캐싱

동일한 시스템 프롬프트를 사용하는 요청이 많은 경우, 프리픽스 캐싱(Prefix Caching)을 통해 KV 캐시를 재사용할 수 있습니다. 시스템 프롬프트에 해당하는 KV 캐시를 한 번만 계산하고, 이후 요청에서는 이를 공유합니다.

프리픽스 캐싱 활성화
bash
vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --enable-prefix-caching \
  --max-model-len 8192

프리픽스 캐싱의 효과는 시스템 프롬프트의 길이와 요청의 다양성에 따라 달라집니다. 시스템 프롬프트가 길고(1000 토큰 이상), 대부분의 요청이 동일한 시스템 프롬프트를 사용하는 경우 TTFT가 크게 단축됩니다.

text
프리픽스 캐싱 효과 예시:
 
시스템 프롬프트 1000 토큰인 서비스:
  캐싱 미적용: 프리필 1000 + 사용자 입력 200 = 1200 토큰 처리
  캐싱 적용:   프리필 200 토큰만 처리 (시스템 프롬프트는 캐시 히트)
  TTFT 개선: 약 80% 단축

최대 시퀀스 길이 제한

--max-model-len 옵션은 KV 캐시에 직접적인 영향을 미칩니다. 모델이 지원하는 최대 컨텍스트 길이(예: 128K)를 그대로 사용하면, KV 캐시가 과도하게 할당될 수 있습니다.

실제 서비스에서 필요한 최대 길이를 분석하여 적절한 값으로 제한하는 것이 중요합니다.

컨텍스트 길이 제한
bash
# 모델 최대 128K이지만, 서비스에서는 8K면 충분한 경우
vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --max-model-len 8192
 
# 메모리 여유를 확보하여 더 많은 동시 요청 처리 가능

추론 성능 벤치마킹

최적화의 효과를 측정하려면 체계적인 벤치마킹이 필요합니다. 핵심 메트릭은 다음과 같습니다.

핵심 메트릭

TTFT(Time To First Token): 요청을 보낸 시점부터 첫 번째 토큰이 생성되기까지의 시간입니다. 사용자 체감 응답성에 직접적인 영향을 미칩니다.

TPOT(Time Per Output Token): 토큰 하나를 생성하는 데 걸리는 평균 시간입니다. 스트리밍 응답의 부드러움을 결정합니다.

처리량(Throughput): 초당 생성하는 토큰 수(tokens/s)입니다. 시스템 전체의 처리 용량을 나타냅니다.

동시 요청 수(Concurrency): GPU 메모리가 허용하는 범위 내에서 동시에 처리할 수 있는 최대 요청 수입니다.

벤치마킹 도구

vLLM에 내장된 벤치마킹 스크립트를 사용할 수 있습니다.

vLLM 벤치마크 실행
bash
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct &
 
# 벤치마크 실행
python benchmarks/benchmark_serving.py \
  --backend vllm \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --num-prompts 1000 \
  --request-rate 10 \
  --max-concurrency 64

또는 직접 벤치마킹 스크립트를 작성할 수 있습니다.

benchmark.py
python
import asyncio
import time
from openai import AsyncOpenAI
 
client = AsyncOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
)
 
async def send_request(prompt, max_tokens=256):
    start = time.perf_counter()
    tokens = []
    first_token_time = None
 
    response = await client.chat.completions.create(
        model="meta-llama/Llama-3.1-8B-Instruct",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens,
        stream=True,
    )
 
    async for chunk in response:
        if chunk.choices[0].delta.content:
            if first_token_time is None:
                first_token_time = time.perf_counter() - start
            tokens.append(chunk.choices[0].delta.content)
 
    total_time = time.perf_counter() - start
    return {
        "ttft": first_token_time,
        "total_time": total_time,
        "tokens": len(tokens),
        "tpot": (total_time - first_token_time) / max(len(tokens) - 1, 1),
    }
 
async def benchmark(num_requests=100, concurrency=16):
    prompts = ["Kubernetes의 주요 구성 요소를 설명해주세요."] * num_requests
    semaphore = asyncio.Semaphore(concurrency)
 
    async def limited_request(prompt):
        async with semaphore:
            return await send_request(prompt)
 
    results = await asyncio.gather(
        *[limited_request(p) for p in prompts]
    )
 
    avg_ttft = sum(r["ttft"] for r in results) / len(results)
    avg_tpot = sum(r["tpot"] for r in results) / len(results)
    total_tokens = sum(r["tokens"] for r in results)
    total_time = max(r["total_time"] for r in results)
 
    print(f"평균 TTFT: {avg_ttft*1000:.1f}ms")
    print(f"평균 TPOT: {avg_tpot*1000:.1f}ms")
    print(f"처리량: {total_tokens/total_time:.1f} tokens/s")
 
asyncio.run(benchmark())

최적화 적용 순서 권장 사항

모델 최적화를 적용할 때는 다음 순서를 권장합니다.

  1. 기본 설정으로 베이스라인 측정: FP16으로 서빙하며 성능 기준점을 잡습니다.
  2. 메모리 활용률 조정: --gpu-memory-utilization 값을 조정하여 동시 처리량을 최대화합니다.
  3. 프리픽스 캐싱 활성화: 시스템 프롬프트를 사용하는 경우 --enable-prefix-caching을 적용합니다.
  4. 양자화 적용: 메모리가 부족하거나 비용 절감이 필요한 경우 AWQ 또는 GPTQ를 적용합니다.
  5. 컨텍스트 길이 조정: 서비스에 필요한 최소한의 --max-model-len으로 제한합니다.
  6. 배칭 파라미터 튜닝: --max-num-seqs, --max-num-batched-tokens 값을 워크로드에 맞게 조정합니다.
Warning

양자화 적용 후에는 반드시 품질 평가를 수행해야 합니다. 특히 INT4 양자화는 일부 작업(수학, 코딩 등)에서 눈에 띄는 품질 저하가 발생할 수 있습니다. 이전 시리즈인 "LLM 애플리케이션 평가와 모니터링"에서 다룬 평가 프레임워크를 활용하는 것을 권장합니다.

정리

모델 최적화는 AI 서비스의 성능과 비용을 결정하는 핵심 단계입니다. 양자화로 메모리 사용량을 줄이고, 연속 배칭으로 GPU 활용률을 높이며, KV 캐시 관리로 동시 처리량을 극대화할 수 있습니다. 최적화 기법들은 상호 보완적이므로, 벤치마킹을 통해 자신의 워크로드에 맞는 최적 조합을 찾는 것이 중요합니다.

다음 장에서는 최적화된 모델 서빙 환경을 Docker 컨테이너로 패키징하는 방법을 다루겠습니다. GPU 지원 컨테이너의 특수한 요구사항과 효율적인 이미지 빌드 전략을 설명합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mlops#kubernetes#infrastructure#performance

관련 글

AI / ML

4장: 컨테이너화 - Docker로 AI 서비스 패키징

GPU 지원 Docker 컨테이너로 AI 서비스를 패키징하는 방법을 다루며, NVIDIA Container Toolkit 설정부터 멀티 스테이지 빌드까지 실전 기법을 소개합니다.

2026년 1월 24일·15분
AI / ML

2장: 모델 서빙 프레임워크 - vLLM과 TGI 심층 비교

LLM 추론의 핵심 엔진인 vLLM과 Text Generation Inference를 아키텍처, 성능, 기능 측면에서 심층 비교하고 적합한 선택 기준을 제시합니다.

2026년 1월 20일·19분
AI / ML

5장: Kubernetes 기초 - AI 워크로드를 위한 클러스터 설계

Kubernetes의 핵심 개념을 AI 워크로드 관점에서 설명하고, GPU 노드 구성과 AI 서비스에 적합한 클러스터 아키텍처를 설계합니다.

2026년 1월 26일·14분
이전 글2장: 모델 서빙 프레임워크 - vLLM과 TGI 심층 비교
다음 글4장: 컨테이너화 - Docker로 AI 서비스 패키징

댓글

목차

약 22분 남음
  • 모델 최적화가 중요한 이유
  • 양자화의 원리와 종류
    • 양자화란 무엇인가
    • GPTQ (GPT Quantization)
    • AWQ (Activation-aware Weight Quantization)
    • FP8 양자화
    • 양자화 기법 비교
  • 배칭 전략
    • 정적 배칭의 한계
    • 연속 배칭
    • 프리필과 디코딩 분리
  • KV 캐시 관리 전략
    • KV 캐시의 역할
    • GPU 메모리 활용률 설정
    • 프리픽스 캐싱
    • 최대 시퀀스 길이 제한
  • 추론 성능 벤치마킹
    • 핵심 메트릭
    • 벤치마킹 도구
  • 최적화 적용 순서 권장 사항
  • 정리