26O07e

QK·5일 전

네. 이 경우에는 처음부터 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

1. 전체 테스트를 이렇게 나누겠습니다

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 자체를 먼저 검증하는 편이 원인 분석이 쉽습니다.


2. Stage 1 — 모델별 단독 테스트

우선 모델 하나만 B300 node에 올립니다.

예를 들어 Qwen 계열 MoE라면 다음과 같은 matrix를 만들겠습니다.

TestGPUTPDPEP목적
Q1111OFF1 GPU 수용 가능 여부
Q2221OFFTP baseline
Q3212ONEP 효과
Q4414ONEP scaling
Q5422ONTP+EP 조합
Q6818ONfull-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를 작성해야 합니다.


3. 각 모델에서 찾고 싶은 최종 결과

모델마다 결국 아래 한 줄을 만드는 것이 목표입니다.

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을 만드는 것은 상대적으로 쉽습니다.


4. 테스트 지표는 tokens/sec만 보면 안 됩니다

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

vLLM Benchmark CLI 문서


5. Context length를 반드시 여러 개 테스트

이 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입니다.


6. 중요한 것은 Saturation point를 찾는 것

예를 들어 결과가:

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로 잡는 식입니다.

이걸 각 모델마다 찾아야 합니다.


7. MoE 모델은 EP ON/OFF를 반드시 비교

현재 목록은 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

순서가 좋습니다.


8. Stage 2 — 실제 “주간 세트”를 동시에 올려보기

개별 모델 최적값을 찾았으면 그다음부터가 진짜 중요한 테스트입니다.

첨부하신 주간 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 결과를 보기 전에는 어느 쪽이 맞다고 고정하지 않는 게 좋습니다.


9. OCR/Docling → LLM까지 실제 pipeline test도 필요

각 모델을 독립적으로 때리는 것만으로는 부족합니다.

실제 주간 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

등을 찾을 수 있습니다.


10. Stage 3 — AIStor/local NVMe까지 포함

그다음에는 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부터 준비하는 게 충분한지 판단할 수 있습니다.


11. Stage 4 — 실제 Day → Night 전환

야간에는 첨부하신 세 모델을 실제로 올립니다.

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

로 남깁니다.


12. 이 단계에서야 LiteLLM을 붙이는 것이 좋음

여기까지 통과하면:

               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가 의도대로 작동하는지를 별도로 측정하면 됩니다.


13. MemKV도 마지막 쪽에서 A/B test

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의 효과를 제대로 측정하지 못할 수 있습니다.


제가 추천하는 실제 테스트 순서

  1. GPU/node validation — nvidia-smi, topology, NVLink/NVSwitch, NCCL, HBM, CPU NUMA부터 검증합니다.
  2. 모델 하나씩 vLLM에 올리기 — 일단 “실행된다”를 확인합니다.
  3. GPU/TP/DP/EP matrix — 각 모델 최적 parallelism을 찾습니다.
  4. Context × concurrency sweep — 최대 처리량이 아니라 SLA를 만족하는 saturation point를 찾습니다.
  5. 주간 모델 세트 동시 실행 — 실제 GPU capacity와 interference를 측정합니다.
  6. 야간 모델 세트 동시 실행 — 같은 방식으로 측정합니다.
  7. AIStor → NVMe → HBM — cold/warm startup 시간을 측정합니다.
  8. Day ↔ Night 전환 — prefetch/warm-up/drain까지 포함해 검증합니다.
  9. LiteLLM + 사용자 quota — 실제 multi-user traffic을 재현합니다.
  10. MemKV A/B — KV reuse가 높은 workload에서만 효과를 검증합니다.

여기서 가장 먼저 만들 산출물은 모델별 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의 책임을 섞지 않는 것입니다.

1. 모델마다 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의 근거가 됩니다.


2. 모델별 Deployment를 완전히 분리

하나의 거대한 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

를 독립적으로 설정할 수 있습니다.

장애 격리도 훨씬 쉽습니다.


3. 사용자에게는 실제 모델 Deployment를 노출하지 않음

외부에서는:

/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가 영향을 받지 않습니다.


4. 주간/야간은 두 개의 "Serving Set"으로 정의

이 환경에서는 개별 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을 둡니다.


5. 실제 전환은 단순 Cron scale-up/down으로 하지 않음

여기가 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을 넘기는 목표 시간입니다.

이 차이가 중요합니다.


6. 이를 담당하는 Model Lifecycle 계층이 필요

초기에는 별도의 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로 승격하면 됩니다.


7. Local NVMe는 계속 유지

서비스 환경에서도 모델을 매번 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를 받으면 됩니다.


8. GPU node별 cache locality도 고려

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 상태를 확인해 갱신하도록 발전시키는 편이 안전합니다.


9. LiteLLM은 항상 켜져 있는 Front Door

주/야간에 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을 분리합니다.


10. LiteLLM은 HA로

Production이면 LiteLLM도 하나만 두지 않습니다.

                 Load Balancer
                       │
              ┌────────┴────────┐
              ▼                 ▼
          LiteLLM #1        LiteLLM #2
              │                 │
              └────────┬────────┘
                       ▼
                  PostgreSQL

LiteLLM Pod가 죽는 것이 전체 AI 서비스 장애가 되지 않도록 합니다.

그리고 GPU resource는 필요 없습니다.


11. 동일 모델 replica가 늘면 Inference Extension 추가

처음에:

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

구조로 발전시키면 됩니다.

즉 처음부터 반드시 넣을 구성요소는 아닙니다.


12. MemKV도 production 기본값으로 무조건 넣지 않음

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에는 꺼질 수도 있습니다.

그게 정상입니다.


13. PaddleOCR/Docling은 LLM ServingPool과 분리

첨부한 목록에서 이 부분은 조금 다르게 가져가는 것이 좋습니다.

                  Document API
                       │
                       ▼
              Document Pipeline
                       │
       ┌───────────────┼───────────────┐
       ▼               ▼               ▼
  PaddleOCR-VL      Docling       FigureClassifier
       │               │               │
       └───────────────┼───────────────┘
                       ▼
               Extracted Context
                       │
                       ▼
                    LiteLLM
                       │
                       ▼
                DeepSeek / GLM

즉 Docling 계열까지 전부 vLLM에 억지로 넣는 구조는 피하겠습니다.

LLM/VLM을 vLLM ServingPool, document preprocessing/classification은 별도 application service로 관리하는 게 좋습니다.


14. 장애 시에는 현재 모델을 유지하는 게 원칙

예를 들어 21:50에 Night GLM이 안 뜹니다.

잘못된 자동화는:

22:00 됨
   ↓
Day 모델 종료
   ↓
Night 모델 실패
   ↓
서비스 장애

입니다.

Production에서는:

Night deployment
      │
      ▼
Ready?
 /       \
NO        YES
│          │
│       traffic switch
│
Day model 유지
+
alert

가 되어야 합니다.

시간보다 readiness가 우선입니다.


15. GPU가 부족해서 Day와 Night를 동시에 올릴 수 없는 경우

실제로 이게 가장 까다로운 부분일 수 있습니다.

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를 만족시키는가?

를 봐야 합니다.


16. 가능하면 N+1 capacity를 권장

Production에서는 GPU를 항상 100% 꽉 채우는 설계보다:

steady state
70~85% capacity

15~30%
headroom

을 어느 정도 확보하는 편이 좋습니다.

이 headroom은:

traffic burst
model rollout
Day/Night transition
GPU failure
warm-up
rescheduling

에 사용됩니다.

특히 지금처럼 모델 세트 자체가 하루 두 번 바뀌는 환경에서는 가치가 큽니다.


17. Production에서 최종적으로 관리할 객체

저라면 운영 모델을 네 가지 논리 객체로 정리합니다.

① 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 최적화 값, 시간대별 운영정책, 사용자 정책이 서로 뒤엉키지 않습니다.


제가 이 환경에서 잡을 최종 Production Baseline

                    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 전환은 초~분 단위로 만들 수 있습니다.

1. Stage7에서 무엇을 확정해야 하나

Stage7의 최종 산출물을 모델별로 이런 형태로 만드는 게 좋습니다.

항목Stage7에서 결정?결정 방법
GPU 수O모델 적재 + 성능/SLA
TPOTP sweep
DPOthroughput/replica test
EPOMoE EP on/off + 조합
Weight HBM측정값실제 vLLM/DCGM
KV HBMOcontext/concurrency sweep
max contextO실제 요구사항 + HBM
concurrencyOSLA 만족 최대값
CPUOCPU saturation 관찰
RAMOpeak 측정
NUMAOtopology A/B
NVMe 필요량O실제 model artifact
NVMe→Ready 시간Ocold/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가 되어야 합니다.


2. 특히 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%

같은 기준입니다.


3. 그래서 Stage7 결과는 모델별 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의 핵심 결과물입니다.


4. 그다음 말씀하신 대로 DAY/NIGHT Profile을 만든다

맞습니다.

단, 모델별 최적값을 단순 합산하는 것으로 끝나지는 않습니다.

예를 들어 모델 단독 테스트 결과가:

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가 있기 때문입니다.


5. 그래서 Stage7 후반부가 DAY/NIGHT Profile validation

흐름을 이렇게 잡는 것이 정확합니다.

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이 나옵니다.


6. 그리고 가장 중요한 질문: 전환 때 30~60분 서비스 중단?

꼭 그렇지는 않습니다.

오히려 production에서는 30~60분 downtime이 생기지 않도록 설계하는 게 목표입니다.

여기서 세 시간을 구분해야 합니다.

① AIStor → NVMe
   model prefetch

② NVMe → RAM → HBM
   model loading

③ vLLM warm-up

①은 기존 서비스가 동작하는 동안 미리 할 수 있습니다.


7. 예를 들어 22:00 Night 전환이라면

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

뿐입니다.


8. 그런데 여기에는 중요한 전제조건이 있다

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가 생깁니다.


9. 하지만 그것도 30~60분이어야 한다는 뜻은 아니다

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 등에 따라 측정해야 합니다.


10. Zero/near-zero downtime을 원하면 spare capacity가 필요

가장 좋은 방법은 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이 아닙니다.


11. 모든 GPU를 두 배로 준비할 필요도 없다

비용 때문에 완전한 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도 가능합니다.


12. 그래서 production capacity 계산에는 “전환 capacity”도 넣어야 한다

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을 해야 합니다.


13. 그래서 Stage7에 꼭 추가하고 싶은 테스트

기존 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를 분리하면 됩니다.

1. 가장 단순한 자동 전환

가장 간단한 건 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 할 필요가 없습니다.


2. 가장 단순한 구현은 CronJob + shell script

예를 들어:

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 처리를 넣어야 합니다.


3. Prefetch CronJob은 별도로 두는 게 좋음

전환 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

으로 줄어듭니다.


4. 단, CronJob 하나에 모든 걸 넣는 것의 단점

처음에는 좋지만 점점 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로 가는 게 좋습니다.


5. 다음 단계는 Argo Workflows/CronWorkflow

이미 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를 개발할 필요는 없습니다.


6. 여유 GPU가 없을 때 장애 대응은 특별히 중요

새 모델이 안 뜨면 이전 모델도 이미 내린 상태일 수 있기 때문입니다.

그래서 최소한:

NIGHT startup 실패
       │
       ▼
Night scale=0
       │
       ▼
DAY scale=1
       │
       ▼
DAY Ready
       │
       ▼
DAY OPEN
       │
       ▼
Alert

라는 rollback을 만들어야 합니다.

즉 22:00이 됐다는 이유만으로 무조건 NIGHT로 넘어가는 것이 아니라:

schedule
   +
health condition

두 조건을 사용합니다.


7. Client URL은 처음부터 고정시키자

이건 반드시 그렇게 하는 것을 추천합니다.

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이 없어도 이렇게 합니다.


8. 초기에는 Cilium Gateway API로 충분

현재 Cilium을 사용하고 있으니 별도 NGINX를 추가할 필요 없이:

                Client

https://ai-api.company.internal
                   │
                   ▼
             Cilium Gateway
                   │
                HTTPRoute
                   │
                   ▼
             vLLM Service

형태로 시작할 수 있습니다.

예를 들어 client 입장에서는 계속:

POST https://ai-api.company.internal/v1/chat/completions

만 사용합니다.


9. 나중에 LiteLLM을 중간에 삽입

처음:

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


10. 다만 API contract도 처음부터 LiteLLM을 고려해야 함

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가 거의 그대로 유지됩니다.


11. 모델 이름도 처음부터 logical name으로

이것도 지금부터 해두는 것이 좋습니다.

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 응답을 줄지도 정책으로 정해야 합니다.


12. 오히려 주/야간 전환에도 고정 Gateway가 매우 유용

예를 들어 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만으로 Keycloak JWT 인증까지 해결하려고 하지는 않겠습니다

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 API key도 가능

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 기반 인증을 권합니다.


그리고 이 방식이면 LiteLLM 추가할 때도 아주 깔끔합니다

초기 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 변경이 없습니다.


처음부터 Token/Role도 이렇게 잡아두면 좋습니다

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만 맞으면 통과시키는 것은 피하는 게 좋습니다.


LiteLLM을 추가하면 여기서 한 단계 발전

초기에는:

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를 처음부터 고정하는 게 핵심입니다.


Phase 2 — LiteLLM 도입

나중에 구조만 이렇게 바꿉니다.

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 하나뿐입니다.

기존 vLLM key를 그대로 LiteLLM key로 쓰는 건?

기술적으로 억지로 호환 계층을 만들 수는 있겠지만 권하지 않습니다.

보안 경계를 분리하는 게 좋습니다.

Client
  │
  │ LiteLLM Virtual Key
  │ sk-user-a-...
  ▼
LiteLLM
  │
  │ 별도 Backend Key
  │ internal-vllm-...
  ▼
vLLM

즉:

External credential
────────────────────
User → LiteLLM

Internal credential
────────────────────
LiteLLM → vLLM

으로 분리합니다.

그러면 vLLM key가 사용자에게 더 이상 노출되지 않습니다.


오히려 LiteLLM 전환 때 사용자별 키로 바꾸기

초기에는 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


Phase 3 — Keycloak 연동

그 이후 Keycloak을 붙이는 것도 문제 없습니다.

이때 인증 방법을 두 가지로 가져갈 수 있습니다.

방법 A

Application / Script
      │
      │ LiteLLM API Key
      ▼
   LiteLLM

→ 프로그램/서비스 계정에 편함

그리고:

방법 B

Human User
    │
    ▼
Keycloak
    │
    │ JWT/OIDC
    ▼
LiteLLM

입니다.

실제 운영에서는 둘을 병행하는 게 오히려 자연스럽습니다.


사람과 Application 인증을 분리하는 것을 추천

최종적으로는:

                   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가 좋습니다.


최종적으로는 vLLM API key를 외부에서 완전히 숨김

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 URLBackend
초기 PoCvLLM shared API keyai-api.company.internalvLLM
초기 운영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 구성에는 거의 영향을 주지 않습니다.

profile
engineer

0개의 댓글