NVIDIA B300(Blackwell Ultra, 단일 GPU당 HBM3e 288GB 탑재) 8장으로 구성된 단일 노드는 총 VRAM만 약 2.3TB에 달하며, 5세대 NVLink(양방향 1.8TB/s per GPU)로 풀메시(Full-mesh) 연결되어 있습니다.
단일 노드 내에서 여러 모델(예: DeepSeek 계열, Qwen3-32B, 경량 임베딩/리랭커 등)을 동시에 서빙할 때 NVLink 토폴로지 유지, VRAM/KV Cache 고립, 엔진 서빙 아키텍처 및 메트릭 관측성 관점에서의 베스트 프랙티스는 다음과 같습니다.
B300 8-way 노드는 NVLink Switch를 통해 8장이 단일 패브릭으로 묶여 있지만, 여러 모델을 동시에 쪼개어 서빙할 때는 NUMA 노드와 PCIe/NVLink 도메인에 맞춰 2의 거듭제곱 단위(TP=1, 2, 4, 8)로 격리해야 All-Reduce 병목이 발생하지 않습니다.
8장 노드는 통상 듀얼 소켓 CPU(NUMA 0, NUMA 1) 구조입니다. 모델 서빙 프로세스가 GPU뿐만 아니라 대응하는 NUMA 노드의 CPU 코어/메모리에 정확히 고정되어야 인터커넥트 병목이 없습니다.
# 예: GPU 0~3번은 NUMA node 0번에 고정하여 vLLM 인스턴스 기동
numactl --cpunodebind=0 --membind=0 vllm serve /models/Qwen-70B \
--tensor-parallel-size 4 \
--gpu-memory-utilization 0.90 \
--port 8001
여러 추론 프로세스가 동일 물리 노드에 공존할 때 OOM(Out of Memory) 연쇄 장애를 막는 설정입니다.
CUDA_VISIBLE_DEVICES 하드웨어 격리:CUDA_VISIBLE_DEVICES=0,1,2,3CUDA_VISIBLE_DEVICES=4,5--gpu-memory-utilization 고정 및 KV Cache 버퍼 제어:0.85 ~ 0.90 수준으로 설정해 메모리 스파이크를 방지합니다.TP=1, 2, 4)로 물리 카드를 분배하는 것이 좋습니다.노드 1대 내부에서 모델별로 각기 다른 포트(8001, 8002, 8003...)로 엔진이 뜨게 되므로, 앞단에 경량 AI 게이트웨이를 배치해야 클라이언트 관리가 단순해집니다.
model 필드에 따라 로컬 포트로 라우팅:"model": "qwen3-32b" localhost:8001"model": "deepseek-r1-distill-70b" localhost:8002"model": "bge-reranker" localhost:8003--enable-prefix-caching을 켜서 중복 토큰 연산과 KV Cache 소비를 40~60% 절감합니다.단일 노드 멀티 모델 환경에서는 "어느 모델이 어느 GPU 대역폭을 잠식하고 있는지"를 즉각 분리해 관측해야 합니다.
[B300 GPUs] ──> DCGM Exporter (포트 9400) ──┐
[vLLM #1] ──> Prometheus 메트릭 (8001) ──┼──> Prometheus Server ──> Grafana Dashboard
[vLLM #2] ──> Prometheus 메트릭 (8002) ──┘
DCGM_FI_DEV_GPU_UTIL: GPU 연산 코어 가동률DCGM_FI_DEV_MEM_COPY_UTIL: HBM3e 메모리 대역폭 포화 여부DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL: GPU 간 All-Reduce 교환량 (TP 분할 모델 정상 동작 검증)DCGM_FI_DEV_POWER_USAGE / DCGM_FI_DEV_GPU_TEMP: B300 노드 고전력/열 부하 감시각 vLLM 인스턴스는 자체 포트(예: :8001/metrics, :8002/metrics)로 상세 메트릭을 노출합니다.
vllm:gpu_cache_usage_factor: 가장 중요. 0.85 이상 지속 시 새 요청이 큐에 쌓이며 TTFT(Time to First Token) 지연 급증.vllm:num_requests_waiting: 요청이 처리되지 못하고 큐에 머무는 수. 0보다 크면 동시 요청 처리 한계 도달을 의미.vllm:num_requests_running: 현재 PagedAttention으로 동시 디코딩 중인 배치 크기.vllm:time_to_first_token_seconds: 초기 Prefill 지연 시간.vllm:time_per_output_token_seconds: 디코딩 토큰 생성 간격 (사용자 체감 속도).| 점검 영역 | 권장 설정값 / 액션 | 목적 |
|---|---|---|
| NUMA 바인딩 | numactl --cpunodebind=X --membind=X | 소켓 간 PCIe 크로스오버 통신 억제 |
| GPU 격리 | 컨테이너별 CUDA_VISIBLE_DEVICES 명시 | 프로세스 간 VRAM 침범 및 충돌 방지 |
| KV Cache | --gpu-memory-utilization 0.85~0.90 | PagedAttention 할당 버퍼 확보로 OOM 방지 |
| 멀티 모델 분할 | TP 크기는 2의 제곱수(1, 2, 4, 8) 유지 | NVLink 전송 효율 최적화 및 비대칭 링 방지 |
| 로그 수집 | vLLM Engine 에러 로그와 DCGM XID 수집 | 하드웨어 ECC 에러 및 엔진 Crash 빠른 격리 |
==
네. B300 8장짜리 단일 GPU 노드에서 여러 LLM을 동시에 서비스하려면, 단순히 모델마다 GPU를 1~2장씩 고정 배정하는 것보다 GPU partitioning + inference scheduler + QoS + observability를 함께 설계하는 게 좋습니다.
특히 지금 말씀하신 환경처럼 B300 8-GPU 서버에서 여러 모델의 inference benchmark → 실제 서비스로 가져가려는 경우라면, 저는 아래 구조를 권합니다.
Client
│
┌──────▼──────┐
│ API Gateway │
│ / Router │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
Model A / 70B Model B / 8B
BF16/FP8 FP8/FP4
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Triton / │ │ Triton / │
│ vLLM / TRT │ │ vLLM / TRT │
└──────┬──────┘ └──────┬──────┘
│ │
GPU 0-3 GPU 4
GPU 4-7 GPU 5
GPU 6/7 ...
핵심은 "모델 → GPU"를 정적으로만 매핑하지 말고, 모델별 QoS를 정의한 후 그에 맞춰 GPU를 배치하는 것입니다.
B300 8장이라면 처음부터 모든 GPU를 하나의 거대한 pool로 보고 스케줄링하기보다는 다음처럼 GPU pool을 만드는 것을 추천합니다.
예를 들어:
| Pool | GPU | 용도 |
|---|---|---|
| Pool-A | 0-3 | 70B급 모델 |
| Pool-B | 4 | 8B/14B |
| Pool-C | 5 | 8B/14B |
| Pool-D | 6-7 | 실험/Batch/대형 모델 |
또는 모델이 많다면:
GPU 0-3 : Large Model Pool
GPU 4 : Small Model A
GPU 5 : Small Model B
GPU 6 : Small Model C
GPU 7 : Experimental / overflow
이렇게 하면 작은 모델 하나 때문에 4~8 GPU짜리 모델의 inference가 영향을 받는 상황을 줄일 수 있습니다.
예를 들어:
TP=4
GPU 0-3
TP=1
GPU 4
TP=2
GPU 5-6
이런 식입니다.
특히 B300에서는 TP=1/2/4/8을 모두 측정한 후 결정하는 게 중요합니다.
현재 진행하시는 benchmark matrix:
TP = 1 / 2 / 4 / 8
Precision = BF16 / FP8 / FP4
Context = ...
Concurrency = ...
결과를 이용해서 각 모델의 "최적 GPU 수"를 결정하면 됩니다.
예:
Model TP GPU Max Throughput
------------------------------------------------
Llama-8B 1 1 1,200 tok/s
Qwen-32B 2 2 700 tok/s
Llama-70B 4 4 520 tok/s
DeepSeek-R1-70B 4 4 480 tok/s
GPT-OSS-120B 4 4 430 tok/s
그러면 8 GPU에서 어떤 모델을 동시에 돌릴지 계산할 수 있습니다.
LLM 여러 개를 동시에 돌릴 때 흔히:
GPU utilization 70%
만 보고 판단하는데, 이것만 보면 상당히 위험합니다.
반드시 다음을 같이 봐야 합니다.
GPU memory used
GPU memory free
KV cache usage
KV cache hit rate
KV cache eviction
SM utilization
Tensor Core utilization
HBM bandwidth
PCIe/NVLink/NVSwitch traffic
특히 inference에서는:
Weights
+
KV Cache
+
CUDA Graph
+
Runtime workspace
+
Activation
이 전부 GPU memory를 사용합니다.
그래서 모델별로 memory budget을 명시적으로 설정하는 게 좋습니다.
예:
GPU 0-3
Model A
weights 280 GB
KV cache 80 GB
workspace 20 GB
reserve 20 GB
total 400 GB
그리고 절대로 100%까지 채우지 않습니다.
실서비스라면 대략:
Target:
GPU memory 70~80%
Warning:
80~85%
Critical:
>90%
정도로 운영하는 것이 안전합니다.
정확한 threshold는 B300의 실제 HBM 용량과 사용하는 inference engine에 맞춰 잡아야 합니다.
이 부분이 상당히 중요합니다.
예를 들어:
GPU 4
Model B 8B
Model C 7B
Model D embedding
처럼 할 수 있습니다.
하지만 이것을 무작정 하면 안 됩니다.
특히:
Model B → high concurrency
Model C → high concurrency
Model D → batch
가 동시에 들어오면 서로 GPU를 잡아먹습니다.
그래서 GPU sharing은 workload class까지 같이 나눠야 합니다.
추천:
GPU 4
┌────────────────────────────┐
│ Model B │
│ latency-sensitive │
│ priority = 100 │
├────────────────────────────┤
│ Model C │
│ normal │
│ priority = 50 │
├────────────────────────────┤
│ Embedding │
│ batch │
│ priority = 10 │
└────────────────────────────┘
B300에서 GPU partitioning을 생각하면 MIG가 먼저 떠오를 수 있습니다.
하지만 LLM inference에서는 무조건 MIG가 좋은 것은 아닙니다.
특히:
70B
TP=4
NVLink/NVSwitch
KV cache
large batch
같은 workload에서는 GPU 전체를 사용하는 편이 훨씬 유리할 수 있습니다.
따라서 저는:
Full GPU
Full GPU
+ multiple inference processes
MIG
순서로 검토하겠습니다.
지금 환경이 Kubespray + NVIDIA GPU Operator이므로 Kubernetes로 운용한다면 특히 중요합니다.
기본적으로:
Pod
└── GPU resource request
를 사용합니다.
예:
resources:
limits:
nvidia.com/gpu: 4
그러면 Kubernetes가 GPU 4장을 할당합니다.
하지만 여러 모델을 한 GPU에 공유하려면 별도의 전략이 필요합니다.
그래서 모델별 GPU allocation policy를 먼저 정하는 게 좋습니다.
예:
large-model
gpu: 4
priority: high
medium-model
gpu: 2
priority: normal
small-model
gpu: 1
priority: normal
batch
gpu: 1
priority: low
현재 B300 benchmark를 하고 있으니 저는 실제 서비스 구조에서는 Triton을 상당히 유력하게 봅니다.
구조는:
API Gateway
│
▼
Model Router
│
┌────────────┼────────────┐
▼ ▼ ▼
Model A Model B Model C
Triton Triton Triton
│ │ │
GPU0-3 GPU4 GPU5
Triton의 장점은:
등입니다.
다만 LLM 자체의 scheduler/KV cache 최적화는 vLLM/TensorRT-LLM 등 engine의 특성이 더 중요하기 때문에,
Triton
+
TensorRT-LLM
또는
Triton
+
vLLM backend
같은 형태를 모델에 따라 비교하는 것이 좋습니다.
예를 들어:
Class A
Interactive
SLA 중요
latency 우선
Class B
General inference
throughput 우선
Class C
Batch
비용/throughput 우선
그리고 GPU도:
A → GPU 0-3
B → GPU 4-6
C → GPU 7
처럼 배치합니다.
이게 실제 운영에서 상당히 중요합니다.
제가 추천하는 dashboard는 최소한 다음 구조입니다.
GPU utilization
HBM utilization
HBM bandwidth
Tensor Core utilization
Power
Temperature
PCIe throughput
NVLink/NVSwitch throughput
ECC/XID
Requests/sec
Concurrency
TTFT
ITL
E2E latency
Input tokens/sec
Output tokens/sec
Total tokens/sec
Queue time
Batch size
KV cache utilization
KV cache hit ratio
OOM
Request rejection
특히 TTFT와 ITL을 반드시 따로 봐야 합니다.
TTFT
= Time To First Token
ITL
= Inter Token Latency
예를 들어:
TTFT = 2.1 sec
ITL = 20 ms
이면 prompt processing 쪽이 문제일 수 있고,
TTFT = 100 ms
ITL = 200 ms
이면 generation/concurrency 문제가 더 클 수 있습니다.
현재 Prometheus/Grafana를 사용하고 있으니:
model="llama-70b"
model="qwen-32b"
model="llama-8b"
같은 label을 반드시 넣는 것을 추천합니다.
예:
llm_requests_total{
model="llama-70b",
precision="fp8",
tp="4"
}
그리고:
llm_ttft_seconds
llm_itl_seconds
llm_input_tokens_total
llm_output_tokens_total
llm_kv_cache_usage_ratio
llm_queue_depth
llm_active_requests
등을 모델별로 수집합니다.
예를 들어:
Model A
GPU utilization = 95%
throughput = 300 tok/s
과
Model A
GPU utilization = 75%
throughput = 420 tok/s
라면 후자가 훨씬 좋은 configuration일 수 있습니다.
따라서 모델별로:
tokens/sec/GPU
를 만들어야 합니다.
예:
| Model | GPU | Throughput | tok/s/GPU |
|---|---|---|---|
| 8B | 1 | 1,000 | 1,000 |
| 32B | 2 | 700 | 350 |
| 70B | 4 | 520 | 130 |
| 120B | 4 | 430 | 108 |
이 지표가 GPU allocation 의사결정의 핵심이 됩니다.
모델마다 무한정 concurrency를 올리면 안 됩니다.
예:
Llama-70B
Concurrency
1 100 tok/s
2 190
4 350
8 500
16 520
32 515
64 490
그러면:
Operating point = concurrency 8~16
정도로 잡습니다.
그 이후는 throughput도 증가하지 않고 latency만 증가하기 때문입니다.
이것을 모델별로 자동화하면 좋습니다.
예를 들어:
GPU 0-3
70B model
현재:
active requests = 14
KV cache = 82%
queue = 20
인데 새로운 request를 계속 받으면:
latency ↑
KV cache ↑
OOM
으로 갈 수 있습니다.
그래서:
queue threshold
KV cache threshold
concurrency threshold
를 기준으로:
accept
queue
rate-limit
reject
route-to-other-model
를 결정해야 합니다.
예:
Gateway
│
▼
┌─────────────┐
│ Model Router│
└──────┬──────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Llama-70B Qwen-32B Llama-8B
GPU 0-3 GPU 4-5 GPU 6
│
GPU 7
Router가 단순히:
/model/llama
만 보는 게 아니라:
model
GPU availability
KV cache
queue depth
current concurrency
priority
SLA
를 보고 routing하면 훨씬 효율적입니다.
현재 말씀하신 목적을 기준으로 하면:
B300 Node
┌───────────────────┐
│ │
│ GPU 0 ─┐ │
│ GPU 1 │ │
│ GPU 2 ├─ 70B │
│ GPU 3 ─┘ │
│ │
│ GPU 4 ─┐ │
│ GPU 5 ─┴─ 32B │
│ │
│ GPU 6 ─── 8B │
│ │
│ GPU 7 ─── 8B/ │
│ Batch │
└───────────────────┘
그리고 GPU 7을 여유/overflow GPU로 남겨두는 것도 꽤 좋은 전략입니다.
8장을 처음부터 100% 다 채우기보다:
70B → GPU 0-3
32B → GPU 4-5
8B → GPU 6
overflow→ GPU 7
로 시작하고 실제 workload를 측정한 뒤 재배치합니다.
장기적으로는:
Model A
GPU 0-3
라는 설정 자체보다
Model A
required GPU = 4
min GPU = 2
preferred GPU = 4
priority = 100
처럼 policy 형태로 관리하는 것을 추천합니다.
그러면 나중에:
70B request 증가
↓
8B GPU 일부 회수
↓
70B GPU 4 → 6
↓
8B → GPU 1장
같은 elastic allocation을 구현할 수 있습니다.
제가 보기에는 단순 benchmark 결과표보다 아래 데이터를 만드는 것이 훨씬 중요합니다.
┌──────────────────────┐
│ Model │
├──────────────────────┤
│ Precision │
│ TP │
│ Context │
│ Concurrency │
│ GPU count │
├──────────────────────┤
│ TTFT │
│ ITL │
│ Input tok/s │
│ Output tok/s │
│ Total tok/s │
│ tok/s/GPU │
│ GPU memory │
│ KV cache │
│ GPU utilization │
│ HBM bandwidth │
└──────────────────────┘
이 데이터를 쌓으면 결국 "B300 8장을 어떤 모델 조합으로 배치해야 하는가"를 정량적으로 결정할 수 있습니다.
그리고 지금 진행하시는 TP=1/2/4/8 × BF16/FP8/FP4 × context × concurrency benchmark는 바로 이 목적에 아주 잘 맞습니다.
원하시면 다음 단계로는 B300 8장에 Llama 70B + Qwen 32B + Llama 8B + GPT-OSS/DeepSeek 등을 동시에 서비스한다는 가정으로, 실제 GPU 배치표 + Triton/vLLM 구성 + Prometheus/Grafana metric + QoS/priority + 장애/OOM 대응까지 포함한 "B300 8-GPU 운영 아키텍처"를 구체적인 Kubernetes YAML 수준까지 설계해드릴 수 있습니다.
==
B300 8장짜리 단일 inference node에서 여러 모델을 서비스한다는 전제라면, Front Gateway는 일반적인 L7 Load Balancer보다는 LLM-aware routing이 가능한 구조를 추천합니다.
제 기준으로는:
1순위: Envoy Gateway/Envoy + 별도 LLM Router
2순위: NVIDIA Dynamo Frontend/Router 계열
3순위: LiteLLM
4순위: 단순 NGINX/HAProxy
입니다.
다만 각각 역할이 조금 다릅니다.
Client
│
▼
┌────────────────┐
│ Envoy Gateway │
│ TLS / Auth │
│ Rate Limit │
│ Routing │
└───────┬────────┘
│
▼
┌────────────────┐
│ LLM Router │
│ │
│ model │
│ queue │
│ KV cache │
│ concurrency │
│ GPU load │
└───────┬────────┘
│
┌───────────┼────────────┐
▼ ▼ ▼
Model A Model B Model C
70B 32B 8B
GPU 0-3 GPU 4-5 GPU 6
여기서 중요한 것은 Envoy와 LLM Router를 분리하는 것입니다.
Envoy에게
"현재 KV cache가 72%인 70B replica보다 43%인 replica로 보내줘"
같은 판단을 맡기지 않는 것이 좋습니다.
여러 모델을 한 API endpoint처럼 제공하려면 상당히 편합니다.
예:
/v1/chat/completions
model=llama-70b
model=qwen-32b
model=llama-8b
model=deepseek-r1
Front에서:
Client
↓
LiteLLM
↓
vLLM / Triton / TensorRT-LLM
형태로 만들 수 있습니다.
장점은:
입니다.
PoC나 초기 서비스에는 가장 쉽게 접근할 수 있습니다.
다만 B300 8장을 제대로 활용해서 GPU 상태/KV cache/queue 기반의 세밀한 dynamic load balancing까지 하려면 LiteLLM만으로 끝내기보다는 뒤쪽에 별도 scheduler/metrics layer를 두는 편이 낫습니다.
지금 사용자 환경에서는 저는 NVIDIA Dynamo를 특히 검토하겠습니다.
이유는 B300에서 단순히:
Request → GPU
가 아니라,
Prefill
Decode
KV Cache
GPU
Network
를 고려한 inference orchestration으로 가고 있기 때문입니다.
특히 향후 사용자가 생각하고 있는 MemKV / AIStor / AIStor Memory / NIXL 계열까지 연결하려면 일반적인 API Gateway보다 이런 inference-native architecture가 더 잘 맞습니다.
개념적으로:
Frontend
│
▼
┌──────────────┐
│ Dynamo Router│
└───────┬──────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Prefill Prefill Prefill
│ │ │
└──────────┼──────────┘
▼
KV Cache
│
┌──────────┼──────────┐
▼ ▼ ▼
Decode Decode Decode
GPU0-3 GPU4-5 GPU6
처럼 LLM inference 자체를 하나의 distributed system으로 보는 방향입니다.
따라서 단순히 여러 모델을 띄우는 수준이면 과할 수 있지만, B300을 inference platform으로 운영할 생각이라면 장기적으로 검토 가치가 높습니다.
Front Door는 저는 Envoy 쪽을 선호합니다.
Internet / Internal Client
│
▼
Envoy
│
├── Auth
├── TLS
├── Rate limit
├── Request size
├── Timeout
├── Retry
├── Circuit breaker
└── Model routing
│
▼
LLM Router
특히 현재 사용자가 이미 Cilium + Gateway API 환경을 쓰고 있으므로:
Gateway API
│
▼
Envoy Gateway
│
▼
LLM Router
가 상당히 자연스럽습니다.
그리고 Gateway API를 외부 API 계약으로 사용하는 것도 좋습니다.
예를 들어:
/model/llama
/model/qwen
/model/deepseek
정도로만 routing한다면 NGINX도 충분합니다.
하지만:
GPU utilization
KV cache
queue depth
active sequence
TTFT
model loading
을 보고 동적으로 routing하려면 NGINX 자체에 로직을 넣는 것은 추천하지 않습니다.
즉:
NGINX
↓
LLM Router
보다는 처음부터:
Envoy
↓
LLM Router
쪽을 추천합니다.
여기가 실제로 가장 중요합니다.
일반 LB:
Round Robin
Least Connection
만 사용하면 안 됩니다.
LLM에서는 예를 들어:
Model A
Replica 1
queue = 3
KV = 42%
Replica 2
queue = 9
KV = 78%
Replica 3
queue = 2
KV = 51%
라면 단순 connection 수가 아니라 LLM load score를 계산하는 것이 좋습니다.
예:
score =
0.35 × queue_pressure
+ 0.30 × KV_pressure
+ 0.20 × active_requests
+ 0.10 × GPU_utilization
+ 0.05 × latency
그리고:
lowest score → routing
합니다.
가중치는 실제 benchmark 결과로 조정하면 됩니다.
이것도 상당히 중요합니다.
예를 들어 사용자가:
model = llama-70b
라고 요청했다고 합시다.
Router의 역할은 두 단계입니다.
1. Model selection
llama-70b
│
▼
2. Replica selection
70B replica A
70B replica B
70B replica C
즉:
Request
│
▼
Model Router
│
┌─────────┴─────────┐
▼ ▼
llama-70b qwen-32b
│
┌─────┼─────┐
▼ ▼ ▼
R1 R2 R3
이렇게 설계하는 게 좋습니다.
B300 8장 단일 노드에서는 처음에는:
70B
GPU 0-3
32B
GPU 4-5
8B
GPU 6
8B/Embedding
GPU 7
처럼 model → GPU set을 먼저 정의하고,
그 다음에 해당 GPU set 안에서 routing하는 게 좋습니다.
예:
llama-70b
│
├── engine-0 → GPU 0-3
└── engine-1 → GPU 4-7
이런 식의 replica를 실제로 만들 수도 있지만, TP 때문에 GPU topology를 반드시 고려해야 합니다.
이건 일반적인 GPU cluster보다 더 중요합니다.
예를 들어 TP=4가:
GPU 0,1,2,3
에 있을 때와
GPU 0,2,5,7
에 있을 때 성능이 같다고 가정하면 안 됩니다.
따라서 Router가 최소한:
model
TP size
GPU set
GPU topology
를 알아야 합니다.
그리고 가능한 경우:
TP group을 하나의 atomic resource
로 취급하는 것이 좋습니다.
즉:
GPU 0
GPU 1
GPU 2
GPU 3
을 각각 LB하는 것이 아니라:
┌──────────────────┐
│ TP Group #1 │
│ GPU 0-3 │
│ Llama-70B │
└──────────────────┘
를 하나의 backend로 봅니다.
Gateway API
│
Envoy Gateway
│
LiteLLM
│
├── vLLM
├── Triton
└── TensorRT-LLM
이 단계에서는 성능 측정과 API 통합에 집중합니다.
Client
│
▼
┌────────────────┐
│ Cilium Gateway │
│ API / TLS / Auth│
└───────┬────────┘
│
▼
┌────────────────┐
│ LLM Router │
│ │
│ Model │
│ Queue │
│ KV cache │
│ Concurrency │
│ SLA │
└───────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
70B Pool 32B Pool 8B Pool
GPU 0-3 GPU 4-5 GPU 6
│
GPU 7
향후:
Gateway API
│
▼
LLM Router / Dynamo
│
├── Prefill
├── Decode
├── KV Cache
├── GPU scheduling
└── Model lifecycle
│
▼
B300 GPU Pool
│
┌─────┴─────┐
▼ ▼
AIStor MemKV /
AIStor Memory
방향으로 발전시키는 것을 추천합니다.
현재 상황에서는 하나를 고르라면 Envoy Gateway + LLM-aware Router 구조를 추천합니다.
그리고 Router 구현체는 단계적으로:
| 목적 | 추천 |
|---|---|
| API Gateway / TLS / Auth | Envoy Gateway |
| 초기 multi-model routing | LiteLLM |
| GPU/KV-aware LLM routing | NVIDIA Dynamo 계열 검토 |
| Inference engine | vLLM / TensorRT-LLM / Triton |
| Metrics | Prometheus + DCGM + inference metrics |
| Dashboard | Grafana |
| Kubernetes networking | Cilium + Gateway API |
특히 B300 8장 한 대에서는 처음부터 복잡한 distributed router를 만드는 것보다,
Cilium Gateway API → Envoy → LLM Router → model/TP-group
구조로 시작하고, benchmark에서 얻은 TTFT / ITL / KV cache / queue / tok/s/GPU를 Router의 routing signal로 넣는 방향이 가장 깔끔합니다.
원하시면 다음 답변에서 "B300 8장 + Kubernetes + Cilium Gateway API + vLLM/Triton 기준으로 실제 Front Gateway/Router를 어떻게 구성할지"를 Gateway API YAML → Envoy → Router → GPU pool → Prometheus metrics까지 실제 배포 구조로 그려드릴게요.
==
네. 이 경우에는 ServiceAccount를 쓰는 게 맞지만, "Kubernetes ServiceAccount token을 모든 인증에 그대로 사용"하는 것은 권장하지 않습니다.
특히 지금처럼 Kubernetes + Cilium ClusterMesh + Front Gateway + 여러 LLM 구조라면 인증을 3개 층으로 분리하는 게 가장 깔끔합니다.
Client
│
┌───────▼────────┐
│ Envoy Gateway │
│ │
│ TLS / OIDC │
│ Rate Limit │
│ API Key │
└───────┬────────┘
│
authenticated
│
┌───────▼────────┐
│ LLM Router │
└───────┬────────┘
│
┌────────┼────────┐
│ │ │
▼ ▼ ▼
70B 32B 8B
Service Service Service
│ │ │
└────────┼────────┘
│
Cilium identity
/ NetworkPolicy
│
ClusterMesh
여기서 각각의 인증/보안 목적을 다르게 가져갑니다.
| 구간 | 권장 방식 | 목적 |
|---|---|---|
| User → Gateway | OIDC/OAuth2 또는 API Key | 사용자/앱 인증 |
| Gateway → LLM Router | mTLS 또는 내부 신뢰 + NetworkPolicy | 서비스 인증 |
| Pod → Kubernetes API | ServiceAccount + projected token | K8s API 인증 |
| Pod → 다른 서비스 | Cilium identity + NetworkPolicy, 필요 시 mTLS | 서비스 간 접근제어 |
| Cluster ↔ Cluster | Cilium ClusterMesh mTLS | ClusterMesh 자체 보안 |
Kubernetes ServiceAccount는 원래 Kubernetes API뿐 아니라 신뢰 관계가 있는 다른 시스템에서도 JWT 인증에 사용할 수 있습니다. 다만 현재 Kubernetes에서는 장기-lived Secret token보다는 TokenRequest 기반의 short-lived projected token이 권장됩니다. (Kubernetes)
예를 들어 사용자가:
Application A
│
│ Authorization: Bearer <token>
▼
LLM Gateway
라고 호출한다고 해서 이 token을 단순히 Kubernetes ServiceAccount token으로 만드는 것은 권장하지 않습니다.
왜냐하면:
ServiceAccount
= workload identity
이고,
User identity
= 사람/애플리케이션의 API identity
이기 때문입니다.
둘을 섞으면 나중에:
누가 호출했는가?
어떤 application인가?
어떤 namespace인가?
어떤 model을 사용할 수 있는가?
를 관리하기 어려워집니다.
예를 들어 사내 사용자가 LLM API를 호출한다면:
User
│
│ OIDC login
▼
Keycloak / AD
│
│ JWT
▼
Envoy Gateway
│
│ validated identity
▼
LLM Router
가 가장 깔끔합니다.
현재 환경에서 Keycloak + AD SSO를 이미 사용하고 있으므로 특히 잘 맞습니다.
JWT에:
sub
groups
preferred_username
aud
iss
exp
등을 넣고 Gateway에서 검증합니다.
그 다음 authorization을:
group = ai-platform-admin
→ 모든 model
group = ai-user
→ 8B / 32B
group = batch-user
→ batch model only
처럼 가져갈 수 있습니다.
인증(Authentication)과 모델 접근권한(Authorization)을 분리하는 것이 핵심입니다.
여기가 사용자 환경에서는 더 중요할 것 같습니다.
예를 들어:
Jupyter
Airflow
StarRocks
AI Agent
internal application
등이 LLM Gateway를 호출하는 경우입니다.
Kubernetes workload마다 ServiceAccount를 만들고:
jupyter
↓
ServiceAccount: jupyter-llm
↓
LLM Gateway
형태로 identity를 전달합니다.
단, Gateway가 Kubernetes ServiceAccount JWT를 검증할 수 있어야 합니다.
Kubernetes의 projected ServiceAccount token은 Pod에 bound되고, 기본적으로 짧은 수명의 TokenRequest token이며 kubelet이 자동 갱신합니다. (Kubernetes)
이 방식은 Kubernetes-native workload identity가 필요할 때 좋습니다.
조금 더 큰 플랫폼으로 가면:
Kubernetes SA
│
│ workload identity
▼
Identity Provider
│
│ short-lived credential
▼
LLM Gateway
형태가 좋습니다.
특히 이미 Keycloak을 쓰고 있다면:
Keycloak
│
┌───────────┼───────────┐
│ │ │
user-app jupyter airfow
│ │ │
└───────────┼───────────┘
▼
Envoy Gateway
│
LLM Router
처럼 통합할 수 있습니다.
내부 workload가 매우 많다면 API Key도 상당히 실용적입니다.
예:
Authorization: Bearer sk-xxxx
하지만 API Key를:
Secret
↓
Pod
↓
LLM Gateway
로 장기간 고정하는 것은 추천하지 않습니다.
장점:
단점:
문제가 있습니다.
그래서 API Key를 쓰더라도:
short-lived
scoped
revocable
model-specific
하게 만드는 것이 좋습니다.
예를 들어:
LLM Router
│
│ mTLS
▼
vLLM
이면 좋습니다.
하지만:
User
│
│ mTLS
▼
LLM Gateway
를 모든 사용자에게 적용하는 것은 운영 복잡도가 상당히 올라갑니다.
mTLS의 목적은:
"이 요청을 보낸 peer가 정말 내가 신뢰하는 service인가?"
에 더 가깝습니다.
Cilium도 service-to-service identity 기반 보안을 지향하고 있고, 현재 Cilium의 별도 mutual authentication 기능은 SPIFFE/SPIRE 기반이지만 아직 beta이며 ClusterMesh와의 단일 trust domain 사용에도 제한이 있습니다. 따라서 지금 환경에서는 Cilium ClusterMesh의 mTLS와 애플리케이션 인증을 동일한 것으로 취급하면 안 됩니다. (Cilium Documentation)
이게 중요합니다.
Cluster A
│
│ Cilium ClusterMesh
│
Cluster B
여기서 Cilium ClusterMesh가 해주는 것은 ClusterMesh control-plane/state synchronization과 cross-cluster networking의 신뢰/보안입니다.
ClusterMesh 자체가:
"이 애플리케이션이 LLM을 사용할 권한이 있는가?"
를 인증해주는 것은 아닙니다.
Cilium 문서에서도 ClusterMesh 연결 클러스터는 하나의 trust domain을 형성하므로 서로 신뢰하고 보안 수준이 동등한 클러스터만 연결해야 한다고 설명합니다. ClusterMesh 자체 통신은 mTLS로 보호됩니다. (Cilium Documentation)
따라서:
ClusterMesh
≠
Application Authentication
입니다.
예를 들어:
Cluster KR01
namespace ai
│
│
▼
LLM Gateway
│
▼
AIStor / Model Server
라면 NetworkPolicy를:
AI workloads
↓ allowed
LLM Gateway
Other namespaces
↓ denied
LLM Gateway
로 제한합니다.
그리고 가능하면 IP 기반이 아니라:
namespace
service account
Cilium identity
service
기준으로 정책을 잡습니다.
이렇게 하면:
Pod IP가 변경됨
Node가 변경됨
ClusterMesh를 통해 다른 cluster에서 호출됨
같은 상황에서도 IP allowlist보다 훨씬 관리하기 쉽습니다.
사용자 환경이라면 저는 이렇게 설계하겠습니다.
┌──────────────┐
│ Keycloak │
│ + AD │
└──────┬───────┘
│
OIDC / JWT
│
▼
┌───────────────┐ ┌────────────────┐
│ Human User │──────▶│ Envoy Gateway │
└───────────────┘ │ │
│ JWT validation │
┌───────────────┐ │ Rate limit │
│ Application │──────▶│ AuthZ │
└───────────────┘ └───────┬────────┘
│
authenticated
│
▼
┌──────────────┐
│ LLM Router │
└──────┬───────┘
│
Cilium identity/policy
│
┌──────────────┼──────────────┐
▼ ▼ ▼
70B 32B 8B
그리고 Kubernetes ServiceAccount는 Gateway 사용자 인증의 기본 수단이 아니라 workload identity로 사용합니다.
예를 들어:
namespace: ai-app
ServiceAccount:
jupyter-llm
ServiceAccount:
agent-llm
ServiceAccount:
batch-llm
namespace: inference
ServiceAccount:
llm-router
ServiceAccount:
model-server
그러면:
jupyter-llm
→ llama-8b
→ qwen-32b
agent-llm
→ llama-8b
→ qwen-32b
→ 70b
batch-llm
→ batch models only
같은 authorization policy를 만들 수 있습니다.
이것도 자주 헷갈립니다.
SA: agent-llm
이 있다고 해서 자동으로:
agent → LLM Gateway
권한이 생기는 것은 아닙니다.
그리고:
agent → Kubernetes API
권한도 별도로 RBAC으로 결정됩니다.
즉:
agent-llm SA
│
┌────────────┴────────────┐
▼ ▼
K8s API RBAC LLM Gateway AuthZ
│ │
pods/get etc. model A/B/C
으로 분리하는 게 좋습니다.
all pods
↓
same-api-key
예전 방식의:
Secret
type: kubernetes.io/service-account-token
을 장기간 보관하는 방식은 피하는 게 좋습니다. Kubernetes는 projected, time-bound ServiceAccount token을 기본 방식으로 사용하며, 오래 살아있는 Secret 기반 token보다 안전한 방향입니다. (Kubernetes)
ClusterMesh 연결됨
→ 누구나 LLM 호출 가능
이렇게 하면 안 됩니다.
특히 ClusterMesh에서는 Pod/Service IP와 topology가 바뀔 수 있기 때문에 identity 기반 정책이 낫습니다.
제가 생각하는 이상적인 형태는:
JWT
│
├── subject
├── issuer
├── groups
├── namespace
├── service-account
└── audience
│
▼
AuthZ Policy
│
├── model
├── max context
├── max tokens
├── concurrency
├── priority
└── rate limit
입니다.
예를 들어:
agent-a
allowed models:
llama-8b
qwen-32b
max context:
32K
max concurrency:
8
priority:
normal
반면:
research-agent
allowed:
llama-70b
deepseek-r1
gpt-oss-120b
max concurrency:
32
priority:
high
처럼 할 수 있습니다.
이렇게 하면 인증이 단순히 "접속 가능/불가능"에서 끝나는 게 아니라 LLM resource governance와 연결됩니다.
사람 → Keycloak/OIDC → Envoy Gateway → LLM Router,
Kubernetes workload → ServiceAccount/workload identity → Gateway,
Gateway/Router → inference service → Cilium identity + NetworkPolicy,
Cluster 간 연결 → Cilium ClusterMesh mTLS로 분리하는 것이 좋습니다. (Kubernetes)
그리고 현재 사용자의 Keycloak + AD + Cilium ClusterMesh + Gateway API 구성을 생각하면, 굳이 새로운 인증 시스템을 하나 더 만드는 것보다는 Keycloak을 human/app identity의 중심으로 두고, Kubernetes SA는 workload identity로 제한하는 방향이 가장 자연스럽습니다.