반응형
GPU 노드 세팅이 끝났으면 이제 LLM을 올릴 차례입니다. 근데 일반 웹 컨테이너 올리듯 하면 90초 만에 OOMKilled 납니다. 모델이 뜨는 데만 20분인데 readinessProbe가 30초 만에 죽입니다. AI 추론 서버는 시작부터 다르게 접근해야 합니다.
📌 핵심 요약
→ 2026 기준 vLLM = 프로덕션 표준 (Meta·Mistral·Cohere·IBM 모두 사용)
→ TGI는 2025년 12월 유지보수 모드 전환 — 신규 배포 권장 안 함
→ Triton = LLM 엔진 아님, TensorRT-LLM 백엔드 필요한 플랫폼
→ 모델 가중치 PVC 캐싱 필수 — 없으면 재시작마다 140GB 다운로드
→ startupProbe → readinessProbe → livenessProbe 3단계 분리 — 하나로 퉁치면 죽음
→ /dev/shm emptyDir 필수 — Tensor Parallelism 시 없으면 OOM
→ ConfigMap으로 모델 설정 관리, Secret으로 HF 토큰 격리
→ PodDisruptionBudget: 롤링 업데이트 중 최소 1개 항상 살아있게 보장
실전1 — 프레임워크 선택: 뭘 올려야 하나
vLLM은 독립형 LLM 추론 엔진으로 내장 프로덕션 HTTP 서버를 갖춘 반면, TGI는 2025년 12월부터 유지보수 모드로 전환되어 버그 수정만 계속되며 신규 배포는 vLLM이나 SGLang을 권장합니다. Triton은 LLM 엔진이 아니라 오케스트레이션 플랫폼으로, LLM 서빙에는 TensorRT-LLM 백엔드가 별도로 필요합니다.
[2026 LLM 추론 프레임워크 선택 매트릭스]
프레임워크 처리량 설정 복잡도 OpenAI API 멀티모달 추천 상황
──────────────────────────────────────────────────────────────────────
vLLM ★★★★★ 낮음 ✅ 내장 ✅ 대부분의 프로덕션
TensorRT-LLM ★★★★★+ 매우 높음 ❌(Triton) △ NVIDIA H100 최고 성능
SGLang ★★★★ 낮음 ✅ 내장 △ 구조화 출력·긴 컨텍스트
TGI ★★★ 낮음 ✅ △ ❌ 유지보수 모드, 비추천
Triton(단독) ★★★ 매우 높음 ❌ ✅ 멀티모델 파이프라인
→ 신규 프로젝트: vLLM 먼저 시작
→ H100 최대 성능 필요: TensorRT-LLM + Triton (단, ML 인프라팀 있어야 함)
→ TGI 이미 운영 중: 유지하되 신규 모델은 vLLM으로
→ Stripe: vLLM 전환 후 추론 비용 73% 절감 (하루 5천만 API 호출)
실전2 — 모델 가중치 로딩 전략: PVC + Init Container 패턴
모델 캐시에 반드시 PVC를 사용해야 합니다. 없으면 Pod 재시작마다 모델을 다시 다운로드하게 되며, 재시작마다 140GB 다운로드는 프로덕션 장애 직전의 상황입니다.
# model-pvc.yaml — 모델 가중치 영구 저장
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-cache-pvc
namespace: ml-serving
spec:
accessModes:
- ReadWriteOnce # 단일 노드 마운트
storageClassName: gp3 # EKS: gp3 SSD, GKE: pd-ssd
resources:
requests:
storage: 200Gi # 70B 모델 기준 (FP16 = 140GB+)
# model-downloader-job.yaml — 첫 배포 시 1회만 실행하는 다운로더
# Init Container 대신 별도 Job으로 다운로드 → 동시 다운로드 충돌 방지
apiVersion: batch/v1
kind: Job
metadata:
name: model-downloader
namespace: ml-serving
spec:
template:
spec:
restartPolicy: OnFailure
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: downloader
image: python:3.11-slim
command:
- /bin/sh
- -c
- |
pip install huggingface_hub -q
python -c "
from huggingface_hub import snapshot_download
snapshot_download(
repo_id='meta-llama/Llama-3-8B-Instruct',
local_dir='/model-cache/Llama-3-8B-Instruct',
token='$(HF_TOKEN)',
ignore_patterns=['*.pt', '*.bin'] # safetensors만 다운로드
)
print('✅ 모델 다운로드 완료')
"
env:
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-credentials
key: token
volumeMounts:
- name: model-cache
mountPath: /model-cache
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
# S3 기반 모델 로딩 (온프레미스 또는 S3에 모델 보유 시)
# init-container로 S3 → PVC 복사
initContainers:
- name: model-loader
image: amazon/aws-cli:latest
command:
- /bin/sh
- -c
- |
if [ ! -f /model-cache/Llama-3-8B/config.json ]; then
echo "모델 없음, S3에서 다운로드 시작..."
aws s3 sync s3://my-model-bucket/Llama-3-8B/ /model-cache/Llama-3-8B/ \
--no-progress \
--exclude "*.pt" # 구버전 포맷 제외
echo "✅ 다운로드 완료"
else
echo "✅ 모델 캐시 존재, 스킵"
fi
env:
- name: AWS_REGION
value: "ap-northeast-2"
volumeMounts:
- name: model-cache
mountPath: /model-cache
→ Job vs Init Container 다운로드 차이:
Job: 1회 실행 후 완료 → 여러 Pod가 동시 다운로드 충돌 없음 (권장)
Init Container: Pod마다 실행 → 첫 번째 Pod는 다운로드, 나머지는 캐시 확인
→ accessModes ReadWriteOnce: 단일 노드 마운트 → 여러 노드에 배포 시 ReadWriteMany 필요
ReadWriteMany: EFS(AWS), Filestore(GCP), NFS — 속도는 느리지만 공유 가능
→ 모델 파일 형식: safetensors 우선 (.bin, .pt 제외) — 로딩 속도 + 보안
실전3 — vLLM 프로덕션 Deployment: 전체 YAML
# vllm-deployment.yaml — 프로덕션 완전 예시
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-llama3-8b
namespace: ml-serving
labels:
app: vllm-inference
model: llama3-8b
spec:
replicas: 2
selector:
matchLabels:
app: vllm-inference
# 롤링 업데이트 전략
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 한 번에 1개씩 새 Pod 추가
maxUnavailable: 0 # 기존 Pod 먼저 죽이지 않음
template:
metadata:
labels:
app: vllm-inference
model: llama3-8b
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
priorityClassName: inference-critical # 1편에서 정의한 우선순위
serviceAccountName: inference-server
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload-class
operator: In
values: ["inference"]
# 동일 노드에 같은 모델 2개 금지 → 가용성 보장
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: vllm-inference
topologyKey: kubernetes.io/hostname
# ── Init Container: 모델 캐시 확인 ─────────────────
initContainers:
- name: model-cache-check
image: busybox:1.36
command:
- /bin/sh
- -c
- |
echo "모델 캐시 확인..."
if [ ! -f /model-cache/Llama-3-8B-Instruct/config.json ]; then
echo "❌ 모델 없음 — model-downloader Job 먼저 실행하세요"
exit 1
fi
echo "✅ 모델 캐시 확인 완료"
volumeMounts:
- name: model-cache
mountPath: /model-cache
readOnly: true
containers:
- name: vllm
# 버전 고정 필수 — latest 쓰면 재시작마다 다른 버전
image: vllm/vllm-openai:v0.4.2
command: ["python", "-m", "vllm.entrypoints.openai.api_server"]
args:
- "--model=/model-cache/Llama-3-8B-Instruct"
- "--host=0.0.0.0"
- "--port=8000"
- "--tensor-parallel-size=1" # GPU 1개 → 1, 4개 병렬이면 4
- "--gpu-memory-utilization=0.90" # VRAM 90% 사용 (OOM 여유 10%)
- "--max-model-len=8192" # KV 캐시 길이 제한
- "--enable-prefix-caching" # 프리픽스 캐싱 활성화 (처리량 향상)
- "--enable-chunked-prefill" # 청크 프리필 (레이턴시 개선)
- "--disable-log-requests" # 프로덕션: 요청 로그 끔 (성능)
- "--served-model-name=llama3-8b" # OpenAI API model 파라미터 값
env:
- name: HUGGING_FACE_HUB_TOKEN
valueFrom:
secretKeyRef:
name: hf-credentials
key: token
- name: CUDA_VISIBLE_DEVICES
value: "0"
- name: VLLM_WORKER_MULTIPROC_METHOD
value: "spawn"
ports:
- name: http
containerPort: 8000
protocol: TCP
resources:
requests:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: "1" # GPU: requests = limits 필수
# ── Probe 3단계 분리 — 핵심 ─────────────────────
# 70B 모델은 로딩만 20분 → initialDelaySeconds 크게 설정
startupProbe: # 시작 완료 확인 (이 기간은 liveness 실행 안 됨)
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 120 # 60 + (10×120) = 1260초 = 21분 허용
successThreshold: 1
readinessProbe: # 트래픽 받을 준비 됐는지
httpGet:
path: /health
port: 8000
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
livenessProbe: # 살아있는지 (죽으면 재시작)
httpGet:
path: /health
port: 8000
periodSeconds: 10
failureThreshold: 6 # 60초 무응답 시 재시작
volumeMounts:
- name: model-cache
mountPath: /model-cache
readOnly: true
- name: shm # Tensor Parallelism용 공유메모리 — 필수
mountPath: /dev/shm
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: 16Gi # GPU VRAM의 절반 정도 설정
→ startupProbe가 핵심: 이 기간 동안 livenessProbe 실행 안 됨
→ 모델 로딩 중 liveness가 죽이는 사고 방지
→ failureThreshold 계산:
7B 모델: initialDelaySeconds=60, failureThreshold=30 (약 5분)
70B 모델: initialDelaySeconds=60, failureThreshold=120 (약 21분)
→ /dev/shm emptyDir: tensor-parallel-size > 1이면 반드시 필요
없으면 GPU 워커 간 통신 실패 → OOMKilled
→ --gpu-memory-utilization=0.90: 0.95 이상이면 OOM 위험
KV 캐시 + 모델 가중치 + 오버헤드 합산으로 계산
실전4 — ConfigMap·Secret: 모델 설정 관리
# model-config.yaml — ConfigMap으로 모델 설정 분리
apiVersion: v1
kind: ConfigMap
metadata:
name: vllm-config
namespace: ml-serving
data:
# 모델별 설정을 ConfigMap으로 관리 → 코드 변경 없이 설정 변경
MODEL_PATH: "/model-cache/Llama-3-8B-Instruct"
TENSOR_PARALLEL_SIZE: "1"
GPU_MEMORY_UTILIZATION: "0.90"
MAX_MODEL_LEN: "8192"
MAX_NUM_SEQS: "256" # 동시 처리 최대 시퀀스 수
SERVED_MODEL_NAME: "llama3-8b"
---
# hf-secret.yaml — HuggingFace 토큰 Secret 분리
apiVersion: v1
kind: Secret
metadata:
name: hf-credentials
namespace: ml-serving
type: Opaque
stringData:
token: "hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 실제 배포 시 Sealed Secret 또는 ESO 사용
---
# network-policy.yaml — 추론 서버 네트워크 격리
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: inference-network-policy
namespace: ml-serving
spec:
podSelector:
matchLabels:
app: vllm-inference
policyTypes:
- Ingress
- Egress
ingress:
# API Gateway / 모니터링 스택만 인바운드 허용
- from:
- namespaceSelector:
matchLabels:
name: api-gateway
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 8000
egress:
# HuggingFace 토큰 검증·아웃바운드 제한
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # AWS 메타데이터 서버 차단
ports:
- protocol: TCP
port: 443
실전5 — Service + PodDisruptionBudget
# vllm-service.yaml
apiVersion: v1
kind: Service
metadata:
name: vllm-inference-svc
namespace: ml-serving
labels:
app: vllm-inference
spec:
selector:
app: vllm-inference
ports:
- name: http
port: 80
targetPort: 8000
protocol: TCP
type: ClusterIP # 외부 노출은 Ingress/Gateway에서 담당
---
# pdb.yaml — 롤링 업데이트 중 서비스 끊김 방지
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: vllm-pdb
namespace: ml-serving
spec:
minAvailable: 1 # 최소 1개 항상 Running 유지
selector:
matchLabels:
app: vllm-inference
→ PDB 없이 롤링 업데이트 시:
replicas=2 → 한 번에 2개 다 죽고 재시작 가능 → 서비스 다운
→ PDB minAvailable: 1 적용 시:
1개 살아있는 상태에서만 다음 Pod 교체 → 다운타임 없음
→ GPU 노드가 1개뿐이면 maxUnavailable: 0 + maxSurge: 1 조합 불가
→ 먼저 새 노드 확보 후 교체하는 blue/green 방식 필요
실전6 — TGI 기존 운영 중이라면: 마이그레이션 패턴
# TGI Deployment (기존 운영 중인 팀 참고용 — 신규는 vLLM 권장)
apiVersion: apps/v1
kind: Deployment
metadata:
name: tgi-llama3-8b
namespace: ml-serving
spec:
replicas: 1
template:
spec:
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: tgi
image: ghcr.io/huggingface/text-generation-inference:2.4.0 # 버전 고정
args:
- "--model-id=/model-cache/Llama-3-8B-Instruct"
- "--port=8080"
- "--max-total-tokens=8192"
- "--max-input-tokens=4096"
- "--max-batch-prefill-tokens=16384"
- "--num-shard=1"
resources:
limits:
nvidia.com/gpu: "1"
# TGI는 /health가 아닌 별도 엔드포인트
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 120
periodSeconds: 10
volumeMounts:
- name: model-cache
mountPath: /model-cache
- name: shm
mountPath: /dev/shm
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: 8Gi
[TGI → vLLM 마이그레이션 전략]
1단계: vLLM을 별도 Deployment로 배포 (TGI와 공존)
2단계: Service 가중치 조정 (TGI 90% → vLLM 10% → 점진 증가)
3단계: 메트릭 비교 (TTFT, 처리량, 오류율)
4단계: vLLM 100% 전환 후 TGI Deployment 제거
→ OpenAI 호환 API 덕분에 클라이언트 코드 변경 없음
→ 포트만 다르게 설정 → Ingress weight 조절로 트래픽 분산
→ TGI v3 여전히 성능은 나쁘지 않음 — 급하게 마이그레이션 불필요
실전7 — 배포 후 검증: API 동작 확인
# 1. Pod 상태 확인
kubectl get pods -n ml-serving -w
# NAME READY STATUS RESTARTS
# vllm-llama3-8b-xxx-yyy 0/1 Init:0/1 0 ← init container 실행 중
# vllm-llama3-8b-xxx-yyy 0/1 Running 0 ← 모델 로딩 중
# vllm-llama3-8b-xxx-yyy 1/1 Running 0 ← 완료
# 2. 로그로 로딩 상태 확인
kubectl logs -n ml-serving -l app=vllm-inference -f
# INFO: Loading model weights...
# INFO: GPU blocks: 1024, CPU blocks: 512
# INFO: Application startup complete. ← 이게 나와야 정상
# 3. 포트 포워딩으로 직접 테스트
kubectl port-forward -n ml-serving svc/vllm-inference-svc 8080:80 &
# 4. OpenAI 호환 API 테스트
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama3-8b",
"messages": [{"role": "user", "content": "K8s가 뭔가요?"}],
"max_tokens": 200,
"temperature": 0.7
}'
# 5. 모델 목록 확인
curl http://localhost:8080/v1/models
# {"data":[{"id":"llama3-8b","object":"model",...}]}
# 6. GPU 사용률 확인
kubectl exec -n ml-serving -it $(kubectl get pod -n ml-serving -l app=vllm-inference -o name | head -1) \
-- nvidia-smi
# GPU 메모리 사용량, 온도, 전력 소비 확인
→ 가장 흔한 배포 실패 3가지:
1) OOMKilled 즉시 발생
→ --gpu-memory-utilization 낮춤 (0.90 → 0.85)
→ /dev/shm emptyDir 추가 확인
→ 모델이 GPU VRAM보다 큼 → 양자화 적용 (--quantization awq)
2) startupProbe failureThreshold 초과 → CrashLoopBackOff
→ 모델 크기별 로딩 시간 계산 후 failureThreshold 늘림
→ 7B: 5분, 13B: 10분, 70B: 25분 기준
3) Pod는 Running인데 API 500
→ kubectl logs 확인 → 모델 경로 틀림 or HF 토큰 만료
→ /v1/models 호출로 모델 인식 여부 먼저 확인
✅ 2편 핵심 정리
✅ 2026 프레임워크 선택: vLLM(기본) → TensorRT-LLM(H100 최적화) → TGI(유지보수 모드, 비추천)
✅ 모델 가중치 PVC 캐싱 필수 — 재시작마다 140GB 다운로드는 장애
✅ startupProbe 3단계 분리 — 로딩 중 liveness가 죽이는 사고 방지
✅ /dev/shm emptyDir: Tensor Parallelism 사용 시 반드시 추가
✅ PodDisruptionBudget: 롤링 업데이트 중 최소 1개 항상 유지
✅ NetworkPolicy: API Gateway·모니터링만 인바운드 허용
❌ image: latest 사용 금지 — 재시작마다 다른 버전 뜰 수 있음
❌ readinessProbe 단독으로 쓰면 모델 로딩 중 죽임 — startupProbe 필수
❌ --gpu-memory-utilization 0.95+ 금지 — OOM 여유 없음
❌ TGI 신규 배포 비추천 — 유지보수 모드, vLLM으로 시작할 것
관련글
반응형
'AI 개발' 카테고리의 다른 글
| K8s AI 워크로드 4편—프로덕션 관찰가능성·카나리 배포·비용 최적화, 운영에서 살아남는 법 (0) | 2026.05.23 |
|---|---|
| K8s AI 워크로드 3편—KEDA 스케일링과 멀티테넌시, HPA가 LLM에 왜 안 되는지부터 (0) | 2026.05.23 |
| K8s AI 워크로드 1편—GPU 노드 설정과 인프라 기초, 비싼 GPU 낭비 없이 쓰는 법 (0) | 2026.05.23 |
| AWS Kiro 3편—AgentCore 연동과 CDK 자동화, 코드 한 줄 없이 서버리스 풀스택 배포까지 (0) | 2026.05.22 |
| AWS Kiro 2편—Specs Wave 실행·Steering 아키텍처·Autonomous Agent, 팀 프로덕션에서 살아남는 법 (0) | 2026.05.22 |