자체 GPU 위에서 오픈 웨이트 모델을 직접 서빙하기로 결정했다면, 다음 갈림길은 “어떤 서빙 엔진을 쓸 것인가”입니다. 후보는 대체로 TGI(Text Generation Inference), vLLM, TensorRT-LLM 셋으로 좁혀집니다. 셋 다 “LLM을 HTTP로 서빙”한다는 점은 같지만, 처리량을 짜내는 방식과 운영에 드는 손품이 근본적으로 다릅니다.

핵심을 먼저 말하면, 세 엔진은 편의성과 최고 성능 사이의 트레이드오프 축 위에 놓여 있습니다. vLLM은 처리량과 사용성의 균형이 좋은 사실상의 표준, TGI는 실전 운영 기능이 잘 갖춰진 서버, TensorRT-LLM은 엔진을 컴파일해 최저 지연·최고 처리량을 뽑되 손이 가장 많이 가는 도구입니다. 이 글에서는 세 엔진의 내부 모델을 비교하고, 어떤 워크로드에서 무엇을 골라야 하는지 실무 기준으로 정리합니다.

먼저 알아야 할 공통 병목: KV 캐시와 배칭

자기회귀 생성은 토큰을 하나씩 만들며 이전 토큰들의 KV 캐시를 GPU 메모리에 쌓습니다. 실제 서빙에서 GPU를 놀게 만드는 주범은 연산량이 아니라 메모리 대역폭과 KV 캐시 단편화입니다. 요청마다 최대 길이만큼 메모리를 미리 잡으면, 짧게 끝나는 요청 때문에 GPU 메모리가 텅 빈 채로 낭비됩니다.

그래서 현대 서빙 엔진의 성능은 두 기법으로 갈립니다. 연속 배칭(continuous batching)은 요청이 끝나는 즉시 배치 슬롯을 새 요청으로 채워 GPU를 쉬지 않게 하고, PagedAttention 계열은 KV 캐시를 페이지 단위로 관리해 단편화를 없앱니다. 세 엔진 모두 이 두 아이디어를 구현하고 있으며, 얼마나 잘 구현했는가가 곧 처리량 차이입니다.

  • 프리필(prefill): 입력 프롬프트를 한 번에 처리하는 단계. 연산 집약적(compute-bound)이며 TTFT를 좌우.
  • 디코드(decode): 토큰을 하나씩 생성하는 단계. 메모리 대역폭 집약적(memory-bound)이라 배칭 효과가 크고 TPOT를 좌우.

vLLM: 처리량과 사용성의 표준점

vLLM은 PagedAttention을 처음 대중화한 엔진으로, 지금은 오픈 웨이트 서빙의 사실상 기본 선택지입니다. KV 캐시를 OS 가상 메모리처럼 고정 크기 블록으로 나눠 관리해 단편화를 거의 없애고, 그만큼 더 큰 배치를 GPU에 밀어 넣습니다. 별도 컴파일 없이 모델 이름만 넘기면 바로 뜬다는 것이 최대 강점입니다.

# vLLM: OpenAI 호환 서버를 한 줄로 기동
# --gpu-memory-utilization: KV 캐시에 쓸 GPU 메모리 비율(기본 0.9)
#   너무 높이면 긴 시퀀스에서 OOM, 너무 낮추면 배치 크기가 줄어듦
python -m vllm.entrypoints.openai.api_server 
  --model mistralai/Mistral-7B-Instruct-v0.3 
  --gpu-memory-utilization 0.90 
  --max-model-len 8192 
  --tensor-parallel-size 1 
  --port 8000

클라이언트는 표준 OpenAI SDK를 그대로 씁니다. base_url만 바꾸면 상용 API를 쓰던 코드가 온프레미스 서버에 그대로 붙어, 옮겼다는 사실이 애플리케이션 코드에 거의 드러나지 않습니다.

from openai import OpenAI

# 로컬 vLLM 서버를 상용 API처럼 호출 (base_url만 교체)
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
resp = client.chat.completions.create(
    model="mistralai/Mistral-7B-Instruct-v0.3",
    messages=[{"role": "user", "content": "온프레미스 서빙의 장점은?"}],
    max_tokens=256,
)
print(resp.choices[0].message.content)

vLLM은 텐서 병렬(--tensor-parallel-size)로 여러 GPU에 모델을 쪼개 올리는 것도 플래그 하나로 처리합니다. 대부분의 팀에게 “일단 여기서 시작하라”가 정답에 가깝고, 성능이 충분하면 굳이 더 복잡한 도구로 갈 이유가 없습니다.

TGI: 운영 기능이 다듬어진 프로덕션 서버

TGI는 처리량 자체보다 프로덕션 운영에 필요한 주변 기능이 잘 갖춰진 것이 특징입니다. 연속 배칭과 페이지드 어텐션은 기본이고, 표준 Prometheus 메트릭·헬스체크·토큰 스트리밍에 컨테이너 이미지가 정돈돼 있어 쿠버네티스에 얹기 좋습니다.

배포는 공식 컨테이너 이미지로 하는 것이 표준입니다. 모델과 샤딩(--num-shard)만 지정하면 됩니다.

# TGI: 공식 이미지로 서빙, GPU 2장에 샤딩
docker run --gpus all --shm-size 1g -p 8080:80 
  -v $PWD/data:/data 
  ghcr.io/huggingface/text-generation-inference:latest 
  --model-id mistralai/Mistral-7B-Instruct-v0.3 
  --num-shard 2 
  --max-input-tokens 4096 
  --max-total-tokens 8192 
  --max-batch-prefill-tokens 8192   # 프리필 배치 상한(OOM 방어의 핵심 노브)

운영 관점에서 TGI의 진가는 관측성에서 드러납니다. /metrics 엔드포인트가 표준 형식으로 큐 대기 시간(tgi_request_queue_duration), 현재 배치 크기(tgi_batch_current_size), 순수 추론 시간(tgi_request_inference_duration) 등을 노출해, 별도 계측 없이 대시보드·오토스케일링에 바로 연결됩니다.

정리하면 TGI는 “성능은 vLLM에 크게 밀리지 않으면서, 처음부터 운영을 염두에 둔 서버”입니다. 관측성과 배포 표준화를 중시하고 이미 컨테이너 인프라를 운영 중인 팀에게 마찰이 가장 적습니다.

TensorRT-LLM: 컴파일해서 뽑아내는 최고 성능

TensorRT-LLM은 앞의 둘과 성격이 다릅니다. 모델을 특정 GPU 아키텍처에 맞춰 엔진(engine)으로 사전 컴파일한 뒤 실행합니다. 커널 퓨전, 특정 정밀도(FP8 등) 최적화, GPU별 튜닝을 컴파일 타임에 몰아넣어, 같은 하드웨어에서 지연시간·처리량을 가장 낮게/높게 끌어냅니다. 대가는 빌드 단계의 복잡성입니다. vLLM/TGI가 “모델을 가리키면 뜬다”라면, TensorRT-LLM은 “가중치를 변환하고 엔진을 빌드한 뒤 그 엔진을 서빙”하는 2단계 워크플로입니다.

# TensorRT-LLM: 2단계 워크플로
# (1) HF 체크포인트를 TensorRT-LLM 포맷으로 변환 (FP8 양자화 예시)
python convert_checkpoint.py 
  --model_dir ./Mistral-7B-Instruct-v0.3 
  --output_dir ./tllm_ckpt 
  --dtype float16 
  --use_fp8               # 지원 GPU에서 메모리·대역폭 절감

# (2) 변환된 체크포인트로 실행 엔진 빌드
#     이 엔진은 빌드한 GPU 아키텍처에 종속됨 (다른 세대 GPU에서 재사용 불가)
trtllm-build 
  --checkpoint_dir ./tllm_ckpt 
  --output_dir ./engine 
  --gemm_plugin float16 
  --max_batch_size 64 
  --max_input_len 4096 
  --max_seq_len 8192

여기서 가장 중요한 함정은 엔진이 GPU 아키텍처에 종속된다는 점입니다. 특정 세대 GPU에서 빌드한 엔진은 다른 세대에서 그대로 쓸 수 없어, CI에서 엔진을 빌드해 산출물로 저장하고 서빙 노드는 그 엔진만 로드하는 파이프라인이 필요합니다. 또한 max_batch_size·max_seq_len이 빌드 타임에 고정돼 런타임에 바꿀 수 없다는 제약도 함께 옵니다.

빌드된 엔진은 보통 Triton Inference Server에 얹어 서빙하며, 빌드 파이프라인을 감당할 인력과 하드웨어 표준화가 갖춰진 대규모 서빙에서 진가가 나옵니다.

양자화: 어느 층위에서 하느냐가 다르다

온프레미스 서빙에서 양자화는 메모리와 처리량을 좌우하는 핵심 레버입니다. 가중치를 8비트나 4비트로 줄이면 더 큰 모델을 같은 GPU에 올리거나 동시 요청 수를 늘릴 수 있습니다. 세 엔진은 양자화를 다루는 층위가 다릅니다.

  • vLLM: 이미 양자화된 체크포인트(예: AWQ·GPTQ)를 런타임에 로드하는 방식이 일반적입니다.
  • TGI: 마찬가지로 사전 양자화된 가중치를 --quantize 플래그로 지정해 로드합니다.
  • TensorRT-LLM: 양자화를 컴파일 파이프라인에 통합합니다. FP8 같은 방식은 엔진 빌드 시점에 적용돼, 커널 최적화와 함께 맞물려 돌아갑니다. 예컨대 vLLM에서 AWQ 체크포인트를 서빙하려면 --quantization awq --model <awq-repo>처럼 런타임 플래그로 끝나지만, TensorRT-LLM에서는 앞서 본 --use_fp8처럼 변환·빌드 단계에 녹아듭니다.

주의할 점은 양자화가 공짜가 아니라는 것입니다. 비트 폭을 줄이면 출력 품질이 미묘하게 저하될 수 있으므로, 반드시 자체 평가셋으로 양자화 전후 품질을 비교 측정한 뒤 배포해야 합니다. “메모리가 줄었으니 좋다”로 끝내는 순간 품질 회귀를 뒤늦게 발견합니다.

벤치마킹: 단일 지표에 속지 않기

엔진을 고르기 전에 반드시 자신의 워크로드로 직접 벤치마킹해야 합니다. 남의 벤치마크는 입력·출력 길이 분포, 동시성, 하드웨어가 달라 그대로 이식되지 않기 때문입니다. 서빙 성능은 최소 세 축으로 봅니다.

  • 처리량(초당 토큰·요청): 배치 효율 반영. 오프라인 워크로드의 핵심.
  • TTFT: 스트리밍에서 체감하는 첫 반응 속도. 챗봇 UX 핵심.
  • 부하 곡선: 동시 요청을 늘리며 지연이 언제 급증하는지. 포화점을 알아야 오토스케일 임계치를 정합니다.
import asyncio, time
from openai import AsyncOpenAI

client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

async def one_request(prompt: str):
    t0 = time.perf_counter()
    first_at, n_out = None, 0
    stream = await client.chat.completions.create(
        model="mistralai/Mistral-7B-Instruct-v0.3",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=256, stream=True,
    )
    async for chunk in stream:
        if chunk.choices[0].delta.content:
            if first_at is None:
                first_at = time.perf_counter()  # TTFT 측정 지점
            n_out += 1
    total = time.perf_counter() - t0
    return {"ttft": first_at - t0, "tpot": (total - (first_at - t0)) / max(n_out, 1)}

async def bench(concurrency: int, prompts: list[str]):
    # 동시성을 고정하고 TTFT/TPOT 분포를 수집 → p95/p99 로 요약
    sem = asyncio.Semaphore(concurrency)
    async def guarded(p):
        async with sem:
            return await one_request(p)
    return await asyncio.gather(*(guarded(p) for p in prompts))

여기서 반드시 평균이 아니라 p95·p99를 봐야 합니다. 연속 배칭 엔진은 부하가 오르면 평균은 완만해도 꼬리 지연이 급격히 나빠지곤 합니다. 사용자 경험을 결정하는 것은 이 꼬리입니다.

결정 기준: 워크로드로 되묻기

성능 수치를 비교하기 전에, 자신의 워크로드에 먼저 질문을 던지는 것이 가장 빠른 판단법입니다.

  • 지금 바로 뜨고 유연하게 실험하고 싶다 → vLLM. 컴파일 단계가 없고 모델 교체가 자유로워 프로토타이핑부터 중간 규모 프로덕션까지 마찰이 가장 적습니다.
  • 관측성·배포 표준화가 중요하고 이미 K8s를 쓴다 → TGI. 메트릭·헬스체크·컨테이너가 정돈돼 있어 운영 인프라에 얹기 좋습니다.
  • 하드웨어가 고정돼 있고 최후의 1%까지 성능을 짜야 한다 → TensorRT-LLM. 빌드 파이프라인의 복잡성을 감수할 규모와 인력이 있을 때만 투자가 회수됩니다.

실무에서는 이 선택이 단계적으로 진화합니다. vLLM으로 서비스를 띄워 워크로드와 트래픽 패턴을 파악하고, 성능이 병목이 되고 하드웨어가 표준화된 뒤에야 TensorRT-LLM으로 옮기는 식입니다. 처음부터 가장 복잡한 도구를 고르는 것은 대개 조기 최적화입니다.

마무리

온프레미스 LLM 서빙 엔진의 선택은 결국 편의성과 최고 성능 사이의 트레이드오프를 자신의 상황에 맞춰 푸는 일입니다. 세 엔진은 연속 배칭과 페이지드 KV 캐시라는 같은 뿌리를 공유하지만, vLLM은 즉시성과 유연성, TGI는 운영 성숙도, TensorRT-LLM은 컴파일된 최고 성능이라는 서로 다른 지점에 서 있습니다.

가장 중요한 원칙은 두 가지입니다. 반드시 자신의 워크로드로 p95·p99까지 직접 벤치마킹할 것, 그리고 양자화든 컴파일이든 최적화를 적용했다면 품질 회귀를 평가셋으로 검증할 것. 남의 숫자와 기본값을 그대로 믿는 순간, 최적화는 프로덕션에서 예상치 못한 지연과 품질 문제로 되돌아옵니다.