네. 이 경우에는 처음부터 LiteLLM/자동 전환까지 한꺼번에 검증하기보다, “각 모델의 B300 최적 vLLM 설정을 찾는 Stage 1 → 실제 주/야간 모델 세트를 검증하는 Stage 2 → 전환 자동화를 검증하는 Stage 3” 순서가 좋습니다.
특히 지금 가장 먼저 알아야 하는 것은 각 모델에 대해 몇 GPU가 필요한지, TP/DP/EP를 어떻게 잡아야 하는지, HBM 중 weight와 KV에 각각 얼마를 쓰는지가 최적인지입니다. vLLM도 single-node에서는 모델이 1 GPU에 들어가면 single GPU, 그렇지 않으면 TP를 늘리는 접근을 기본 가이드로 제시하고 있습니다. MoE에서는 EP를 별도로 실험할 가치가 있습니다. vLLM
Stage 1 모델별 단독 Characterization
│
│ GPU / TP / EP / KV 최적값 탐색
▼
Stage 2 주간/야간 Model-set 동시 구동
│
│ GPU allocation / 간섭 / capacity 검증
▼
Stage 3 AIStor → NVMe → HBM 전환 테스트
│
│ cold/warm start 측정
▼
Stage 4 Day ↔ Night 자동 전환
│
│ prefetch → load → warmup → switch → drain
▼
Stage 5 LiteLLM + multi-user 부하
│
▼
Production baseline
처음부터 LiteLLM을 넣지 않는 것이 포인트입니다. Stage 1~3에서는 vLLM endpoint를 직접 때려서 GPU serving 자체를 먼저 검증하는 편이 원인 분석이 쉽습니다.
우선 모델 하나만 B300 node에 올립니다.
예를 들어 Qwen 계열 MoE라면 다음과 같은 matrix를 만들겠습니다.
| Test | GPU | TP | DP | EP | 목적 |
|---|---|---|---|---|---|
| Q1 | 1 | 1 | 1 | OFF | 1 GPU 수용 가능 여부 |
| Q2 | 2 | 2 | 1 | OFF | TP baseline |
| Q3 | 2 | 1 | 2 | ON | EP 효과 |
| Q4 | 4 | 1 | 4 | ON | EP scaling |
| Q5 | 4 | 2 | 2 | ON | TP+EP 조합 |
| Q6 | 8 | 1 | 8 | ON | full-node EP |
중요한 점은 최신 vLLM에서 --enable-expert-parallel을 켠 경우 expert layer의 EP size가 TP×DP로 구성된다는 것입니다. 예를 들어 TP=2, DP=4이면 8 GPU에서 expert layer는 EP=8이고 attention layer는 각 DP group 안에서 TP=2가 됩니다. vLLM
따라서 앞에서 이야기했던 TP/DP/EP를 완전히 독립적인 세 숫자로 생각하기보다는 실제 사용하는 vLLM 버전의 구현에 맞춰 test matrix를 작성해야 합니다.
모델마다 결국 아래 한 줄을 만드는 것이 목표입니다.
Qwen3.x-35B-A3B
GPU 2
TP 1
DP 2
EP enabled (effective EP=2)
max-model-len 32K
KV HBM xx GiB/GPU
GPU memory xx %
CPU 24 cores
NUMA 0
Throughput xxxx tok/s
TTFT P50/P95 xx / xx ms
TPOT P50/P95 xx / xx ms
Max concurrency xx
NVMe→HBM load xx sec
이게 만들어지면 나중에 Kubernetes Deployment YAML을 만드는 것은 상대적으로 쉽습니다.
LLM serving은 최소한 다음을 같이 봐야 합니다.
Performance
────────────────────
Request throughput
Input tokens/sec
Output tokens/sec
Latency
────────────────────
TTFT Time To First Token
TPOT Time Per Output Token
E2E latency
P50 / P95 / P99
Capacity
────────────────────
Concurrent requests
Queue depth
KV cache usage
Prefix cache hit
GPU
────────────────────
GPU utilization
HBM usage
NVLink traffic
Power
PCIe traffic
System
────────────────────
CPU utilization
NUMA remote access
RAM
NVMe read throughput
Network
vLLM은 /metrics에서 production metrics를 노출하므로 Prometheus로 수집하는 게 좋습니다. vLLM
그리고 online benchmark에는 vllm bench serve를 사용할 수 있습니다. request rate, input/output length, prompt 수 등을 바꿔 serving throughput을 측정할 수 있습니다. vLLM
이 workload에서는 이게 상당히 중요합니다.
제가 baseline을 잡는다면:
Short
input 1K
output 256
Medium
input 8K
output 1K
Long
input 32K
output 2K
Very Long
input 64K+
output 2K
정도로 나눕니다.
Wizard/문서 처리에서 생성되는 context가 DeepSeek/GLM으로 넘어간다면 32K~64K 이상 workload가 실제로 나올 가능성도 고려해야 합니다.
그리고 concurrency:
1
2
4
8
16
32
64
...
를 올립니다.
즉 한 모델당:
GPU topology
×
context length
×
concurrency
matrix입니다.
예를 들어 결과가:
Concurrency tok/s P95 TTFT
1 150 80 ms
2 290 90 ms
4 550 110 ms
8 980 180 ms
16 1300 350 ms
32 1400 1200 ms
64 1420 4000 ms
라면 throughput만 보면 64가 가장 좋아 보입니다.
하지만 실제 서비스라면:
16 → 32
throughput
+8%
TTFT
+243%
같은 상태일 수 있습니다.
따라서 16 정도를 serving capacity로 잡는 식입니다.
이걸 각 모델마다 찾아야 합니다.
현재 목록은 MoE 모델 비중이 높으므로 중요합니다.
예를 들어 같은 8 GPU에서:
Test A
TP=8
EP=OFF
vs
Test B
TP=1
DP=8
EP=ON
vs
Test C
TP=2
DP=4
EP=ON
을 비교해볼 가치가 있습니다.
vLLM은 MoE에서 EP를 지원하고, 현재는 --all2all-backend로 여러 communication backend도 선택할 수 있습니다. EP workload는 expert token 분포가 치우칠 수 있어서 EPLB(Expert Parallel Load Balancer)도 제공합니다. vLLM
처음부터 EPLB까지 튜닝하지 말고:
EP OFF
↓
EP ON
↓
all-to-all backend 비교
↓
필요 시 EPLB
순서가 좋습니다.
개별 모델 최적값을 찾았으면 그다음부터가 진짜 중요한 테스트입니다.
첨부하신 주간 workload를 그대로 재현합니다.
DAY
DeepSeek-V4.1-Flash
GLM-5.3-Flash
PaddleOCR-VL
Docling layout
Docling models
Figure classifier
예를 들어 B300 node 여러 대가 있다면 실제 production 후보 배치 그대로 올립니다.
GPU Node 01
┌──────────────────────────┐
│ DeepSeek Flash │
│ GPU 0~7 │
└──────────────────────────┘
GPU Node 02
┌──────────────────────────┐
│ GLM Flash │
│ GPU 0~7 │
└──────────────────────────┘
Compute / Small GPU
┌──────────────────────────┐
│ PaddleOCR / Docling │
└──────────────────────────┘
실제 Stage 1 결과에 따라 4 GPU씩 들어간다면:
GPU Node
GPU0 ─┐
GPU1 │ DeepSeek
GPU2 │
GPU3 ─┘
GPU4 ─┐
GPU5 │ GLM
GPU6 │
GPU7 ─┘
도 테스트합니다.
Stage 1 결과를 보기 전에는 어느 쪽이 맞다고 고정하지 않는 게 좋습니다.
각 모델을 독립적으로 때리는 것만으로는 부족합니다.
실제 주간 workflow가:
PDF/Image
│
▼
PaddleOCR
│
▼
Docling
│
├── layout
├── figure
└── context extraction
│
▼
DeepSeek / GLM
│
▼
response
라면 end-to-end workload도 만들어야 합니다.
그래야 GPU serving benchmark에는 안 나타나는:
OCR bottleneck
CPU preprocessing
object download
context serialization
network
tokenization
LLM queue
등을 찾을 수 있습니다.
그다음에는 model loading을 별도 테스트합니다.
세 가지 시간을 측정합니다.
Case A — Cold
AIStor
│
│ S3
▼
NVMe
│
▼
RAM
│
▼
HBM
T_cold = ?
두 번째는:
Case B — Warm NVMe
NVMe
│
▼
RAM
│
▼
HBM
T_warm = ?
세 번째:
Case C — Already loaded
HBM
│
▼
ready
T_ready = ?
이 결과가 주간↔야간 전환 시간을 결정합니다.
예를 들어:
AIStor → NVMe 6 min
NVMe → model ready 2 min
warmup 1 min
total cold 9 min
이면 22:00 전환을 위해 21:30부터 준비하는 게 충분한지 판단할 수 있습니다.
야간에는 첨부하신 세 모델을 실제로 올립니다.
DAY
──────────────────────────
DeepSeek-V4.1-Flash
GLM-5.3-Flash
PaddleOCR/Docling
│
│ prefetch
▼
21:xx AIStor → NVMe
│
▼
NIGHT LOAD
GLM-5.3
Qwen3.x-35B-A3B
gpt-oss-20b
│
▼
health/warmup
│
▼
traffic switch
여기서는 전환시간 자체가 테스트 결과입니다.
측정값을:
Prefetch time
Model load time
Warm-up time
Route switch time
Drain time
총 전환시간
전환 중 실패 request
전환 중 P95/P99 latency
로 남깁니다.
여기까지 통과하면:
Keycloak
│
▼
Users ───────── LiteLLM
│
┌───────┼────────┐
▼ ▼ ▼
GLM Qwen DeepSeek
│ │ │
▼ ▼ ▼
vLLM vLLM vLLM
으로 바꿉니다.
그 후 실제 사용자 패턴을 만들어:
Team A 50 users
Team B 20 users
Batch 5 jobs
RPM
TPM
Concurrency
model quota
를 적용한 부하 테스트를 합니다.
이 단계에서 LiteLLM overhead와 quota가 의도대로 작동하는지를 별도로 측정하면 됩니다.
MemKV도 처음부터 켜면 원인 분석이 어려워집니다.
Test A
vLLM
└── HBM KV only
Test B
vLLM
├── HBM KV
└── MemKV
동일한 workload를 사용해서:
TTFT
TPOT
throughput
KV hit rate
HBM usage
RDMA traffic
를 비교합니다.
특히:
Shared system prompt
Repeated RAG context
Multi-turn conversation
Long context
를 따로 만들어야 합니다.
random prompt만 사용하면 external KV cache의 효과를 제대로 측정하지 못할 수 있습니다.
nvidia-smi, topology, NVLink/NVSwitch, NCCL, HBM, CPU NUMA부터 검증합니다.여기서 가장 먼저 만들 산출물은 모델별 Test Matrix입니다. 예를 들어 각 모델마다 precision × GPU수 × TP × DP × EP × max-model-len × concurrency × gpu-memory-utilization을 바꾸면서 HBM / KV capacity / TTFT P95 / TPOT P95 / tok/s를 자동 수집하고, 마지막에 “이 모델의 B300 권장 vLLM parameter” 한 줄을 자동으로 뽑는 테스트 패키지를 만드는 게 좋습니다.
지금 진행 중인 Stage7의 목적이 바로 “실제 사용 모델별 설정값 찾기”였으므로, 이 테스트를 기존 Stage7에 7a 모델 적재 → 7b TP/EP sweep → 7c context/concurrency sweep → 7d day-set → 7e night-set → 7f AIStor/NVMe cold-warm → 7g 전환으로 넣는 방식이 현재 프로젝트 구조와 가장 잘 맞습니다. vLLM
===
테스트가 끝나 각 모델의 GPU/TP/DP/EP/HBM/KV/context/concurrency 값이 확정됐다고 보면, 실제 서비스는 “모델 Pod들을 띄운다”보다 하나의 Inference Platform으로 운영하는 게 좋습니다.
제가 현재 환경이라면 아래 구조를 목표로 하겠습니다.
┌──────── ADMIN CLUSTER ────────┐
│ │
│ Keycloak │
│ GitOps / CI-CD │
│ Private Registry │
│ Model/Profile Repository │
│ Prometheus / Grafana │
└──────────────┬────────────────┘
│
Management
│
▼
┌──────────────────────────── GPU CLUSTER ─────────────────────────────┐
│ │
│ Compute Cluster / Users │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ LiteLLM │ ← 인증: Keycloak │
│ │ │ │
│ │ API Key / Team │ │
│ │ Model ACL │ │
│ │ RPM / TPM / Quota │ │
│ │ Usage Accounting │ │
│ │ Logical Model Name │ │
│ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Inference Routing Layer │ ← replica 증가 시 │
│ │ Gateway API + EPP │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────┐ │
│ │ │ │
│ │ vLLM Serving Pools │ │
│ │ │ │
│ │ DeepSeek │ GLM │ Qwen │ gpt-oss │ OCR/VLM │ │
│ │ │ │
│ └───────────────────┬────────────────────────────┘ │
│ │ │
│ B300 GPU / HBM │
│ │ │
│ KV L1 / Prefix Cache │
│ │ │
│ MemKV (*) │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Node Local NVMe │ │
│ │ 8 NVMe RAID0 + XFS │ │
│ │ /model-cache │ │
│ └────────────────────────────┬─────────────────────────────────────┘ │
└──────────────────────────────┼───────────────────────────────────────┘
│ S3 / Ethernet
▼
┌──── STORAGE CLUSTER ────┐
│ │
│ AIStor │
│ │
│ models/ │
│ datasets/ │
│ checkpoints/ │
│ artifacts/ │
│ AIMem (*) │
└─────────────────────────┘
여기서 중요한 운영 원칙은 LiteLLM / vLLM / Kubernetes / AIStor의 책임을 섞지 않는 것입니다.
ServingProfile을 고정Stage7 테스트 결과를 그대로 운영 configuration으로 승격합니다. 예를 들어:
model: glm-5.3-flash
runtime: vllm
gpu:
count: 4
tensorParallel: 2
dataParallel: 2
expertParallel: true
memory:
gpuMemoryUtilization: 0.90
maxModelLen: 32768
cpu:
cores: 32
memory: 256Gi
cache:
localNVMe: true
prefixCaching: true
memkv: false
limits:
maxConcurrency: 16
이 값은 사람이 production에서 즉흥적으로 바꾸지 않고 Git으로 관리합니다.
즉:
Stage7 benchmark
│
▼
Approved Serving Profile
│
▼
Git
│
▼
GitOps
│
▼
vLLM Deployment
으로 연결합니다.
이렇게 하면 Stage7 결과가 production configuration의 근거가 됩니다.
하나의 거대한 vLLM에서 모델을 계속 갈아끼우는 것보다:
glm-5.3-flash
│
└── Deployment/ServingPool
deepseek-v4.1-flash
│
└── Deployment/ServingPool
qwen
│
└── Deployment/ServingPool
처럼 모델 단위 serving pool로 가져가는 것을 권합니다.
그러면 모델별로:
GPU
TP/DP/EP
CPU
memory
max-model-len
KV
replica
SLA
를 독립적으로 설정할 수 있습니다.
장애 격리도 훨씬 쉽습니다.
외부에서는:
/v1/chat/completions
model = glm
model = deepseek
model = qwen
model = gpt-oss
정도만 보이게 합니다.
내부적으로:
LiteLLM
glm
│
└─> glm-5.3-night-prod-v4
glm-flash
│
└─> glm-5.3-flash-day-prod-v2
deepseek
│
└─> deepseek-v4.1-flash-prod-v3
로 mapping합니다.
그래야 model version이나 TP 설정을 바꿔도 client가 영향을 받지 않습니다.
이 환경에서는 개별 Deployment를 하나씩 스케줄링하는 것보다 Day Serving Set / Night Serving Set이라는 개념으로 관리하는 것이 좋습니다.
DAY Serving Set
────────────────────────────
DeepSeek-V4.1-Flash
GLM-5.3-Flash
PaddleOCR-VL
Docling
NIGHT Serving Set
────────────────────────────
GLM-5.3
Qwen3.x-35B-A3B
gpt-oss-20b
그리고 Git에는:
day:
start: "08:00"
models:
- deepseek-v4.1-flash
- glm-5.3-flash
- paddleocr
night:
start: "22:00"
models:
- glm-5.3
- qwen3.6-35b-a3b
- gpt-oss-20b
정도의 선언적 configuration을 둡니다.
여기가 production에서 제일 중요합니다.
예를 들어 08:00 전환이라면:
07:20
│
├─ Day Serving Set 확인
│
▼
AIStor
│
│ manifest/hash/version check
▼
Local NVMe
│
│ Cache HIT?
├──────── YES ─────────┐
│ │
NO │
│ │
AIStor → NVMe │
│ │
└──────────────────────┘
│
07:40 ▼
Start vLLM
│
NVMe → HBM
│
07:50
Warm-up inference
│
Health / readiness
│
▼
HEALTHY ?
/ \
NO YES
│ │
ABORT 08:00
│ │
Night 유지 Route Switch
│
▼
Night drain
│
▼
Night unload
즉 08:00은 Pod를 시작하는 시간이 아니라 traffic을 넘기는 목표 시간입니다.
이 차이가 중요합니다.
초기에는 별도의 custom operator까지 만들 필요 없습니다.
기존 Admin cluster의 GitOps/Workflow를 이용해서:
Model Lifecycle Workflow
1. Resolve desired ServingSet
2. Check AIStor manifest
3. Check local cache
4. Prefetch missing model
5. Verify checksum
6. Deploy vLLM
7. Wait Ready
8. Warm-up
9. Synthetic inference test
10. Switch route
11. Drain old model
12. Scale old deployment to 0
정도로 만들면 됩니다.
운영이 복잡해지면 나중에 이를 Kubernetes Operator/Controller로 승격하면 됩니다.
서비스 환경에서도 모델을 매번 AIStor에서 직접 읽지 않습니다.
AIStor
Source of Truth
│
S3 / Ethernet
│
▼
Local NVMe Cache
8 × RAID0
│
▼
RAM
│
▼
HBM
NVMe 용량이 충분하다면 가장 좋은 것은:
/model-cache/
├── day/
│ ├── deepseek-v4.1-flash
│ └── glm-5.3-flash
│
└── night/
├── glm-5.3
├── qwen
└── gpt-oss
를 동시에 보관하는 것입니다.
그러면 매일 주/야간마다:
AIStor → NVMe
를 반복하지 않고 대부분:
NVMe → HBM
만 수행합니다.
AIStor에서는 version이 변경됐을 때만 새 artifact를 받으면 됩니다.
GPU node가 여러 대라면 이것도 중요해집니다.
예를 들어:
GPU01 local NVMe
├ DeepSeek
└ GLM-Flash
GPU02 local NVMe
├ GLM
├ Qwen
└ gpt-oss
상태에서 Qwen Pod를 GPU01로 보내면 다시 AIStor에서 Qwen을 받아야 합니다.
그래서 Model Lifecycle/Scheduler 쪽에서 가능하면:
model cache locality
+
GPU availability
+
NUMA/GPU topology
를 고려하는 게 좋습니다.
초기에는 node label만으로도 충분합니다.
model-cache/qwen=true
model-cache/glm=true
같은 식입니다.
단, label을 사람이 관리하기보다 cache agent가 실제 cache 상태를 확인해 갱신하도록 발전시키는 편이 안전합니다.
주/야간에 LiteLLM 자체를 내렸다 올릴 이유는 없습니다.
LiteLLM
24 × 7 실행
│
┌────────────────┼─────────────────┐
│ │ │
User A User B Batch
여기서:
authentication
API key
Team
Model ACL
RPM
TPM
Concurrency
Token quota
Usage accounting
을 담당합니다.
즉 GPU workload lifecycle과 사용자 control plane을 분리합니다.
Production이면 LiteLLM도 하나만 두지 않습니다.
Load Balancer
│
┌────────┴────────┐
▼ ▼
LiteLLM #1 LiteLLM #2
│ │
└────────┬────────┘
▼
PostgreSQL
LiteLLM Pod가 죽는 것이 전체 AI 서비스 장애가 되지 않도록 합니다.
그리고 GPU resource는 필요 없습니다.
처음에:
glm
│
▼
vLLM Pod 1
뿐이라면 필요 없습니다.
하지만:
glm
│
┌─────────┼─────────┐
▼ ▼ ▼
vLLM-1 vLLM-2 vLLM-3
이 되면:
LiteLLM
│
▼
Gateway API
│
InferencePool
│
EPP
│
├── queue
├── load
├── KV/cache locality
└── serving state
│
▼
vLLM-x
구조로 발전시키면 됩니다.
즉 처음부터 반드시 넣을 구성요소는 아닙니다.
Stage7 이후 별도의 A/B test에서 효과가 확인된 모델만:
vLLM
│
├── HBM KV
│ │
│ HIT
│
└──── MISS
│
▼
MemKV
를 사용합니다.
예를 들어 최종 ServingProfile에:
kvCache:
prefixCaching: true
external:
enabled: true
provider: memkv
처럼 모델별로 넣습니다.
DeepSeek/GLM long-context/RAG에는 켜지고 gpt-oss short request에는 꺼질 수도 있습니다.
그게 정상입니다.
첨부한 목록에서 이 부분은 조금 다르게 가져가는 것이 좋습니다.
Document API
│
▼
Document Pipeline
│
┌───────────────┼───────────────┐
▼ ▼ ▼
PaddleOCR-VL Docling FigureClassifier
│ │ │
└───────────────┼───────────────┘
▼
Extracted Context
│
▼
LiteLLM
│
▼
DeepSeek / GLM
즉 Docling 계열까지 전부 vLLM에 억지로 넣는 구조는 피하겠습니다.
LLM/VLM을 vLLM ServingPool, document preprocessing/classification은 별도 application service로 관리하는 게 좋습니다.
예를 들어 21:50에 Night GLM이 안 뜹니다.
잘못된 자동화는:
22:00 됨
↓
Day 모델 종료
↓
Night 모델 실패
↓
서비스 장애
입니다.
Production에서는:
Night deployment
│
▼
Ready?
/ \
NO YES
│ │
│ traffic switch
│
Day model 유지
+
alert
가 되어야 합니다.
시간보다 readiness가 우선입니다.
실제로 이게 가장 까다로운 부분일 수 있습니다.
GPU 8장이 100% 사용 중이면 새 모델을 HBM에 먼저 올려놓을 여유가 없습니다.
그러면 완전한 Blue/Green은 불가능합니다.
이 경우:
AIStor
│
▼
NVMe
Night model prefetch 완료
│
▼
22:00
Day request 차단
│
▼
Drain
│
▼
Day HBM unload
│
▼
Night NVMe → HBM
│
▼
Warmup
│
▼
Night traffic
이 필요합니다.
즉 짧은 maintenance transition window가 생깁니다.
이를 없애려면 최소한 전환용 spare GPU capacity가 필요합니다.
그래서 Stage7 이후 capacity planning에서 단순히:
8 GPU에 모델이 딱 들어간다.
가 아니라:
전환 시에도 SLA를 만족시키는가?
를 봐야 합니다.
Production에서는 GPU를 항상 100% 꽉 채우는 설계보다:
steady state
70~85% capacity
15~30%
headroom
을 어느 정도 확보하는 편이 좋습니다.
이 headroom은:
traffic burst
model rollout
Day/Night transition
GPU failure
warm-up
rescheduling
에 사용됩니다.
특히 지금처럼 모델 세트 자체가 하루 두 번 바뀌는 환경에서는 가치가 큽니다.
저라면 운영 모델을 네 가지 논리 객체로 정리합니다.
① ModelArtifact
AIStor URI
version
hash
precision
② ServingProfile
GPU
TP/DP/EP
CPU
RAM
context
KV
concurrency
③ ServingSet
DAY
NIGHT
④ AccessPolicy
Team
User
Model ACL
RPM
TPM
Quota
그리고:
ModelArtifact
+
ServingProfile
│
▼
vLLM Deployment
ServingSet
│
▼
Lifecycle Controller
AccessPolicy
│
▼
LiteLLM
로 연결합니다.
이렇게 해두면 모델 파일, GPU 최적화 값, 시간대별 운영정책, 사용자 정책이 서로 뒤엉키지 않습니다.
Keycloak
│
▼
LiteLLM HA
+ PostgreSQL
│
▼
[Inference Gateway (*)]
│
▼
vLLM Serving Pools
│
┌──────────┴──────────┐
│ │
HBM / KV MemKV (*)
│
▼
Local NVMe Cache
│
▼
AIStor
운영 Control Plane
──────────────────────────
GitOps
│
├─ ModelArtifact
├─ ServingProfile
├─ DAY/NIGHT ServingSet
└─ AccessPolicy
│
▼
Model Lifecycle Workflow
│
├─ prefetch
├─ verify
├─ deploy
├─ warmup
├─ health check
├─ route switch
└─ drain
(*) Inference Gateway/EPP는 동일 모델 replica가 여러 개가 될 때, MemKV는 Stage7/후속 A-B 테스트에서 이득이 확인될 때 추가합니다. AIMem은 일반 inference path에는 넣지 않고 향후 Agent 서비스가 생길 때 별도 도입하는 편이 좋습니다.
결국 Stage7의 종료 조건도 단순히 “모델이 잘 뜬다”가 아니라, 각 모델의 ServingProfile 확정 + DAY/NIGHT ServingSet 확정 + 각 set의 필요 GPU capacity + NVMe→HBM 전환시간 + SLA를 만족하는 concurrency까지 확정하는 것으로 잡으면, 그 결과를 거의 그대로 위 Production 구조에 넣을 수 있습니다.
==
네. 다만 세 가지를 구분해야 합니다.
① Stage7을 한 번 돌리면 값이 자동으로 “정답”으로 나오는 것은 아니고, 후보값을 sweep해서 운영 SLA에 맞는 ServingProfile을 결정하는 과정입니다.
② 모델별 ServingProfile이 확정되면 그것들을 조합해 DAY/NIGHT Profile을 만드는 방향이 맞습니다.
③ 주↔야 전환 때문에 반드시 30~60분 서비스가 중단되는 것은 아닙니다. 설계를 잘하면 대부분의 준비를 서비스 중에 해두고, 실제 traffic 전환은 초~분 단위로 만들 수 있습니다.
Stage7의 최종 산출물을 모델별로 이런 형태로 만드는 게 좋습니다.
| 항목 | Stage7에서 결정? | 결정 방법 |
|---|---|---|
| GPU 수 | O | 모델 적재 + 성능/SLA |
| TP | O | TP sweep |
| DP | O | throughput/replica test |
| EP | O | MoE EP on/off + 조합 |
| Weight HBM | 측정값 | 실제 vLLM/DCGM |
| KV HBM | O | context/concurrency sweep |
| max context | O | 실제 요구사항 + HBM |
| concurrency | O | SLA 만족 최대값 |
| CPU | O | CPU saturation 관찰 |
| RAM | O | peak 측정 |
| NUMA | O | topology A/B |
| NVMe 필요량 | O | 실제 model artifact |
| NVMe→Ready 시간 | O | cold/warm load test |
예를 들어 GLM-5.3에 대해 단순히:
GPU=4
TP=2
EP=ON
이 나왔다고 끝이 아닙니다.
Stage7에서:
GLM-5.3
GPU
├── 2 → OOM
├── 4 → OK
└── 8 → OK, 성능 +35%
4 GPU
├── TP=4
├── TP=2 + EP
└── TP=1 + EP
↓
비교
Context
├── 8K
├── 32K
└── 64K
Concurrency
├── 1
├── 4
├── 8
├── 16
└── 32
↓
TTFT/TPOT/SLA 비교
를 거쳐 최종적으로 예를 들면:
model: glm-5.3
gpu: 4
tp: 2
dp: 1
ep: true
maxModelLen: 32768
maxConcurrency: 16
gpuMemoryUtilization: 0.90
cpu: 32
memory: 256Gi
prefixCaching: true
같은 검증된 운영 후보값이 나오는 것입니다.
따라서 Stage7은 단순 smoke test가 아니라 model characterization + capacity benchmark가 되어야 합니다.
context와 concurrency는 "모델 최대값"을 찾는 게 아니다이 부분이 중요합니다.
모델이 128K context를 지원한다고 해서 production을:
max-model-len = 128K
로 잡을 필요는 없습니다.
실제 사용자 요구가:
95% < 8K
99% < 24K
최대 32K
라면 32K로 제한하는 게 KV HBM 사용 측면에서 훨씬 유리할 수 있습니다.
Concurrency도 마찬가지입니다.
Concurrency Throughput P95 TTFT
8 800 150ms
16 1300 250ms
32 1450 900ms
64 1500 3500ms
라면 물리적으로 64가 가능하더라도 운영 profile은 16 또는 32가 될 가능성이 높습니다.
즉 Stage7의 입력으로 SLA가 필요합니다.
예를 들어:
P95 TTFT < 1 sec
P95 TPOT < 50 ms
Error < 0.1%
GPU util target 70~90%
같은 기준입니다.
ServingProfile현재 모델 목록이면 최종적으로 다음 profile들이 생깁니다.
profiles/
├── glm-5.3.yaml
├── qwen3.6-35b-a3b.yaml
├── gpt-oss-20b.yaml
│
├── deepseek-v4.1-flash.yaml
├── glm-5.3-flash.yaml
│
└── paddleocr-vl.yaml
각 파일에:
GPU
TP
DP
EP
precision
CPU
RAM
max context
concurrency
HBM weight
HBM KV
NVMe size
model load time
warmup time
expected:
TTFT
TPOT
tok/s
까지 기록하는 걸 권합니다.
이게 Stage7의 핵심 결과물입니다.
맞습니다.
단, 모델별 최적값을 단순 합산하는 것으로 끝나지는 않습니다.
예를 들어 모델 단독 테스트 결과가:
DeepSeek 4 GPU
GLM-Flash 4 GPU
라면 Day profile 후보가:
B300 Node
GPU0~3 DeepSeek
GPU4~7 GLM-Flash
가 됩니다.
그런데 반드시 다시 동시 실행 테스트를 해야 합니다.
왜냐하면:
DeepSeek 단독 최적
+
GLM 단독 최적
≠
동시 실행 최적
일 수 있기 때문입니다.
CPU, RAM, PCIe, NVMe, NIC 등의 공유 resource가 있기 때문입니다.
흐름을 이렇게 잡는 것이 정확합니다.
Stage 7A
HW/GPU topology validation
↓
Stage 7B
개별 Model Characterization
↓
Stage 7C
모델별 ServingProfile 확정
↓
Stage 7D
DAY ServingSet 조합
↓
Stage 7E
DAY 동시 부하테스트
↓
Stage 7F
NIGHT ServingSet 조합
↓
Stage 7G
NIGHT 동시 부하테스트
↓
Stage 7H
Day ↔ Night transition test
↓
Production Profile
그러면 최종적으로:
day:
models:
deepseek-v4.1-flash:
profile: profile-deepseek-v3
replicas: 1
glm-5.3-flash:
profile: profile-glm-flash-v2
replicas: 1
night:
models:
glm-5.3:
profile: profile-glm-v4
replicas: 1
qwen:
profile: profile-qwen-v2
replicas: 2
gpt-oss:
profile: profile-gptoss-v1
replicas: 1
같은 ServingSet이 나옵니다.
꼭 그렇지는 않습니다.
오히려 production에서는 30~60분 downtime이 생기지 않도록 설계하는 게 목표입니다.
여기서 세 시간을 구분해야 합니다.
① AIStor → NVMe
model prefetch
② NVMe → RAM → HBM
model loading
③ vLLM warm-up
①은 기존 서비스가 동작하는 동안 미리 할 수 있습니다.
22시에 시작하는 게 아닙니다.
20:30
│
│ 기존 DAY 서비스 정상 운영
│
├──── AIStor → NVMe
│ Night model prefetch
│
│ GLM-5.3
│ Qwen
│ gpt-oss
│
21:00
│
├──── checksum/version 검증
│
│
21:30
│
├──── 필요한 경우 Night Pod 준비
│
│
21:50
│
├──── Night model HBM load
│
├──── warm-up
│
├──── synthetic request
│
21:59
│
│ DAY service 여전히 정상
│
22:00
├──── Traffic switch
│
├──── DAY request drain
│
└──── DAY model unload
이렇게 하면 사용자 관점에서 전환은 거의:
old endpoint
↓
route switch
↓
new endpoint
뿐입니다.
Night model을 HBM에 올릴 여유 GPU가 있어야 합니다.
예를 들어 GPU 8개뿐이고 DAY가:
DeepSeek GPU0~3
GLM-Flash GPU4~7
로 8개를 전부 점유하고 있다면:
Night GLM
Qwen
gpt-oss
를 미리 HBM에 올릴 GPU가 없습니다.
이 경우 NVMe prefetch까지는 가능하지만 HBM load는 못 합니다.
21:00
GPU
████████████████
100% DAY 사용
NVMe
DAY models
████████
NIGHT models
████████ ← 여기까지 미리 준비 가능
22:00
│
DAY drain
│
GPU release
│
▼
NVMe → HBM
│
warmup
│
▼
NIGHT service
이 경우 실제 interruption window가 생깁니다.
AIStor → NVMe 다운로드를 미리 끝냈기 때문입니다.
전환 critical path에는:
NVMe → RAM/HBM
+
vLLM initialization
+
warm-up
만 남습니다.
Stage7에서 이 시간을 실제로 측정해야 합니다.
예를 들어 결과가:
AIStor → NVMe 18 min ← 서비스 중 미리 실행
NVMe → vLLM Ready 3 min
Warm-up 1 min
Health check 30 sec
라면 실제 서비스 interruption은 22분이 아니라 대략 4~5분입니다.
실제 값은 모델/파일 형식/병렬 load/CPU/NVMe 등에 따라 측정해야 합니다.
가장 좋은 방법은 Blue/Green입니다.
예를 들어 두 GPU node가 있다면:
21:30
GPU Node A
┌─────────────────────┐
│ DAY │
│ serving │
│ █████████████████ │
└─────────────────────┘
GPU Node B
┌─────────────────────┐
│ NIGHT │
│ loading / warmup │
│ █████████████████ │
└─────────────────────┘
22:00:
LiteLLM
│
route switch
A ──→ B
│
▼
NIGHT ready
그리고 A의 기존 request만 drain:
Node A
DAY
active request
↓
finish
↓
unload
합니다.
이러면 모델 loading 시간이 30분이어도 사용자 downtime은 거의 없습니다.
30분은 deployment 준비시간이지 downtime이 아닙니다.
비용 때문에 완전한 Blue/Green이 어렵다면 N+1/spare node 전략도 가능합니다.
예를 들어:
GPU Node 1 DAY production
GPU Node 2 DAY production
GPU Node 3 Transition / spare
Node 3에 먼저 Night 모델을 올립니다.
21:30
Node1 DAY
Node2 DAY
Node3 NIGHT warmup
21:55
Node3 NIGHT READY
그다음 점진적으로:
22:00
traffic 일부 → Node3
Node1 drain
↓
Night load
Node2 drain
↓
Night load
하는 rolling transition도 가능합니다.
Stage7 결과가:
DAY
DeepSeek 4 GPU
GLM Flash 4 GPU
────────────
8 GPU
라고 해서:
GPU 8장이면 production 가능.
이라고 결론 내리면 안 됩니다.
실제로는:
Steady-state capacity
+
Failure headroom
+
Transition headroom
을 봐야 합니다.
예를 들어:
8 GPU = 서비스 최소치
16 GPU = 완전 Blue/Green 가능
12 GPU = workload 조합에 따라
부분 Blue/Green/rolling 가능
같은 식으로 Stage7 결과에서 capacity planning을 해야 합니다.
기존 Stage7에서 model load time을 하나의 숫자로만 측정하지 말고 세 가지로 분리하십시오.
T1 = AIStor → Local NVMe
Cold Prefetch Time
T2 = Local NVMe → vLLM Ready
Warm Start Time
T3 = Ready → Production Ready
Warmup Time
그리고:
T_transition_ideal
≈ T_route_switch
Blue/Green이면 거의 이것만 사용자에게 보이고,
spare GPU가 없다면:
T_transition_no_spare
≈ T2 + T3 + route switch
가 실제 interruption의 대략적인 하한이 됩니다.
이 숫자가 GPU capacity를 몇 장 더 살 것인지 판단하는 매우 중요한 자료가 됩니다.
네, Stage7에서 각 모델을 sweep해서 GPU/TP/DP/EP/HBM/KV/context/concurrency의 운영 후보값을 확정하고 → 그 결과로 DAY/NIGHT ServingProfile을 구성하고 → 다시 각 ServingSet 전체를 동시 부하 테스트해서 최종 Production Profile을 확정하는 순서가 맞습니다.
그리고 주/야간 전환에 30~60분이 걸린다고 해서 그 시간 동안 서비스가 중단될 필요는 없습니다. AIStor→NVMe prefetch는 훨씬 전에 수행하고, spare GPU/node가 있으면 새 ServingSet의 HBM load와 warm-up까지 기존 서비스 중에 끝낸 뒤 traffic만 바꾸면 됩니다. 반대로 GPU를 100% 사용해서 spare capacity가 전혀 없다면 NVMe→HBM + vLLM initialization + warm-up 동안의 짧은 interruption은 피하기 어렵습니다.
따라서 Stage7의 최종 산출물은 모델별 Profile뿐 아니라 DAY profile, NIGHT profile, 그리고 0 spare / N+1 / full Blue-Green 세 경우의 전환시간과 필요한 GPU 수까지 만드는 것으로 잡는 것을 권합니다. 이걸 해두면 GPU 증설량과 실제 운영 전환 방식까지 Stage7 결과만으로 결정할 수 있습니다.
===
수동일 필요 없습니다. 여유 GPU가 0이어도 시간 기반 자동 전환은 충분히 가능합니다. 다만 GPU가 꽉 차 있으므로 Blue/Green이 아니라 Scheduled Stop → Load → Warm-up → Start 방식이 됩니다.
그리고 두 번째 질문도 가능합니다. 처음에는 LiteLLM 없이 시작하고 나중에 추가해도 Client URL을 전혀 바꾸지 않게 설계할 수 있습니다. 처음부터 client-facing URL과 실제 vLLM Service를 분리하면 됩니다.
가장 간단한 건 Kubernetes CronJob입니다.
예를 들어 22:00 야간 전환이라면 실제로는:
20:00~21:30
│
│ AIStor → NVMe
│ Night model prefetch
│
│ DAY 서비스는 계속 정상
▼
22:00
│
├─ 신규 요청 차단
│
├─ DAY request drain
│
├─ DAY Deployment scale=0
│
├─ GPU release
│
├─ NIGHT Deployment scale=1
│
├─ NVMe → HBM load
│
├─ readiness
│
└─ warm-up
│
▼
NIGHT service OPEN
반대 방향도 아침에 동일하게 수행합니다.
07:00 DAY model NVMe cache 확인/prefetch
08:00 NIGHT drain
↓
NIGHT scale=0
↓
DAY scale=1
↓
Ready
↓
warm-up
↓
Service OPEN
즉 사람이 kubectl scale 할 필요가 없습니다.
예를 들어:
CronJob
day-switch
schedule: 08:00
CronJob
night-switch
schedule: 22:00
두 개만 있어도 됩니다.
Night script의 논리는 대략:
# 1. 신규 request 차단
close_traffic day
# 2. 기존 request drain
wait_for_drain
# 3. DAY 종료
kubectl scale deploy/deepseek --replicas=0
kubectl scale deploy/glm-flash --replicas=0
# 4. GPU release 확인
wait_gpu_free
# 5. NIGHT 시작
kubectl scale deploy/glm --replicas=1
kubectl scale deploy/qwen --replicas=1
kubectl scale deploy/gpt-oss --replicas=1
# 6. readiness 대기
wait_ready
# 7. warm-up request
warmup_models
# 8. traffic 허용
open_traffic night
이 정도로 Phase 1 production을 시작해도 됩니다.
다만 실제 script에서는 반드시 timeout/failure 처리를 넣어야 합니다.
전환 Job과 model download를 분리하는 것을 추천합니다.
DAY
06:30
Day Prefetch CronJob
08:00
Day Switch CronJob
NIGHT
20:30
Night Prefetch CronJob
22:00
Night Switch CronJob
Prefetch는:
AIStor
│
│ version/hash 확인
▼
/model-cache
│
├── 있으면 → 아무것도 안 함
│
└── 없으면 → download
만 담당합니다.
그러면 실제 switch의 critical path는:
Old drain
↓
Old HBM unload
↓
New NVMe → HBM
↓
warm-up
↓
OPEN
으로 줄어듭니다.
처음에는 좋지만 점점 shell이 이렇게 됩니다.
download
↓
verify
↓
scale down
↓
wait
↓
scale up
↓
wait
↓
warmup
↓
health
↓
route
↓
failure?
↓
rollback?
특히 문제가:
22:00
DAY scale=0
↓
NIGHT scale=1
↓
NIGHT OOM
↓
?????
입니다.
그래서 규모가 커지면 Workflow로 가는 게 좋습니다.
이미 Admin cluster에서 GitOps/CI-CD 계열을 운영하고 있다면 제가 가장 선호하는 다음 단계입니다.
CronWorkflow
22:00
│
▼
NIGHT SWITCH
│
┌───────────▼──────────┐
│ Verify NVMe models │
└───────────┬──────────┘
▼
Close traffic
│
▼
Drain
│
▼
DAY scale = 0
│
▼
GPU released?
/ \
NO YES
│ │
FAIL ▼
NIGHT scale=1
│
▼
Ready?
/ \
NO YES
│ │
rollback warmup
│ │
▼ ▼
DAY start Test
│
OK?
/ \
NO YES
│ │
rollback OPEN
CronJob보다 실행 이력, retry, timeout, step별 상태, 실패 처리가 훨씬 명확합니다.
따라서 저는 단계적으로:
초기
CronJob + script
│
▼
안정화
CronWorkflow
│
▼
규모 증가/복잡화
Custom Model Lifecycle Controller
를 권합니다.
처음부터 Operator를 개발할 필요는 없습니다.
새 모델이 안 뜨면 이전 모델도 이미 내린 상태일 수 있기 때문입니다.
그래서 최소한:
NIGHT startup 실패
│
▼
Night scale=0
│
▼
DAY scale=1
│
▼
DAY Ready
│
▼
DAY OPEN
│
▼
Alert
라는 rollback을 만들어야 합니다.
즉 22:00이 됐다는 이유만으로 무조건 NIGHT로 넘어가는 것이 아니라:
schedule
+
health condition
두 조건을 사용합니다.
이건 반드시 그렇게 하는 것을 추천합니다.
Client가 절대로:
http://vllm-glm.gpu-cluster.svc:8000
같은 URL을 직접 바라보게 하지 않습니다.
처음부터:
https://ai-api.company.internal
같은 고정 endpoint를 만듭니다.
구조는 처음에는:
Client
│
│ https://ai-api.company.internal
▼
Gateway / Reverse Proxy
│
▼
vLLM
입니다.
LiteLLM이 없어도 이렇게 합니다.
현재 Cilium을 사용하고 있으니 별도 NGINX를 추가할 필요 없이:
Client
https://ai-api.company.internal
│
▼
Cilium Gateway
│
HTTPRoute
│
▼
vLLM Service
형태로 시작할 수 있습니다.
예를 들어 client 입장에서는 계속:
POST https://ai-api.company.internal/v1/chat/completions
만 사용합니다.
처음:
Client
│
│ ai-api.company.internal
▼
Cilium Gateway
│
▼
vLLM
에서 나중에:
Client
│
│ ai-api.company.internal
▼
Cilium Gateway
│
▼
LiteLLM
│
▼
vLLM
로 바꾸면 됩니다.
Client는 아무것도 바뀌지 않습니다.
변경되는 것은 Gateway의 backend뿐입니다.
Before
HTTPRoute
│
└── backendRef: vllm
After
HTTPRoute
│
└── backendRef: litellm
입니다.
Gateway API의 HTTPRoute는 backend Service로 트래픽을 전달하는 표준 방식을 제공하므로 이런 분리가 자연스럽습니다. Kubernetes Gateway API HTTP routing
URL만 같고 API가 바뀌면 client 수정이 필요하니까요.
그래서 처음부터 OpenAI-compatible API로 고정하는 것을 권합니다.
POST /v1/chat/completions
Authorization: Bearer ...
{
"model": "glm",
...
}
vLLM 자체가 OpenAI-compatible server를 제공하므로 초기 구조에 잘 맞습니다. vLLM OpenAI-compatible Server
그러면:
Phase 1
Client
↓
Cilium Gateway
↓
vLLM OpenAI API
Phase 2
Client
↓
Cilium Gateway
↓
LiteLLM OpenAI API
↓
vLLM OpenAI API
로 바꿔도 client contract가 거의 그대로 유지됩니다.
이것도 지금부터 해두는 것이 좋습니다.
Client가:
model =
"glm-5.3-flash-fp8-tp4-v3"
라고 하지 않게 합니다.
대신:
model = "glm"
또는
model = "glm-flash"
같은 logical name만 사용합니다.
그러면 내부에서는:
Client
model="glm"
│
▼
Gateway / LiteLLM
│
├─ DAY
│ └→ glm-5.3-flash
│
└─ NIGHT
└→ glm-5.3
같은 mapping도 가능합니다.
다만 현재처럼 주/야간에 제공되는 모델 자체가 다르면, 없는 모델을 호출했을 때 어떤 API 응답을 줄지도 정책으로 정해야 합니다.
예를 들어 NIGHT 전환 중에는:
Client
│
▼
Cilium Gateway
│
├── /health → 정상
│
└── /v1/... → maintenance / 503
로 만들 수 있습니다.
그리고 새 vLLM이 Ready가 되면:
Cilium Gateway
│
▼
NIGHT Service
로 route를 열면 됩니다.
Client가 Kubernetes Service가 바뀌는 것을 알 필요가 없습니다.
가장 단순한 Phase 1은 이겁니다.
Keycloak (*)
│
│
Client
│
│ ai-api.company.internal
▼
Cilium Gateway API
│
▼
vLLM Services
│
▼
B300
Model Lifecycle
06:30 DAY prefetch CronJob
08:00 DAY switch CronJob
20:30 NIGHT prefetch CronJob
22:00 NIGHT switch CronJob
│
▼
AIStor → Local NVMe → HBM
이렇게 먼저 서비스하고, 운영 안정화 후:
Client
│
│ URL 동일
▼
Cilium Gateway
│
▼
LiteLLM HA
│
▼
vLLM
로 변경합니다.
그다음 replica가 증가하면:
LiteLLM
↓
Gateway API Inference Extension
↓
vLLM replicas
를 추가하면 됩니다.
따라서 지금부터 고정해야 할 것은 LiteLLM 자체가 아니라 ai-api.<내부도메인>이라는 client endpoint + OpenAI-compatible API contract + logical model name입니다. 이 세 가지를 처음부터 고정해두면 LiteLLM, Inference Extension, MemKV 등을 나중에 추가해도 client 변경을 최소화할 수 있습니다.
===
네. LiteLLM을 처음부터 넣지 않아도 인증된 사용자만 vLLM을 사용하게 만들 수 있습니다. 현재 이미 Keycloak이 있으므로, 저는 처음부터 인증 경계를 vLLM이 아니라 Cilium Gateway 앞단에 두는 방향을 권합니다.
개념적으로:
Client
│
│ HTTPS
│ Authorization: Bearer <JWT>
▼
┌─────────────────────┐
│ Cilium Gateway │
│ + Auth 계층 │
│ │
│ JWT 검증 │
│ issuer / audience │
│ role/group 확인 │
└─────────┬───────────┘
│ 인증 성공
▼
vLLM Service
│
▼
B300
Keycloak은 토큰을 발급합니다.
User
│
▼
Keycloak
│
└── Access Token (JWT)
│
▼
Client
│
Authorization:
Bearer eyJ...
│
▼
AI API Gateway
그리고 외부에서는 vLLM Service에 직접 접근하지 못하도록 Cilium NetworkPolicy까지 걸어야 합니다.
Allowed
Client
↓
Gateway
↓
vLLM
Blocked
Client ─────X────→ vLLM
Other Pod ──X────→ vLLM
Cilium은 기본적으로 네트워크/CNI/Gateway/NetworkPolicy 영역에 두고, OIDC/JWT 인증을 담당할 아주 얇은 auth proxy를 Gateway와 vLLM 사이에 두는 게 운영상 깔끔합니다.
예를 들면:
Keycloak
│
OIDC / JWT
│
▼
Client
│
│ ai-api.company.internal
▼
Cilium Gateway
│
▼
Auth Proxy
│
│ JWT validation
│ role/group check
▼
vLLM
초기에는 이 Auth Proxy가 정말 단순해도 됩니다.
JWT가 유효한가?
이 API를 사용할 role/group인가?
두 가지만 검사합니다.
Keycloak과 연동할 수 있는 OIDC-aware proxy/gateway를 쓰거나 기존 조직의 API gateway가 있다면 그것을 재사용해도 됩니다.
vLLM OpenAI-compatible server 자체도 --api-key를 지원합니다. vLLM OpenAI-compatible Server
그래서 최초 PoC는:
Client
│
│ Authorization: Bearer <API-KEY>
▼
Cilium Gateway
│
▼
vLLM
으로 시작할 수도 있습니다.
다만 production에서는 공용 API key 하나를 여러 사용자가 공유하면:
누가 사용했나? 어려움
사용자 revoke 어려움
Team별 권한 어려움
사용량 추적 어려움
RPM/TPM quota 어려움
이 생깁니다.
따라서 Stage7/PoC에는 vLLM API key, 실제 초기 서비스부터는 Keycloak JWT 기반 인증을 권합니다.
초기 Production:
Keycloak
│
▼
Client
│
│ JWT
▼
Cilium Gateway
│
▼
Auth Proxy
│
▼
vLLM
나중에는:
Keycloak
│
▼
Client
│
│ JWT
▼
Cilium Gateway
│
▼
LiteLLM
│
▼
vLLM
으로 바꿉니다.
즉 LiteLLM이 기존의 얇은 Auth/API 계층을 대체하거나 그 뒤에 들어가는 겁니다.
Client는 계속:
https://ai-api.company.internal/v1/...
만 봅니다.
URL 변경이 없습니다.
Keycloak에 예를 들어:
Realm
│
└── ai-platform
│
├── ai-user
├── ai-power-user
├── ai-batch
└── ai-admin
같은 role/group을 두고,
JWT가:
sub = user123
groups = AI-Team-A
roles = ai-user
aud = ai-api
iss = https://keycloak.../realms/ai-platform
같은 정보를 갖게 합니다.
Auth 계층에서는 최소한:
signature ✓
expiration ✓
issuer ✓
audience ✓
role/group ✓
를 검증합니다.
단순히 JWT signature만 맞으면 통과시키는 것은 피하는 게 좋습니다.
초기에는:
Keycloak
"이 사람은 AI 서비스를
사용할 수 있는가?"
│
▼
Auth Proxy
정도만 합니다.
LiteLLM을 넣으면:
Keycloak
│
│ identity
▼
LiteLLM
User A
├─ DeepSeek O
├─ GLM O
├─ Qwen X
├─ RPM 30
├─ TPM 500K
└─ quota 10M/day
처럼 AI-specific authorization/usage policy까지 발전시킬 수 있습니다.
그래서 구조적으로는:
처음
────────────────────────────────
Keycloak
│
▼
Cilium Gateway
│
▼
Auth Proxy
│
▼
vLLM
LiteLLM 도입 후
────────────────────────────────
Keycloak
│
▼
Cilium Gateway
│
▼
LiteLLM
│
├── User / Team
├── Model ACL
├── RPM / TPM
├── Quota
└── Usage
│
▼
vLLM
를 추천합니다.
따라서 LiteLLM을 나중에 넣는 전략 때문에 초기 서비스의 인증을 포기할 필요는 전혀 없습니다. 오히려 처음부터 Keycloak + 고정 URL + JWT + OpenAI-compatible API + logical model name까지 contract로 고정해두고, 초기에는 얇은 Auth Proxy를 사용하다가 사용량/팀별 quota가 필요해지는 시점에 LiteLLM을 넣는 것이 가장 자연스럽습니다.
===
네. 초기 사용자가 3~4명이라면 그 순서가 오히려 현실적입니다. 다만 “기존 vLLM API key를 LiteLLM에서도 그대로 유지”하는 것보다는 LiteLLM 도입 시 키를 한 번 교체하는 것을 권합니다.
Phase 1 — 초기 3~4명
────────────────────────
Client
│
│ Bearer <vLLM API KEY>
▼
ai-api.company.internal
│
Cilium Gateway
│
▼
vLLM
이 단계에서는 사용자별로 vLLM 인스턴스를 나누지 않는 이상 vLLM의 --api-key는 기본적으로 서비스 접근을 보호하는 shared secret 성격으로 보는 게 좋습니다. 사용자별 quota/accounting을 위한 본격적인 IAM 수단은 아닙니다. vLLM OpenAI-compatible Server
그래서 3~4명이라면 일단 충분합니다.
URL:
https://ai-api.company.internal
API:
OpenAI compatible
Authorization:
Bearer <initial-vllm-key>
여기서 URL과 API contract를 처음부터 고정하는 게 핵심입니다.
나중에 구조만 이렇게 바꿉니다.
Before
Client
│
│ vLLM key
▼
Cilium Gateway
│
▼
vLLM
After
Client
│
│ LiteLLM key
▼
Cilium Gateway
│
▼
LiteLLM
│
│ backend credential
▼
vLLM
Client URL은 여전히:
https://ai-api.company.internal/v1/...
입니다.
바뀌는 건 Bearer token 하나뿐입니다.
기술적으로 억지로 호환 계층을 만들 수는 있겠지만 권하지 않습니다.
보안 경계를 분리하는 게 좋습니다.
Client
│
│ LiteLLM Virtual Key
│ sk-user-a-...
▼
LiteLLM
│
│ 별도 Backend Key
│ internal-vllm-...
▼
vLLM
즉:
External credential
────────────────────
User → LiteLLM
Internal credential
────────────────────
LiteLLM → vLLM
으로 분리합니다.
그러면 vLLM key가 사용자에게 더 이상 노출되지 않습니다.
초기에는 3~4명이:
User A ─┐
User B ─┼─> shared-vllm-key
User C ─┤
User D ─┘
를 사용해도 되지만 LiteLLM을 넣을 때:
User A → key-A
User B → key-B
User C → key-C
User D → key-D
│
▼
LiteLLM
로 바꾸는 게 좋습니다.
그러면 바로:
User A
├─ usage
├─ RPM
├─ TPM
└─ model ACL
User B
├─ usage
├─ RPM
├─ TPM
└─ model ACL
같은 관리가 가능해집니다.
LiteLLM의 virtual key/team 기능은 이런 사용자/팀 단위 limit와 model access를 지원합니다. LiteLLM Virtual Keys / Users
그 이후 Keycloak을 붙이는 것도 문제 없습니다.
이때 인증 방법을 두 가지로 가져갈 수 있습니다.
방법 A
Application / Script
│
│ LiteLLM API Key
▼
LiteLLM
→ 프로그램/서비스 계정에 편함
그리고:
방법 B
Human User
│
▼
Keycloak
│
│ JWT/OIDC
▼
LiteLLM
입니다.
실제 운영에서는 둘을 병행하는 게 오히려 자연스럽습니다.
최종적으로는:
AI API
│
┌────────────┴────────────┐
│ │
Human User Application
│ │
▼ │
Keycloak │
│ │
JWT API Key
│ │
└────────────┬────────────┘
▼
LiteLLM
│
┌───────┼───────┐
│ │ │
DeepSeek GLM Qwen
│ │ │
└───────┼───────┘
▼
vLLM
예를 들어 Compute cluster에서 자동화된 RAG application이 호출한다면 매번 사람의 Keycloak 로그인 과정을 거치는 것보다 service identity/API credential을 사용하는 편이 자연스럽습니다.
반면 웹 UI에서 사람이 직접 AI를 사용한다면 Keycloak SSO가 좋습니다.
LiteLLM 도입 이후에는 NetworkPolicy까지:
Users
│
▼
Cilium Gateway
│
▼
LiteLLM
│
│ allowed
▼
vLLM
User ─────────X────────> vLLM
Compute Pod ──X────────> vLLM
으로 만드는 것을 권합니다.
즉 vLLM key는 더 이상 user credential이 아니라:
LiteLLM → vLLM backend credential
이 됩니다.
Cilium NetworkPolicy로 vLLM ingress source를 LiteLLM namespace/identity로만 제한하면 API key가 유출되어도 직접 접근할 network path 자체를 막을 수 있습니다.
| 단계 | 사용자 인증 | Client URL | Backend |
|---|---|---|---|
| 초기 PoC | vLLM shared API key | ai-api.company.internal | vLLM |
| 초기 운영 | vLLM API key | 동일 | vLLM |
| LiteLLM 도입 | 사용자별 LiteLLM key | 동일 | LiteLLM → vLLM |
| Keycloak 도입 | JWT/OIDC + API key 병행 | 동일 | LiteLLM → vLLM |
따라서 지금 3~4명 때문에 LiteLLM+Keycloak까지 한꺼번에 구축할 필요는 없습니다. 처음에는 Cilium Gateway → vLLM + API key로 시작하되, ① client-facing DNS/URL을 vLLM 이름과 분리하고, ② OpenAI-compatible API를 유지하고, ③ client에게 Kubernetes Service 주소를 절대 직접 주지 않는 것만 지키면 됩니다.
그러면 나중에 LiteLLM을 넣을 때 사용자가 해야 할 변경은 사실상 API key를 한 번 교체하는 것뿐이고, 이후 Keycloak을 추가해도 backend vLLM 구성에는 거의 영향을 주지 않습니다.