모델 컨테이너를 띄우는 것과 LLM을 서빙하는 것은 다르다
쿠버네티스에 익숙한 엔지니어라면 처음에는 LLM 추론도 단순한 웹 서버처럼 보일 수 있다. 컨테이너 이미지를 만들고, Deployment를 만들고, Service를 붙이고, 외부에서 호출하면 끝이라고 생각하기 쉽다. 작은 모델 하나를 테스트하는 수준에서는 이 방식도 동작한다. transformers로 모델을 로드한 파이썬 서버를 띄우고 /generate 엔드포인트를 열면 응답은 나온다.
하지만 프로덕션에 들어가는 순간 이 생각은 금방 깨진다. LLM 서버는 일반적인 REST API 서버와 병목 지점이 다르다. 웹 API 서버는 보통 CPU, DB 커넥션, 네트워크 I/O가 병목이 되지만 LLM 서버는 GPU 메모리, KV 캐시, 배치 스케줄러, 모델 가중치 로딩 시간이 병목이 된다. 같은 GPU를 쓰더라도 어떤 추론 런타임을 선택하느냐에 따라 처리량이 몇 배씩 달라질 수 있고, 같은 런타임을 쓰더라도 라우팅 방식이 잘못되면 비싼 GPU가 절반 이상 놀게 된다.
이 장에서 다룰 표준 스택은 세 층으로 나눠 볼 수 있다.
vLLM과 SGLang은 실제로 모델을 GPU에서 실행하는 추론 런타임이다.
KServe는 쿠버네티스 위에서 모델 배포, 라우팅, 트래픽 전환, 오토스케일링을 추상화하는 서빙 플랫폼이다.
OpenAI 호환 API와 게이트웨이 계층은 애플리케이션이 자체 모델, Azure OpenAI, Amazon Bedrock, 외부 LLM 서비스를 같은 방식으로 호출하게 해준다.
이 셋을 제대로 이해하면 "모델 컨테이너 하나 띄우기"를 넘어 "프로덕션 LLM 추론 플랫폼"을 설계할 수 있다. 이 차이는 작지 않다. 전자는 데모이고, 후자는 비용과 장애와 트래픽을 견디는 서비스다.

LLM 추론의 두 단계: prefill과 decode
vLLM과 SGLang을 이해하려면 먼저 LLM 추론이 어떻게 진행되는지 알아야 한다. LLM 추론은 크게 prefill과 decode 두 단계로 나뉜다.
사용자가 다음과 같은 요청을 보냈다고 해보자.
시스템: 너는 친절한 쿠버네티스 전문가야.
사용자: vLLM을 쿠버네티스에 배포할 때 주의할 점을 알려줘.모델은 먼저 입력 프롬프트 전체를 읽는다. 시스템 메시지, 사용자 메시지, 이전 대화 이력, RAG로 붙은 문서 조각까지 모두 처리한다. 이 과정이 prefill이다. prefill은 입력 토큰을 한꺼번에 처리하므로 계산량이 크고, 긴 프롬프트가 들어오면 첫 토큰이 나오기까지 시간이 길어진다. 그래서 사용자가 체감하는 첫 응답 시간, 즉 TTFT(Time To First Token)에 큰 영향을 준다.
그다음 모델은 답변을 한 토큰씩 생성한다. 이전 토큰을 바탕으로 다음 토큰을 예측하고, 그 토큰을 다시 문맥에 붙여 다음 토큰을 만든다. 이 과정이 decode다. decode는 한 번에 한 토큰씩 진행되므로 GPU 연산 장치를 항상 꽉 채우기 어렵고, 이전 토큰들의 Key/Value 정보를 저장해 둔 KV 캐시에 계속 접근한다. 이때는 순수 연산량보다 메모리 대역폭과 캐시 관리가 중요해진다.
기존 웹 서버의 관점으로 보면 요청 하나가 들어오고 응답 하나가 나가는 단순한 구조처럼 보이지만, LLM 서버 내부에서는 서로 성격이 다른 두 작업이 섞여 있다. 긴 입력을 빠르게 읽는 작업과, 토큰을 안정적으로 이어서 쓰는 작업이다. 다음 장에서 다룰 disaggregated 추론은 이 두 단계를 아예 다른 GPU 풀로 분리하는 아키텍처다. 이 장에서는 그 전 단계로, 단일 런타임과 KServe를 사용해 표준적인 LLM 추론 서비스를 구성하는 방법을 먼저 살펴본다.
vLLM: 프로덕션 LLM 서빙의 기본값
2026년 기준으로 쿠버네티스에서 자체 LLM을 서빙할 때 가장 먼저 검토할 런타임은 vLLM이다. vLLM은 OpenAI 호환 API 서버를 제공하고, Hugging Face 모델을 쉽게 로드하며, 단일 GPU부터 멀티 GPU 텐서 병렬까지 폭넓게 지원한다. vLLM 공식 문서도 쿠버네티스 배포 예제를 제공하며, GPU 배포 시 PVC, Hugging Face 토큰 Secret, /dev/shm용 메모리 볼륨, readiness/liveness probe를 함께 구성하는 방식을 안내한다.1
vLLM의 핵심은 GPU 메모리를 얼마나 낭비 없이 쓰느냐에 있다. LLM 추론에서 가장 부담스러운 메모리 사용처 중 하나는 KV 캐시다. 사용자가 긴 프롬프트를 보내고 모델이 긴 답변을 생성할수록 KV 캐시는 계속 커진다. 예전 방식처럼 요청마다 최대 길이만큼 연속된 GPU 메모리를 미리 잡아두면, 실제로 쓰지 않는 공간이 많이 남는다. 식당에서 손님 한 명이 올 때마다 10인용 테이블을 통째로 배정하는 것과 비슷하다.
vLLM의 PagedAttention은 이 문제를 운영체제의 페이징처럼 푼다. KV 캐시를 작은 블록으로 나누고, 필요한 만큼만 할당한다. 요청 A의 캐시가 GPU 메모리의 여기저기에 흩어져 있어도 논리적으로는 하나의 연속된 캐시처럼 다룬다. 덕분에 더 많은 요청을 같은 GPU 메모리 안에 담을 수 있고, 배치 크기를 키울 수 있다.
두 번째 핵심은 continuous batching이다. 전통적인 배치 처리는 여러 요청을 묶은 뒤, 그중 가장 긴 요청이 끝날 때까지 배치 전체가 같이 움직인다. 짧은 답변을 생성한 요청은 이미 끝났는데도 긴 답변을 생성하는 요청 때문에 GPU 슬롯이 비어 있는 것처럼 낭비된다. continuous batching은 끝난 요청을 즉시 배치에서 빼고, 대기 중인 새 요청을 바로 넣는다. 대기열에 사람이 계속 들어오고 나가도 계산대가 쉬지 않는 구조다.
세 번째는 OpenAI 호환 API다. 이것은 운영에서 생각보다 중요하다. 애플리케이션 코드가 이미 OpenAI의 /v1/chat/completions 형식에 맞춰져 있다면, vLLM을 붙일 때 클라이언트의 base_url만 바꾸면 된다. 자체 모델을 쓰다가 Azure OpenAI로 폴백하거나, 반대로 외부 API 비용이 부담될 때 자체 모델로 일부 트래픽을 돌리는 구조도 만들기 쉽다.
간단한 vLLM 배포는 다음과 같은 모양이 된다. 실제 운영에서는 이미지 태그를 팀에서 검증한 버전이나 digest로 고정하자. 아래의 버전은 예시일 뿐이며, 사용하는 CUDA·드라이버·vLLM 조합에 맞게 조정해야 한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: llama-8b-vllm
namespace: llm-serving
spec:
replicas: 1
selector:
matchLabels:
app: llama-8b-vllm
template:
metadata:
labels:
app: llama-8b-vllm
spec:
containers:
- name: vllm
image: vllm/vllm-openai:v0.8.0
command: ["/bin/sh", "-c"]
args:
- >
vllm serve meta-llama/Llama-3.1-8B-Instruct
--host 0.0.0.0
--port 8000
--tensor-parallel-size 1
--max-model-len 8192
--gpu-memory-utilization 0.90
--enable-prefix-caching
--enable-chunked-prefill
env:
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: token
ports:
- containerPort: 8000
name: http
resources:
requests:
cpu: "4"
memory: 24Gi
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: 32Gi
nvidia.com/gpu: "1"
volumeMounts:
- name: model-cache
mountPath: /root/.cache/huggingface
- name: shm
mountPath: /dev/shm
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 30
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: llama-8b-cache
- name: shm
emptyDir:
medium: Memory
sizeLimit: 4Gi
terminationGracePeriodSeconds: 300
---
apiVersion: v1
kind: Service
metadata:
name: llama-8b-vllm
namespace: llm-serving
spec:
selector:
app: llama-8b-vllm
ports:
- name: http
port: 80
targetPort: 8000여기서 몇 가지는 그냥 장식이 아니다. startupProbe는 모델 로딩이 오래 걸리는 LLM 서버에서 중요하다. 7B 모델은 수십 초, 70B 모델은 몇 분 이상 걸릴 수 있다. 쿠버네티스가 이 시간을 장애로 오해해 컨테이너를 계속 재시작하면 모델은 영원히 준비되지 않는다. readinessProbe는 모델이 실제로 요청을 받을 수 있을 때만 Service 뒤에 붙게 해준다. /dev/shm 메모리 볼륨은 멀티 GPU와 텐서 병렬 환경에서 공유 메모리 부족 문제를 피하는 데 필요하다. terminationGracePeriodSeconds를 길게 잡는 이유는 생성 중인 응답을 갑자기 끊지 않기 위해서다.
이 배포는 작은 모델을 빠르게 올리기에는 충분하다. 하지만 여기서 멈추면 아직 "표준 스택"이라고 부르기 어렵다. 모델이 늘어나고, 팀이 늘어나고, 트래픽 전환과 오토스케일링이 필요해지면 raw Deployment만으로는 운영 부담이 커진다.
SGLang: 에이전트와 멀티턴 대화에 강한 런타임
vLLM이 범용 프로덕션 기본값이라면, SGLang은 멀티턴 대화, 구조화 출력, 에이전트 워크로드에서 특히 주목할 만한 런타임이다. SGLang 공식 문서는 RadixAttention, prefix caching, continuous batching, multi-GPU parallelism, OpenAI 호환 API를 핵심 기능으로 설명한다.2
SGLang을 이해하는 가장 좋은 키워드는 재사용이다. 많은 AI 서비스는 완전히 새로운 프롬프트만 처리하지 않는다. 오히려 같은 시스템 프롬프트, 같은 도구 설명, 같은 출력 형식 지시문을 계속 반복한다.
예를 들어 고객지원 챗봇은 매 요청마다 이런 프롬프트를 붙일 수 있다.
너는 우리 회사의 고객지원 상담원이다.
환불 정책은 다음과 같다...
배송 정책은 다음과 같다...
반드시 JSON 형식으로 답하라...사용자 질문은 매번 달라도 앞부분은 거의 같다. vLLM도 prefix caching을 지원하지만, SGLang의 RadixAttention은 이런 공통 접두사를 트리 구조로 관리하는 데 초점을 맞춘다. "이미 계산한 앞부분을 다음 요청에서 다시 쓰자"는 생각이다.
SGLang은 구조화 출력에도 강하다. 에이전트가 도구를 호출하려면 모델이 자유로운 자연어만 생성해서는 곤란하다. 다음처럼 일정한 JSON 형식으로 답해야 한다.
{
"tool": "search_docs",
"arguments": {
"query": "KServe LLMInferenceService autoscaling"
}
}이런 출력은 단순히 "예쁘게 답변하기" 문제가 아니다. 잘못된 쉼표 하나, 빠진 괄호 하나가 곧 파서 오류와 작업 실패로 이어진다. SGLang은 구조화 생성과 도구 호출 흐름을 런타임 차원에서 중요하게 다룬다. 에이전트 워크로드를 다룰 때 이 특성이 다시 등장한다.
쿠버네티스 배포 구조는 vLLM과 크게 다르지 않다. OpenAI 호환 API를 사용한다면 클라이언트 입장에서는 base_url만 바꾸는 수준으로 교체할 수도 있다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: qwen-sglang
namespace: llm-serving
spec:
replicas: 1
selector:
matchLabels:
app: qwen-sglang
template:
metadata:
labels:
app: qwen-sglang
spec:
containers:
- name: sglang
image: lmsysorg/sglang:v0.4.6
command: ["/bin/sh", "-c"]
args:
- >
python -m sglang.launch_server
--model-path Qwen/Qwen2.5-7B-Instruct
--host 0.0.0.0
--port 8000
--tp 1
--mem-fraction-static 0.85
ports:
- containerPort: 8000
resources:
requests:
cpu: "4"
memory: 24Gi
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: 32Gi
nvidia.com/gpu: "1"그렇다면 vLLM과 SGLang 중 무엇을 선택해야 할까. 정답은 "벤치마크해 보라"이지만, 시작점은 정리할 수 있다. 단일턴 요청이 많고, 여러 모델과 도구 생태계의 안정성을 우선한다면 vLLM이 안전하다. 같은 시스템 프롬프트가 반복되고, 멀티턴 대화와 JSON 출력, 도구 호출이 핵심이라면 SGLang을 진지하게 테스트해야 한다. 중요한 것은 둘 다 OpenAI 호환 API를 제공하므로, 애플리케이션을 특정 런타임에 너무 강하게 묶지 않는 설계를 할 수 있다는 점이다.
OpenAI 호환 API를 내부 표준으로 삼기
LLM 추론 플랫폼을 운영할 때 API 표준은 생각보다 큰 결정이다. 자체 모델 하나만 운영할 때는 런타임 고유 API를 직접 호출해도 된다. 하지만 시간이 지나면 거의 반드시 다음 요구가 생긴다.
어떤 요청은 내부 vLLM 모델로 처리하고 싶다.
어떤 요청은 Azure OpenAI나 Bedrock 같은 외부 모델로 보내고 싶다.
장애가 나면 더 작은 모델이나 외부 API로 폴백하고 싶다.
팀별로 모델은 달라도 애플리케이션 코드는 같은 SDK를 쓰게 하고 싶다.
이때 OpenAI 호환 API를 내부 표준으로 삼으면 복잡도가 크게 줄어든다. 애플리케이션은 언제나 /v1/chat/completions, /v1/completions, /v1/embeddings 같은 익숙한 형태로 요청한다. 뒤에서는 API 게이트웨이나 라우터가 모델 이름, 테넌트, 비용 정책, 장애 상태에 따라 목적지를 고른다.
from openai import OpenAI
client = OpenAI(
base_url="https://llm.example.com/v1",
api_key="internal-token"
)
response = client.chat.completions.create(
model="company-llama-8b",
messages=[
{"role": "system", "content": "너는 사내 문서 검색 도우미야."},
{"role": "user", "content": "GPU 노드 장애 대응 절차를 알려줘."},
],
temperature=0.2,
max_tokens=512,
)여기서 company-llama-8b는 실제로는 쿠버네티스 안의 vLLM Service일 수도 있고, SGLang Service일 수도 있고, 외부 Azure OpenAI 배포일 수도 있다. 애플리케이션은 모른다. 알아야 할 필요도 없다.
운영 관점에서는 이 구조가 훨씬 유리하다. 평상시에는 자체 8B 모델로 고객 문의 초안을 생성하고, 고가치 고객이나 복잡한 요청만 외부 대형 모델로 보낼 수 있다. 비용 한도를 넘으면 자동으로 작은 모델로 라우팅할 수도 있다. 특정 모델 배포가 장애 상태면 게이트웨이에서 트래픽을 다른 백엔드로 돌릴 수 있다.
쿠버네티스에서는 이 계층을 Gateway API, Envoy, Envoy AI Gateway, 또는 자체 API 프록시로 구성할 수 있다. 중요한 것은 LLM 런타임을 애플리케이션에 직접 노출하지 않는 것이다. 모델 서버는 자주 바뀐다. 오늘은 vLLM, 내일은 SGLang, 다음 달에는 KServe LLMInferenceService일 수 있다. 반면 애플리케이션 API 계약은 오래 유지되어야 한다.
KServe: Deployment가 아니라 모델 서비스를 선언하기
raw Deployment와 Service로 vLLM을 배포하는 방식은 이해하기 쉽다. 그러나 모델이 1개에서 10개, 100개로 늘어나면 같은 YAML을 계속 복사해 붙이는 방식은 한계에 부딪힌다. 모델마다 컨테이너 이미지, GPU 수, PVC, Secret, 라우팅, 오토스케일링, 카나리 배포 규칙을 따로 관리해야 한다. 플랫폼 팀은 어느 순간 "우리는 모델을 운영하는 것인가, YAML 템플릿 공장을 운영하는 것인가"라는 생각을 하게 된다.
KServe는 이 문제를 풀기 위해 등장한 쿠버네티스 네이티브 모델 서빙 플랫폼이다. 기존의 InferenceService는 전통적인 머신러닝 모델과 딥러닝 모델 서빙에 널리 쓰였다. 예측 모델, 분류 모델, 이미지 모델, 작은 LLM까지 하나의 추상화로 다룰 수 있다. 그런데 LLM 추론은 요구사항이 너무 달라졌다. 멀티 GPU, KV 캐시 인식 라우팅, prefill/decode 분리, 대형 모델 스토리지, OpenAI 호환 API, 긴 콜드스타트가 모두 한꺼번에 등장했다.
그래서 KServe는 생성형 AI 워크로드를 위해 LLMInferenceService라는 별도의 CRD를 도입했다. KServe 문서에 따르면 InferenceService는 전통적인 predictive AI 워크로드에, LLMInferenceService는 대형 언어 모델과 생성형 AI 워크로드에 맞춘 이중 전략으로 설명된다.3
작은 모델을 LLMInferenceService로 선언하면 다음과 같은 형태가 된다. 이 예시는 개념을 보여주기 위한 형태이며, 실제 API 버전과 필드는 사용하는 KServe 버전에 맞춰 확인해야 한다.
apiVersion: serving.kserve.io/v1alpha1
kind: LLMInferenceService
metadata:
name: llama-8b
namespace: llm-serving
spec:
model:
uri: hf://meta-llama/Llama-3.1-8B-Instruct
name: meta-llama/Llama-3.1-8B-Instruct
replicas: 2
template:
containers:
- name: main
image: vllm/vllm-openai:v0.8.0
resources:
limits:
nvidia.com/gpu: "1"
cpu: "8"
memory: 32Gi
nodeSelector:
accelerator: h100이 YAML은 단순한 배포 파일처럼 보이지만 의미가 다르다. Deployment는 "이 컨테이너를 몇 개 띄워라"에 가깝다. LLMInferenceService는 "이 모델을 이 리소스로, 이 라우팅 방식으로, 이 복제 수만큼 서비스하라"에 가깝다. 즉 컨테이너가 아니라 모델 서비스가 중심이다.
KServe의 장점은 이 선언 아래에서 여러 운영 요소를 함께 묶을 수 있다는 데 있다. 모델 소스는 Hugging Face, S3, PVC 같은 저장소가 될 수 있고, 라우터는 Gateway API와 연결될 수 있으며, 스케줄러는 단순 라운드로빈보다 LLM에 맞는 방식으로 발전할 수 있다. KServe의 vLLM 통합 문서도 KServe의 Hugging Face ServingRuntime 또는 llm-d 기반 LLMInferenceService를 통해 vLLM을 사용할 수 있다고 설명한다.4
기존 InferenceService도 여전히 중요하다. 작은 분류 모델, 임베딩 모델, reranker, 전통적인 PyTorch 모델에는 InferenceService가 더 단순하고 적합할 수 있다. 반면 대형 LLM, 멀티 GPU 추론, cache-aware routing, disaggregated serving을 고려한다면 LLMInferenceService 쪽을 봐야 한다.
트래픽 분할과 카나리 배포
모델 배포에서 가장 무서운 순간은 새 모델을 전체 사용자에게 한 번에 여는 순간이다. 웹 애플리케이션도 카나리 배포가 필요하지만, LLM은 더 조심해야 한다. 새 모델은 서버 오류를 내지 않더라도 답변 품질이 달라질 수 있다. 말투가 바뀌거나, JSON 형식을 덜 지키거나, 특정 도메인 지식에서 퇴행할 수 있다. 그래서 모델 배포는 코드 배포보다 더 느리고 관찰 가능해야 한다.
KServe의 InferenceService 계열은 트래픽 분할을 추상화해준다. 예를 들어 기존 모델에 90%, 새 모델에 10%만 보내는 식으로 배포할 수 있다. LLMInferenceService에서도 Gateway API와 라우터 계층을 통해 모델 버전별 트래픽 분할을 설계할 수 있다.
운영에서는 다음과 같은 흐름이 일반적이다.
새 모델을 별도 revision 또는 별도 서비스로 배포한다.
내부 사용자와 평가 트래픽만 먼저 보낸다.
실제 사용자 트래픽의 1%, 5%, 10% 순서로 늘린다.
오류율, TTFT, 토큰당 지연, JSON 파싱 실패율, 사용자 피드백을 함께 본다.
문제가 없으면 전체 전환하고, 문제가 있으면 즉시 이전 모델로 되돌린다.
여기서 중요한 지표는 HTTP 500만이 아니다. LLM 서비스는 200 OK를 반환하면서도 실패할 수 있다. 예를 들어 함수 호출 JSON이 깨지거나, RAG 답변이 출처를 누락하거나, 응답이 너무 길어져 비용이 튈 수 있다. 따라서 카나리 배포에서는 인프라 지표와 모델 품질 지표를 같이 봐야 한다.
KV 캐시는 라우팅 문제다
LLM 추론에서 KV 캐시는 단순한 내부 최적화가 아니다. 쿠버네티스 라우팅과 replica 설계에 직접 영향을 주는 운영 요소다.
일반적인 웹 서버는 stateless하게 만들수록 좋다. 요청 A가 어느 Pod로 가든 같은 결과가 나오고, 로드밸런서는 대충 고르게 분산하면 된다. 하지만 LLM 추론 서버는 조금 다르다. 특정 Pod가 이미 어떤 긴 시스템 프롬프트나 대화 이력의 KV 캐시를 갖고 있다면, 다음 요청도 그 Pod로 보내는 편이 유리할 수 있다. 반대로 라운드로빈으로 매번 다른 Pod에 보내면 같은 prefix를 계속 다시 계산한다.
llm-d와 KServe를 함께 사용한 운영 사례에서도 단순한 StatefulSet 기반 vLLM 배포는 대형 모델 스토리지, 로컬 볼륨 고정, 라운드로빈 로드밸런싱의 한계를 드러냈고, prefix-cache aware routing을 적용한 뒤 처리량 개선과 TTFT 감소를 관찰했다고 보고했다.5 중요한 것은 수치 자체보다 방향이다. LLM 라우팅은 더 이상 "빈 Pod를 찾아 보내기"만으로 충분하지 않다. "어느 Pod가 이 요청을 가장 싸고 빠르게 처리할 수 있는가"를 봐야 한다.
KV 캐시가 운영에 미치는 영향은 네 가지로 정리할 수 있다.
GPU 메모리: KV 캐시는 요청 수와 컨텍스트 길이에 따라 커진다. 같은 8B 모델이라도 4K 컨텍스트와 32K 컨텍스트의 동시 처리량은 완전히 다르다.
라우팅: 같은 prefix를 공유하는 요청은 같은 replica로 보내는 것이 유리할 수 있다. 단순 라운드로빈은 캐시 효율을 망친다.
콜드스타트: 새 replica는 모델 가중치만 로드한다고 끝이 아니다. KV 캐시는 비어 있으므로 초반에는 캐시 히트율이 낮다.
replica 설계: replica를 무작정 늘리면 요청은 분산되지만 각 replica의 캐시가 희석된다. 트래픽이 적은데 replica가 너무 많으면 오히려 효율이 떨어질 수 있다.
이 지점이 전통적인 HPA와 LLM 오토스케일링이 갈라지는 곳이다. CPU 사용률 70%를 기준으로 replica를 늘리는 방식은 LLM 서버에 잘 맞지 않는다. GPU 사용률, 대기열 길이, KV 캐시 사용률, TTFT, inter-token latency 같은 지표를 함께 봐야 한다. 오토스케일링 장에서 KEDA와 함께 이 문제를 더 깊게 다룬다.
모델 가중치와 콜드스타트
LLM 서빙에서 콜드스타트는 일반 웹 서버보다 훨씬 무겁다. 웹 서버는 컨테이너가 뜨고 의존성을 로드하면 몇 초 안에 준비될 수 있다. LLM 서버는 수십 GB에서 수백 GB에 이르는 모델 가중치를 내려받고, 디스크에서 읽고, GPU 메모리에 올리고, 런타임을 초기화해야 한다.
작은 7B 모델도 양자화하지 않은 FP16 기준으로는 대략 14GB 이상의 가중치를 가진다. 70B 모델은 FP16 기준으로 140GB 안팎까지 커질 수 있다. 여기에 KV 캐시, 런타임 오버헤드, CUDA 컨텍스트, tokenizer 파일, LoRA 어댑터가 더해진다. 이런 모델을 Pod가 재시작할 때마다 인터넷에서 다시 받는 것은 운영 사고에 가깝다.
따라서 모델 저장 전략은 추론 서빙의 핵심 설계 요소다. vLLM 쿠버네티스 예제에서도 PVC를 사용해 Hugging Face 모델 캐시를 보존하는 구성을 안내한다.1 KServe 기반 대형 모델 배포 사례에서도 대형 모델 다운로드와 ephemeral storage 한계를 피하기 위해 PVC를 사용하고, storage initializer의 메모리 제한을 조정하는 방식이 소개된다.6
실무에서는 다음 원칙을 권장한다.
개발과 실험 환경에서는 Hugging Face에서 직접 내려받아도 된다.
프로덕션에서는 모델 가중치를 사내 오브젝트 스토리지나 전용 모델 레지스트리에 고정한다.
대형 모델은 PVC 또는 로컬 NVMe 캐시를 사용해 재시작 때 다운로드를 반복하지 않게 한다.
startupProbe를 충분히 길게 잡아 모델 로딩 중 재시작 루프에 빠지지 않게 한다.readiness는 "프로세스가 떴다"가 아니라 "모델이 실제 요청을 받을 수 있다"를 기준으로 한다.
콜드스타트를 줄이려면 모델 다운로드와 모델 로딩을 분리해서 생각해야 한다. PVC가 있으면 다운로드 시간은 줄일 수 있다. 하지만 GPU 메모리에 모델을 올리는 시간은 여전히 남는다. scale-to-zero를 쓸 때 이 점이 특히 중요하다. GPU 비용을 아끼기 위해 replica를 0으로 줄이면, 다음 첫 사용자는 모델 로딩 시간을 기다려야 한다. 실시간 서비스라면 완전한 scale-to-zero 대신 최소 1개 warm replica를 유지하는 편이 나을 수 있다.
작은 모델, 중간 모델, 대형 모델의 배포 방식
LLM 서빙 아키텍처는 모델 크기에 따라 달라진다. 모든 모델을 같은 방식으로 배포하려고 하면 비용을 낭비하거나 장애를 부른다.
작은 모델: 1개 Pod, 1개 GPU
7B 이하 모델이나 양자화된 8B급 모델은 보통 단일 GPU에 올릴 수 있다. 이 경우 가장 단순한 구조가 좋다. vLLM 또는 SGLang Pod 하나가 GPU 하나를 요청하고, 여러 replica를 수평 확장한다.
resources:
limits:
nvidia.com/gpu: "1"이 구조의 장점은 장애 격리가 쉽다는 점이다. replica 하나가 죽어도 다른 replica가 살아 있다. 배포도 단순하고, 노드 스케줄링도 쉽다. H100 같은 고가 GPU가 아니라 L40S, A10, A100 일부, 또는 MIG slice를 활용할 수 있는 경우도 있다.
작은 모델에서 주의할 점은 replica를 너무 많이 늘리지 않는 것이다. 트래픽이 충분하지 않은데 replica를 10개로 늘리면 각 replica의 KV 캐시는 따로 놀고, GPU 사용률은 낮아진다. 작은 모델은 빠르게 많이 띄우는 것보다, 실제 대기열과 지연 시간을 보고 적당히 늘리는 것이 중요하다.
중간 모델: 1개 Pod, 여러 GPU
30B에서 70B 전후 모델은 단일 GPU에 들어가지 않거나, 들어가더라도 KV 캐시 여유가 부족할 수 있다. 이때는 텐서 병렬을 사용해 하나의 모델을 여러 GPU에 나눠 올린다. vLLM에서는 --tensor-parallel-size로 지정한다.
args:
- >
vllm serve Qwen/Qwen2.5-32B-Instruct
--tensor-parallel-size 2
--max-model-len 8192
--gpu-memory-utilization 0.90
resources:
limits:
nvidia.com/gpu: "2"이 구조에서는 Pod 하나가 GPU 2개 또는 4개를 한 번에 잡아야 한다. 따라서 스케줄링 난도가 올라간다. GPU가 한 장씩 흩어져 남아 있어도 Pod는 뜨지 못한다. GPU 토폴로지, 노드 affinity, taint/toleration, DRA의 중요성이 여기서 다시 드러난다.
중간 모델은 단일 Pod 장애의 영향도 커진다. GPU 4개를 사용하는 Pod 하나가 죽으면 GPU 4개 분량의 처리 능력이 한 번에 사라진다. readiness와 graceful shutdown을 더 신중하게 잡아야 하고, 롤링 업데이트 중 가용 용량이 충분한지도 계산해야 한다.
대형 모델: 멀티 GPU, 때로는 멀티 노드
70B 이상, 100B 이상, MoE 모델, 긴 컨텍스트 모델은 단일 노드만으로 부족할 수 있다. 이때는 텐서 병렬, 파이프라인 병렬, 데이터 병렬, expert parallelism 같은 분산 추론 전략이 필요하다. KServe의 LLMInferenceService는 single-node, multi-node, prefill/decode 분리 같은 배포 패턴을 염두에 둔 추상화를 제공한다.3
대형 모델에서는 단순히 nvidia.com/gpu: 8이라고 쓰는 것만으로 충분하지 않다. GPU 간 통신 대역폭이 중요하다. 같은 노드 안의 NVLink로 연결된 8개 GPU와, 네트워크를 건너 연결된 8개 GPU는 성능이 다르다. 멀티 노드 추론에서는 RDMA, InfiniBand, NCCL 설정, 노드 간 지연 시간이 모두 중요해진다.
또 하나의 현실적인 문제는 스토리지다. 대형 모델 가중치는 수십~수백 GB가 될 수 있고, Pod가 여러 개면 다운로드 폭풍이 생긴다. 노드가 교체되거나 scale-out이 발생할 때마다 오브젝트 스토리지에서 같은 모델을 동시에 내려받으면 네트워크와 스토리지 비용이 튄다. 대형 모델에서는 모델 캐시, 로컬 NVMe, 전용 모델 스트리머, PVC 설계를 반드시 함께 봐야 한다.
모델 크기별 시작점을 표로 정리하면 다음과 같다.
| 모델 규모 | 일반적인 배포 | 주요 관심사 | 추천 시작점 |
|---|---|---|---|
| 1B~8B | 단일 Pod, 단일 GPU | 빠른 배포, replica 수, 비용 | vLLM/SGLang Deployment 또는 LLMInferenceService |
| 13B~34B | 단일 Pod, 1~2 GPU | GPU 메모리, 양자화, 텐서 병렬 | vLLM --tensor-parallel-size 1~2 |
| 70B 전후 | 단일 노드, 4~8 GPU | 토폴로지, PVC, 긴 startupProbe | KServe LLMInferenceService 또는 vLLM raw 배포 |
| 100B 이상/MoE | 멀티 GPU 또는 멀티 노드 | RDMA, 분산 라우팅, expert parallelism | LLMInferenceService, llm-d, 전용 분산 서빙 검토 |
표는 절대적인 기준이 아니다. 양자화 방식, GPU 메모리, 컨텍스트 길이, 동시 요청 수에 따라 달라진다. 8B 모델도 128K 컨텍스트와 높은 동시성을 요구하면 결코 작지 않다. 반대로 70B 모델도 4-bit 양자화와 낮은 동시성이라면 더 작은 구성으로 시작할 수 있다.
KServe와 vLLM을 함께 쓸 때의 운영 구조
이제 세 가지 층을 합쳐 보자. 프로덕션에서 가장 현실적인 구조는 다음과 같다.
Client
↓ OpenAI-compatible API
API Gateway / Envoy / Envoy AI Gateway
↓ model-aware routing
KServe LLMInferenceService
↓ manages workloads
vLLM or SGLang Pods
↓
GPU Nodes + Model Cache + Metrics클라이언트는 OpenAI 호환 API만 본다. 게이트웨이는 인증, rate limit, 테넌트 구분, 모델 이름 기반 라우팅을 처리한다. KServe는 모델 서비스의 생명주기와 라우팅 리소스를 관리한다. vLLM이나 SGLang은 실제 GPU 추론을 수행한다. Prometheus와 Grafana는 GPU 사용률, 요청 수, TTFT, 토큰 처리량, 오류율을 수집한다.
KServe, llm-d, vLLM을 조합한 운영 사례에서는 Envoy, Envoy AI Gateway, Gateway API Inference Extension을 활용해 단순 라운드로빈을 넘어 prefix-cache aware routing을 구현하는 방향이 제시되었다.5 이 구조의 핵심은 쿠버네티스의 표준 API 표면을 유지하면서도 LLM 추론에 필요한 지능형 라우팅을 추가하는 것이다.
물론 모든 팀이 처음부터 이 구조로 갈 필요는 없다. 작은 팀이 모델 하나를 운영한다면 raw vLLM Deployment와 ClusterIP Service로 충분하다. 그러나 모델 수가 늘고, 팀별 quota와 배포 권한, 카나리, 오토스케일링, 외부 API 폴백이 필요해지면 KServe 같은 상위 추상화가 필요해진다.
자체 모델과 외부 LLM을 함께 운영하기
현실적인 AI 서비스는 자체 모델만 쓰지도, 외부 모델만 쓰지도 않는다. 비용, 품질, 지연 시간, 보안 요구가 요청마다 다르기 때문이다.
예를 들어 TILNOTE 같은 AI 콘텐츠 플랫폼을 생각해보자. 사용자가 짧은 제목 후보를 10개 생성하는 작업은 자체 8B 모델로도 충분할 수 있다. 긴 기획서 초안을 쓰거나 고품질 문체가 필요한 작업은 Azure OpenAI의 대형 모델을 쓰는 편이 나을 수 있다. 사내 문서 기반 Q&A는 데이터 유출 우려 때문에 자체 모델과 벡터 DB를 써야 할 수 있다. 이미지 설명이나 멀티모달 작업은 Bedrock이나 Gemini 계열 외부 API를 호출할 수 있다.
이때 애플리케이션 코드 곳곳에 if provider == "azure", if provider == "local" 같은 분기를 흩뿌리면 금방 유지보수 지옥이 된다. 대신 내부 LLM 게이트웨이를 두고, 모든 요청을 OpenAI 호환 형식으로 받게 한다.
model: fast-summary → 내부 vLLM 8B
model: agent-json → 내부 SGLang 14B
model: premium-writing → Azure OpenAI
model: fallback-general → Bedrock 또는 다른 외부 LLM
model: embedding-ko → 내부 embedding InferenceService이 구조는 비용 제어에도 좋다. 게이트웨이는 사용자 플랜별로 접근 가능한 모델을 제한할 수 있고, 토큰 사용량을 기록할 수 있으며, 장애 시 폴백 정책을 적용할 수 있다. 플랫폼 팀은 모델을 바꾸더라도 애플리케이션 팀과 API 계약을 다시 맺을 필요가 없다.
쿠버네티스 입장에서 외부 LLM은 클러스터 밖에 있는 또 다른 백엔드일 뿐이다. ServiceEntry나 egress gateway, ExternalName Service, 또는 애플리케이션 레벨 프록시로 연결할 수 있다. 중요한 것은 외부 API 키와 라우팅 정책을 애플리케이션마다 흩뿌리지 않고, 중앙에서 관찰하고 통제할 수 있게 만드는 것이다.
작은 팀을 위한 현실적인 시작점
모든 팀이 처음부터 KServe, LLMInferenceService, llm-d, Gateway API Inference Extension을 다 넣을 필요는 없다. 오히려 처음부터 너무 많은 계층을 넣으면 장애가 났을 때 어디를 봐야 할지 모른다.
작은 팀의 시작점은 다음 정도가 현실적이다.
Client
→ API Gateway 또는 Gateway API
→ Service
→ vLLM 또는 SGLang Deployment
→ PVC model cache
→ GPU Node처음에는 raw Deployment로 모델 서버 하나를 안정적으로 띄운다. 모델 로딩 시간, readinessProbe, GPU 메모리 사용량, OpenAI 호환 API 응답, graceful shutdown을 확인한다. 그다음 모델 수가 늘고, 카나리와 라우팅이 필요해지면 KServe를 검토한다. prefix cache와 disaggregated 추론이 중요해지면 llm-d나 Gateway API Inference Extension 같은 상위 계층을 검토한다.
운영자는 도구의 이름보다 단계의 이유를 이해해야 한다. vLLM은 모델을 빠르게 실행하기 위한 도구다. KServe는 모델 서비스를 선언하고 운영하기 위한 도구다. Gateway와 AI Gateway는 여러 모델과 외부 API를 안전하게 묶기 위한 도구다. 이 역할을 구분하면 스택이 커져도 길을 잃지 않는다.
정리: LLM 서빙은 런타임, 라우팅, 운영의 합이다
LLM 추론 서빙은 단순히 모델 컨테이너를 띄우는 일이 아니다. vLLM과 SGLang 같은 런타임은 GPU 메모리와 배치 효율을 책임진다. OpenAI 호환 API는 애플리케이션과 모델 제공자 사이의 계약을 단순하게 만든다. KServe와 LLMInferenceService는 모델 서비스를 쿠버네티스 리소스로 선언하게 해준다. Gateway와 AI Gateway는 모델별 라우팅, 인증, 비용 통제, 외부 API 폴백을 담당한다.
가장 중요한 것은 경계를 아는 것이다. 작은 모델 하나를 운영할 때는 raw Deployment로 충분하다. 여러 모델과 팀이 들어오면 KServe 같은 추상화가 필요하다. 긴 컨텍스트, prefix cache, prefill/decode 분리, 대형 모델이 등장하면 라우팅과 토폴로지까지 설계해야 한다.
이 장에서 익힌 prefill, decode, KV 캐시, OpenAI 호환 API, vLLM, SGLang, KServe의 개념은 다음 장의 disaggregated 추론으로 이어진다. 이제 병목은 모델 서버 하나 안에서 끝나지 않는다. 여러 모델 서버 사이에서 KV 캐시를 어떻게 옮기고, 어떤 GPU 풀에 어떤 단계를 배치할지가 새로운 질문이 된다.
참고
3KServe LLMInferenceService Documentation
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.