Skip to content
Published on

LLM 추론 최적화 완벽 가이드: vLLM, TensorRT-LLM, Speculative Decoding

Authors
  • Name
    Twitter
LLM 추론 최적화 완벽 가이드

들어가며

LLM(Large Language Model)을 학습시키는 것만큼이나 중요한 것이 바로 추론(Inference) 최적화이다. 아무리 뛰어난 모델을 만들어도, 추론 비용이 과도하거나 응답 지연이 길다면 실제 서비스에 투입하기 어렵다. 특히 70B 이상의 대형 모델을 프로덕션에서 서빙하려면, GPU 메모리 관리, 배칭 전략, 디코딩 가속, 양자화 등 다양한 최적화 기법을 종합적으로 적용해야 한다.

2025년 이후 LLM 추론 최적화 분야는 급격한 발전을 이루었다. vLLM의 PagedAttention이 KV 캐시 메모리 낭비를 4% 미만으로 줄였고, TensorRT-LLM 1.0은 안정화된 PyTorch 기반 아키텍처와 FP8/NVFP4 양자화를 통해 NVIDIA GPU에서 최고 성능을 달성했으며, Speculative Decoding은 출력 품질 손실 없이 2-3배의 속도 향상을 실현하고 있다.

이 글에서는 LLM 추론의 핵심 병목을 분석하고, vLLM, TensorRT-LLM, Speculative Decoding, KV Cache 최적화 등 주요 기술들을 실전 코드와 벤치마크를 통해 비교 분석한다. 프로덕션 환경에서의 운영 노하우와 트러블슈팅 사례까지 포함하여, LLM 추론 최적화의 전체 그림을 제공하고자 한다.

LLM 추론 파이프라인의 이해

Prefill과 Decode 단계

LLM 추론은 크게 두 단계로 나뉜다.

Prefill 단계(프롬프트 처리): 입력 프롬프트의 모든 토큰을 한 번에 병렬 처리하여 KV 캐시를 생성한다. 이 단계는 컴퓨트 바운드(compute-bound) 작업으로, GPU 연산 능력이 핵심이다.

Decode 단계(토큰 생성): 한 번에 하나의 토큰을 자기회귀적으로 생성한다. 매 스텝마다 전체 KV 캐시를 읽어야 하므로 메모리 바운드(memory-bound) 작업이다. 전체 추론 시간의 대부분을 차지한다.

# LLM 추론 파이프라인의 두 단계를 개념적으로 표현
import torch
import time

def llm_inference_pipeline(model, tokenizer, prompt, max_new_tokens=128):
    input_ids = tokenizer.encode(prompt, return_tensors="pt").to(model.device)

    # 1단계: Prefill - 입력 프롬프트 전체를 병렬 처리
    prefill_start = time.time()
    with torch.no_grad():
        outputs = model(input_ids, use_cache=True)
        past_key_values = outputs.past_key_values  # KV Cache 생성
        next_token_logits = outputs.logits[:, -1, :]
    prefill_time = time.time() - prefill_start

    # 2단계: Decode - 토큰을 하나씩 자기회귀적으로 생성
    decode_start = time.time()
    generated_tokens = []
    for step in range(max_new_tokens):
        next_token = torch.argmax(next_token_logits, dim=-1, keepdim=True)
        generated_tokens.append(next_token.item())

        with torch.no_grad():
            outputs = model(
                next_token,
                past_key_values=past_key_values,  # 캐시 재사용
                use_cache=True,
            )
            past_key_values = outputs.past_key_values
            next_token_logits = outputs.logits[:, -1, :]

        if next_token.item() == tokenizer.eos_token_id:
            break

    decode_time = time.time() - decode_start
    tokens_per_sec = len(generated_tokens) / decode_time

    print(f"Prefill 시간: {prefill_time:.3f}s (입력 {input_ids.shape[1]} 토큰)")
    print(f"Decode 시간: {decode_time:.3f}s ({len(generated_tokens)} 토큰 생성)")
    print(f"Decode 속도: {tokens_per_sec:.1f} tokens/s")

    return tokenizer.decode(generated_tokens)

핵심 성능 지표

LLM 추론 성능을 평가할 때 반드시 고려해야 할 지표는 다음과 같다.

지표설명영향 요인
TTFT (Time to First Token)첫 토큰 생성까지의 지연Prefill 속도, 큐 대기 시간
TPOT (Time Per Output Token)출력 토큰 간 간격Decode 속도, 배치 크기
Throughput (tokens/s)초당 처리 토큰 수배칭, 병렬화, 양자화
GPU Memory UtilizationGPU 메모리 사용 효율KV Cache 관리, 양자화
Latency P9999th 백분위 지연전체 시스템 안정성

Static Batching vs Continuous Batching

기존의 정적 배칭(Static Batching)은 배치 내 가장 긴 시퀀스가 완료될 때까지 모든 요청이 대기해야 했다. 이는 심각한 GPU 자원 낭비를 초래한다.

연속 배칭(Continuous Batching, 또는 Iteration-level Batching)은 각 디코딩 스텝마다 완료된 요청을 즉시 빼내고 새 요청을 추가한다. 이를 통해 GPU 활용률을 크게 높일 수 있으며, vLLM, TGI, TensorRT-LLM 등 최신 서빙 엔진은 모두 연속 배칭을 기본 지원한다.

KV Cache 최적화: PagedAttention과 FlashAttention

KV Cache의 메모리 문제

Transformer 모델의 어텐션 메커니즘은 이전 토큰의 Key와 Value 벡터를 저장하는 KV 캐시를 필요로 한다. 이 캐시의 크기는 시퀀스 길이에 비례하여 증가하며, 대형 모델에서는 GPU 메모리의 상당 부분을 차지한다.

예를 들어 Llama 3.1 70B 모델의 경우, FP16에서 단일 요청의 KV 캐시만으로도 수 GB의 메모리를 소비할 수 있다. 기존 방식에서는 각 요청에 대해 최대 시퀀스 길이만큼의 메모리를 미리 할당하므로, 실제 사용량 대비 60-80%의 메모리가 낭비되었다.

PagedAttention: 가상 메모리에서 영감을 받은 혁신

vLLM이 도입한 PagedAttention은 운영체제의 가상 메모리 페이징 기법을 KV 캐시 관리에 적용한 것이다. 핵심 아이디어는 다음과 같다.

  1. 블록 단위 관리: KV 캐시를 고정 크기의 블록으로 분할하여, 비연속적인 물리 메모리에 저장한다.
  2. 블록 테이블: 논리적 블록과 물리적 블록의 매핑을 블록 테이블로 관리한다.
  3. 동적 할당: 실제 필요할 때만 블록을 할당하므로, 메모리 낭비가 4% 미만으로 줄어든다.
  4. 메모리 공유: 빔 서치나 병렬 샘플링 시, 동일한 프롬프트의 KV 캐시 블록을 Copy-on-Write 방식으로 공유할 수 있다.

FlashAttention: IO 최적화 어텐션

FlashAttention은 GPU의 메모리 계층 구조를 고려하여 어텐션 연산을 최적화한다.

  • 타일링(Tiling): 어텐션 행렬을 작은 블록으로 나누어 SRAM에서 처리
  • 커널 퓨전: Softmax와 행렬 곱을 하나의 CUDA 커널로 통합
  • 재계산(Recomputation): 중간 결과를 저장하지 않고 필요 시 재계산하여 HBM 접근 최소화

FlashAttention-2는 원래 버전 대비 약 2배의 성능 향상을 달성했으며, FlashAttention-3는 Hopper 아키텍처(H100)에서 FP8 지원과 비동기 실행을 추가했다.

# FlashAttention과 기본 어텐션의 메모리 사용량 비교
import torch
from flash_attn import flash_attn_func

# 설정: batch=4, heads=32, seq_len=4096, head_dim=128
batch_size, num_heads, seq_len, head_dim = 4, 32, 4096, 128

q = torch.randn(batch_size, seq_len, num_heads, head_dim, dtype=torch.float16, device="cuda")
k = torch.randn(batch_size, seq_len, num_heads, head_dim, dtype=torch.float16, device="cuda")
v = torch.randn(batch_size, seq_len, num_heads, head_dim, dtype=torch.float16, device="cuda")

# 기본 어텐션: O(N^2) 메모리 필요 (어텐션 행렬 전체 저장)
# 4096 * 4096 * 32 * 4 * 2 bytes = ~4 GB

# FlashAttention: O(N) 메모리만 필요 (타일링으로 분할 처리)
output = flash_attn_func(q, k, v, causal=True)
# 메모리 사용량이 시퀀스 길이에 선형적으로 증가
print(f"FlashAttention 출력 shape: {output.shape}")

vLLM: 고성능 LLM 서빙 엔진

vLLM 개요와 아키텍처

vLLM은 UC 버클리에서 개발한 고성능 LLM 추론/서빙 엔진이다. 2025년 기준 최신 버전은 v0.17.x이며, PagedAttention을 핵심으로 다양한 최적화 기술을 통합하고 있다.

주요 특징은 다음과 같다.

  • PagedAttention 기반 KV 캐시 관리
  • Continuous Batching으로 높은 GPU 활용률
  • 텐서 병렬 처리(Tensor Parallelism) 및 파이프라인 병렬 처리 지원
  • OpenAI 호환 API 서버 내장
  • AWQ, GPTQ, FP8 등 다양한 양자화 포맷 지원
  • Prefix Caching으로 공통 프롬프트 최적화

vLLM 설치와 기본 사용법

# vLLM 설치 (CUDA 12.1 이상 필요)
pip install vllm

# OpenAI 호환 API 서버 실행
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 4 \
    --gpu-memory-utilization 0.90 \
    --max-model-len 8192 \
    --enable-prefix-caching \
    --dtype auto \
    --port 8000

# 요청 테스트
curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "meta-llama/Llama-3.1-70B-Instruct",
        "prompt": "LLM 추론 최적화의 핵심은",
        "max_tokens": 256,
        "temperature": 0.7
    }'

vLLM Python API 활용

from vllm import LLM, SamplingParams

# 모델 로드 (자동으로 PagedAttention 적용)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,
    gpu_memory_utilization=0.90,
    max_model_len=4096,
    enable_prefix_caching=True,
    quantization="awq",            # AWQ 양자화 모델 사용 시
    # enforce_eager=True,           # CUDA Graph 비활성화 (디버깅용)
)

# 샘플링 파라미터 설정
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512,
    repetition_penalty=1.1,
)

# 배치 추론 - 여러 프롬프트를 동시에 처리
prompts = [
    "Kubernetes에서 GPU 노드 스케줄링을 최적화하는 방법을 설명해주세요.",
    "Python에서 비동기 프로그래밍의 장단점은 무엇인가요?",
    "마이크로서비스 아키텍처의 장단점을 비교해주세요.",
]

outputs = llm.generate(prompts, sampling_params)

for output in outputs:
    prompt = output.prompt
    generated = output.outputs[0].text
    tokens_count = len(output.outputs[0].token_ids)
    print(f"프롬프트: {prompt[:50]}...")
    print(f"생성 토큰 수: {tokens_count}")
    print(f"응답: {generated[:200]}...\n")

vLLM 프로덕션 설정 가이드

# vllm-deployment.yaml - Kubernetes 배포 예시
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama-70b
  labels:
    app: vllm-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-server
  template:
    metadata:
      labels:
        app: vllm-server
    spec:
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.17.1
          command:
            - python
            - -m
            - vllm.entrypoints.openai.api_server
          args:
            - --model
            - meta-llama/Llama-3.1-70B-Instruct
            - --tensor-parallel-size
            - '4'
            - --gpu-memory-utilization
            - '0.90'
            - --max-model-len
            - '8192'
            - --enable-prefix-caching
            - --max-num-seqs
            - '256'
          ports:
            - containerPort: 8000
          resources:
            limits:
              nvidia.com/gpu: '4'
            requests:
              nvidia.com/gpu: '4'
              memory: '64Gi'
              cpu: '16'
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 180
            periodSeconds: 30
      nodeSelector:
        nvidia.com/gpu.product: A100-SXM4-80GB
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-service
spec:
  selector:
    app: vllm-server
  ports:
    - port: 80
      targetPort: 8000
  type: ClusterIP

TensorRT-LLM: NVIDIA 최적화 추론

TensorRT-LLM 1.0의 핵심 변화

TensorRT-LLM 1.0은 두 가지 중요한 변화를 가져왔다. 첫째, PyTorch 기반 아키텍처가 안정화되어 기본 경험이 되었고, 둘째 LLM API가 안정화되었다. 이전 버전에서 필요했던 복잡한 엔진 빌드 과정이 크게 단순화되었다.

주요 최적화 기능은 다음과 같다.

  • FP8 및 NVFP4 양자화 지원
  • Disaggregated Serving(분리형 서빙)
  • Wide Expert Parallelism(EP) 등 병렬화 기법
  • EAGLE-3 및 Multi-Token Prediction 기반 Speculative Decoding
  • DeepSeek V3/R1 모델 지원

TensorRT-LLM 설치와 추론

# TensorRT-LLM 설치 (Docker 권장)
docker pull nvcr.io/nvidia/tensorrt-llm:latest

# 또는 pip 설치
pip install tensorrt-llm

# Llama 모델 체크포인트 변환 및 엔진 빌드
# 1단계: HuggingFace 모델을 TensorRT-LLM 형식으로 변환
python convert_checkpoint.py \
    --model_dir /models/Llama-3.1-70B-Instruct \
    --output_dir /engines/llama-70b-ckpt \
    --dtype float16 \
    --tp_size 4

# 2단계: TensorRT 엔진 빌드
trtllm-build \
    --checkpoint_dir /engines/llama-70b-ckpt \
    --output_dir /engines/llama-70b-engine \
    --gemm_plugin float16 \
    --max_batch_size 64 \
    --max_input_len 4096 \
    --max_seq_len 8192 \
    --paged_kv_cache enable \
    --use_paged_context_fmha enable \
    --multiple_profiles enable

# 3단계: Triton Inference Server로 서빙
docker run --gpus all -p 8000:8000 \
    -v /engines:/engines \
    nvcr.io/nvidia/tritonserver:latest \
    tritonserver --model-repository=/engines/model_repo

TensorRT-LLM Python API 사용

import tensorrt_llm
from tensorrt_llm import LLM, SamplingParams, KvCacheConfig

# KV Cache 설정
kv_cache_config = KvCacheConfig(
    free_gpu_memory_fraction=0.85,
    enable_block_reuse=True,
)

# 모델 로드 (HuggingFace 모델에서 직접 빌드 가능)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,
    kv_cache_config=kv_cache_config,
)

# 추론 실행
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512,
)

prompts = [
    "LLM 추론 최적화에서 가장 중요한 요소를 설명해주세요.",
    "TensorRT-LLM의 장점과 한계를 비교해주세요.",
]

outputs = llm.generate(prompts, sampling_params=sampling_params)

for output in outputs:
    print(f"생성 결과: {output.outputs[0].text[:200]}")

Speculative Decoding: 초안-검증 기반 가속

작동 원리

Speculative Decoding(투기적 디코딩)은 2022년 Google의 논문에서 제안된 기법으로, 출력 품질의 손실 없이 추론 속도를 높이는 획기적인 방법이다.

핵심 아이디어는 다음과 같다.

  1. 드래프트 모델(Draft Model): 작고 빠른 모델이 K개의 토큰을 미리 생성(추측)한다.
  2. 타겟 모델(Target Model): 크고 정확한 모델이 K개 토큰을 한 번의 forward pass로 병렬 검증한다.
  3. 수락/거절 판정: 타겟 모델의 확률 분포와 비교하여, 일치하는 토큰은 수락하고 불일치하는 지점부터 타겟 모델이 올바른 토큰을 생성한다.

이 방식의 핵심은 검증이 생성보다 빠르다는 점이다. K개 토큰의 검증은 타겟 모델의 단일 forward pass로 수행되므로, 수락률이 높을수록 속도 향상이 크다.

vLLM에서의 Speculative Decoding 설정

# vLLM에서 Speculative Decoding 활성화
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-70B-Instruct \
    --tensor-parallel-size 4 \
    --speculative-model meta-llama/Llama-3.1-8B-Instruct \
    --num-speculative-tokens 5 \
    --speculative-max-model-len 4096 \
    --use-v2-block-manager \
    --port 8000
# Speculative Decoding 효과 측정 스크립트
import time
import requests
import json
import statistics

API_URL = "http://localhost:8000/v1/completions"

def measure_latency(prompt, max_tokens=256, n_requests=10):
    """여러 요청의 지연 시간과 처리량 측정"""
    latencies = []
    total_tokens = []

    for i in range(n_requests):
        payload = {
            "model": "meta-llama/Llama-3.1-70B-Instruct",
            "prompt": prompt,
            "max_tokens": max_tokens,
            "temperature": 0.7,
        }

        start = time.time()
        response = requests.post(API_URL, json=payload)
        elapsed = time.time() - start

        result = response.json()
        completion_tokens = result["usage"]["completion_tokens"]

        latencies.append(elapsed)
        total_tokens.append(completion_tokens)

    avg_latency = statistics.mean(latencies)
    p99_latency = sorted(latencies)[int(0.99 * len(latencies))]
    avg_throughput = statistics.mean(total_tokens) / avg_latency

    print(f"평균 지연: {avg_latency:.3f}s")
    print(f"P99 지연: {p99_latency:.3f}s")
    print(f"평균 처리량: {avg_throughput:.1f} tokens/s")
    return avg_latency, p99_latency, avg_throughput

# 테스트 프롬프트
test_prompt = "다음 주제에 대해 상세히 설명해주세요: 마이크로서비스 아키텍처"

print("=== Speculative Decoding 벤치마크 ===")
measure_latency(test_prompt)

최신 Speculative Decoding 기법들

2025-2026년에는 Speculative Decoding 분야에서 다양한 발전이 이루어졌다.

  • EAGLE-3: TensorRT-LLM에 통합된 고급 추측 기법으로, 드래프트 모델 없이 타겟 모델 자체의 히든 스테이트를 활용하여 토큰을 예측한다. 별도 드래프트 모델 메모리가 불필요하다.
  • Multi-Token Prediction(MTP): 한 스텝에서 여러 토큰을 동시에 예측하는 방식으로, DeepSeek V3에서 채택한 기법이다.
  • TurboSpec: 런타임에 추측 파라미터를 동적으로 조정하는 클로즈드 루프 제어 시스템으로, 워크로드와 하드웨어에 적응한다.
  • 이종 어휘(Heterogeneous Vocabulary) 지원: 드래프트 모델과 타겟 모델이 같은 어휘를 공유하지 않아도 되는 알고리즘이 개발되어, 드래프트 모델 선택의 폭이 넓어졌다. 경험적으로 최대 2.8배의 속도 향상이 보고되었다.

추론 엔진 비교 분석 (vLLM vs TGI vs TensorRT-LLM)

종합 비교표

항목vLLM (v0.17.x)TGI (v3.x)TensorRT-LLM (v1.0)
개발사UC Berkeley / vLLM 커뮤니티Hugging FaceNVIDIA
라이선스Apache 2.0Apache 2.0Apache 2.0
핵심 기술PagedAttentionFlashAttention-2/3, FlashInferTensorRT 엔진 최적화
처리량(req/s)120-160100-140180-220
TTFT50-80ms60-90ms35-50ms
설치 난이도낮음 (pip install)낮음 (Docker)높음 (엔진 빌드 필요)
Continuous Batching지원지원지원
양자화AWQ, GPTQ, FP8AWQ, GPTQ, BitsAndBytesFP8, NVFP4, INT8, INT4
Speculative Decoding지원지원 (제한적)EAGLE-3, MTP 지원
Tensor Parallelism지원지원지원
Pipeline Parallelism지원미지원지원
Prefix Caching지원 (자동)지원지원
장문 컨텍스트보통우수 (TGI v3 기준 13배 빠름)우수
모델 호환성매우 넓음넓음NVIDIA GPU 전용
API 호환성OpenAI 호환자체 API + OpenAI 호환Triton 기반
커뮤니티 활성도매우 높음높음높음

사용 시나리오별 권장 엔진

vLLM을 선택해야 할 때:

  • 빠르게 프로토타이핑하거나 개발 환경에서 테스트할 때
  • 다양한 모델을 지원해야 할 때 (HuggingFace 모델 직접 로드)
  • 높은 동시 접속자 수에서 안정적인 지연 시간이 필요할 때
  • OpenAI 호환 API가 필요할 때

TGI를 선택해야 할 때:

  • Hugging Face 생태계와의 긴밀한 통합이 필요할 때
  • 200K 토큰 이상의 초장문 컨텍스트를 처리해야 할 때 (TGI v3의 13배 속도 향상)
  • Docker 기반의 간편한 배포를 원할 때

TensorRT-LLM을 선택해야 할 때:

  • NVIDIA GPU에서 절대적인 최고 성능이 필요할 때
  • TTFT(첫 토큰 시간)가 극도로 중요한 실시간 서비스에서
  • NVIDIA Triton Inference Server와 통합된 엔터프라이즈 환경에서
  • FP8/NVFP4 등 최신 양자화 기법을 활용하려 할 때

양자화와 추론 최적화 (AWQ, GPTQ, FP8)

양자화 기법 비교

양자화는 모델 가중치의 정밀도를 낮춰서 메모리 사용량을 줄이고 추론 속도를 높이는 기법이다. 메모리 바운드 환경(작은 배치)에서 특히 효과적이다.

기법비트 수메모리 절감품질 손실속도 향상특징
FP16 (기준)16비트---기준선
FP8 (W8A8)8비트약 50%-2.7% (장문)1.5-2xH100 네이티브 지원, 학습 불필요
AWQ (INT4)4비트약 75%-0.2% (장문)2-3x활성화 인식, 빠른 양자화
GPTQ (INT4)4비트약 75%-1.8% (장문)2-3xHessian 기반 최적화, 데이터 필요
NVFP44비트약 75%낮음2-3xTensorRT-LLM 전용, Blackwell 최적

양자화 적용 실전 코드

# AWQ 양자화 모델을 vLLM에서 사용하는 예시
from vllm import LLM, SamplingParams

# AWQ 양자화 모델 로드 (HuggingFace에서 AWQ 모델 직접 사용)
llm_awq = LLM(
    model="TheBloke/Llama-2-70B-Chat-AWQ",
    quantization="awq",
    tensor_parallel_size=2,        # 4비트이므로 GPU 2장이면 충분
    gpu_memory_utilization=0.90,
    max_model_len=4096,
)

# GPTQ 양자화 모델 로드
llm_gptq = LLM(
    model="TheBloke/Llama-2-70B-Chat-GPTQ",
    quantization="gptq",
    tensor_parallel_size=2,
    gpu_memory_utilization=0.90,
)

# FP8 양자화 (H100 이상 권장)
llm_fp8 = LLM(
    model="meta-llama/Llama-3.1-70B-Instruct",
    quantization="fp8",
    tensor_parallel_size=4,
    gpu_memory_utilization=0.90,
)

# 성능 비교 벤치마크
import time

sampling_params = SamplingParams(temperature=0.7, max_tokens=512)
test_prompts = ["마이크로서비스 아키텍처의 장단점을 설명해주세요."] * 10

for name, model in [("AWQ", llm_awq), ("GPTQ", llm_gptq), ("FP8", llm_fp8)]:
    start = time.time()
    outputs = model.generate(test_prompts, sampling_params)
    elapsed = time.time() - start
    total_tokens = sum(len(o.outputs[0].token_ids) for o in outputs)
    print(f"{name}: {elapsed:.2f}s, {total_tokens/elapsed:.1f} tokens/s")

양자화 선택 가이드

실무에서 양자화 기법을 선택할 때의 권장 사항은 다음과 같다.

  • FP8로 시작: H100 이상 GPU를 사용한다면, FP8은 학습 데이터 없이 적용 가능하며 품질 손실이 적다.
  • 메모리가 부족하다면 AWQ: INT4 양자화 중 AWQ가 품질 손실이 가장 적고(-0.2%), 양자화 속도도 빠르다.
  • 배치 크기에 따른 효과 차이 주의: 작은 배치에서는 메모리 바운드이므로 양자화 효과가 크지만, 큰 배치에서는 컴퓨트 바운드가 되어 INT4-to-FP16 역양자화 오버헤드로 인해 효과가 감소한다.

운영 시 주의사항과 트러블슈팅

GPU 메모리 관리

프로덕션에서 가장 흔한 문제는 GPU OOM(Out of Memory)이다. 다음 사항을 점검해야 한다.

  • gpu-memory-utilization 값을 0.90 이상으로 설정하면, 일시적인 메모리 스파이크에 취약하다. 0.85-0.90이 안전한 범위이다.
  • max-model-len을 필요 이상으로 크게 설정하면 KV 캐시가 과도하게 할당된다. 실제 사용 패턴에 맞게 조정해야 한다.
  • 텐서 병렬 시 GPU 간 통신 버퍼도 메모리를 차지한다. NVLink 연결 상태를 확인하자.

모니터링 핵심 지표

# GPU 모니터링 - nvidia-smi 활용
watch -n 1 nvidia-smi

# vLLM 메트릭 확인 (Prometheus 포맷)
curl http://localhost:8000/metrics | grep -E "vllm_(num_requests|gpu_cache|avg_generation)"

# 핵심 모니터링 대상:
# - vllm:num_requests_running: 현재 처리 중인 요청 수
# - vllm:num_requests_waiting: 대기 중인 요청 수
# - vllm:gpu_cache_usage_perc: GPU KV 캐시 사용률
# - vllm:avg_generation_throughput_toks_per_s: 평균 토큰 생성 처리량

CUDA Graph와 메모리 트레이드오프

vLLM은 CUDA Graph를 사용하여 커널 실행 오버헤드를 줄인다. 하지만 CUDA Graph는 추가 GPU 메모리를 소비한다. 메모리가 부족하면 --enforce-eager 옵션으로 비활성화할 수 있지만, 처리량이 감소한다.

요청 타임아웃과 큐 관리

장시간 걸리는 요청이 전체 시스템을 블로킹하는 상황을 방지해야 한다. --max-num-seqs 옵션으로 동시 처리 요청 수를 제한하고, 프록시 레벨에서 타임아웃을 설정하는 것이 좋다.

실패 사례와 복구 절차

사례 1: KV Cache 메모리 부족으로 인한 요청 거부

증상: gpu_cache_usage_perc가 100%에 가까워지면서 새 요청이 큐에 무한 대기하거나 거부된다.

원인: 장문 입력이 몰리거나 동시 요청 수가 급증하여 KV 캐시 공간이 부족한 경우이다.

복구 절차:

  1. max-num-seqs 값을 줄여 동시 요청 수를 제한한다.
  2. max-model-len을 실제 사용 패턴에 맞게 축소한다.
  3. 필요 시 양자화를 적용하여 모델 가중치의 메모리 점유를 줄인다.
  4. Prefix Caching을 활성화하여 공통 시스템 프롬프트의 KV 캐시를 공유한다.

사례 2: TensorRT-LLM 엔진 빌드 실패

증상: trtllm-build 과정에서 OOM 에러 또는 호환성 에러가 발생한다.

원인: 빌드 시에도 상당한 GPU 메모리가 필요하며, 빌드 파라미터가 하드웨어 사양과 맞지 않는 경우이다.

복구 절차:

  1. --max_batch_size--max_input_len 값을 줄여서 재빌드를 시도한다.
  2. --workers 옵션으로 빌드 병렬도를 줄인다.
  3. GPU 드라이버, CUDA, TensorRT 버전의 호환성을 확인한다.
  4. Docker 이미지를 사용하여 환경 의존성 문제를 제거한다.

사례 3: Speculative Decoding 수락률 저하

증상: Speculative Decoding을 적용했는데 오히려 지연이 증가한다.

원인: 드래프트 모델의 예측 정확도가 낮아 대부분의 토큰이 거절되면, 드래프트 모델의 추가 연산이 순수 오버헤드가 된다.

복구 절차:

  1. num-speculative-tokens 값을 줄인다 (5 이하에서 3 이하로).
  2. 드래프트 모델을 타겟 모델과 같은 패밀리의 소형 모델로 교체한다 (예: 70B 타겟이면 8B 드래프트).
  3. 수락률 메트릭을 모니터링하여 60% 이하이면 드래프트 모델을 변경하거나 Speculative Decoding을 비활성화한다.
  4. EAGLE 방식으로 전환하여 드래프트 모델 메모리 부담을 제거하는 것을 검토한다.

사례 4: 로드 밸런싱 불균형

증상: 여러 vLLM 인스턴스 중 특정 인스턴스에만 부하가 집중된다.

원인: 단순 라운드 로빈 로드 밸런싱이 요청 길이의 차이를 고려하지 못하기 때문이다.

복구 절차:

  1. Least-Connection 또는 Weighted Round Robin 방식의 로드 밸런서를 사용한다.
  2. 각 인스턴스의 num_requests_running 메트릭을 기반으로 요청을 분배한다.
  3. 장문 요청 전용 인스턴스와 단문 요청 전용 인스턴스를 분리하는 것을 고려한다.

마치며

LLM 추론 최적화는 단일 기술이 아니라, 여러 계층의 최적화를 조합하는 종합 엔지니어링이다. KV Cache 관리(PagedAttention), 연산 최적화(FlashAttention), 디코딩 가속(Speculative Decoding), 정밀도 최적화(양자화), 시스템 최적화(Continuous Batching, Tensor Parallelism)가 유기적으로 결합되어야 최상의 성능을 달성할 수 있다.

실전에서의 핵심 교훈을 정리하면 다음과 같다.

  1. 처음에는 vLLM으로 시작하라: 설치가 쉽고, 커뮤니티가 크며, 대부분의 시나리오에서 충분한 성능을 제공한다.
  2. TTFT가 최우선이면 TensorRT-LLM을 고려하라: 설정은 복잡하지만, NVIDIA GPU에서 최고의 지연 성능을 달성한다.
  3. 양자화는 FP8부터 시작하라: 품질 손실이 적고 설정이 간단하다. 메모리가 부족할 때만 INT4(AWQ)로 내려가라.
  4. Speculative Decoding은 수락률을 모니터링하라: 60% 이하면 효과가 없다. 드래프트 모델 선택이 핵심이다.
  5. 모니터링 없는 최적화는 맹목적이다: GPU 캐시 사용률, TTFT, 처리량, P99 지연을 반드시 추적하라.

LLM 추론 최적화 기술은 빠르게 진화하고 있다. vLLM은 v2 아키텍처를 준비 중이며, TensorRT-LLM은 차세대 Blackwell GPU 최적화를 강화하고 있고, Speculative Decoding은 이종 어휘 지원과 적응형 제어 시스템으로 발전하고 있다. 이러한 흐름을 지속적으로 추적하면서, 자신의 워크로드에 맞는 최적의 조합을 찾아가는 것이 중요하다.

참고자료