26S30k

QK·2일 전

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 고립, 엔진 서빙 아키텍처 및 메트릭 관측성 관점에서의 베스트 프랙티스는 다음과 같습니다.


1. GPU 토폴로지 기반 자원 파티셔닝 (Tensor Parallelism 분할)

B300 8-way 노드는 NVLink Switch를 통해 8장이 단일 패브릭으로 묶여 있지만, 여러 모델을 동시에 쪼개어 서빙할 때는 NUMA 노드와 PCIe/NVLink 도메인에 맞춰 2의 거듭제곱 단위(TP=1, 2, 4, 8)로 격리해야 All-Reduce 병목이 발생하지 않습니다.

권장 모델 배치 시나리오

  • 케이스 A: 초대형 MoE 1개 + 중소형 모델 병행 (가장 흔한 멀티모델 패턴)
  • GPU 0~3 (TP=4): DeepSeek-R1-Distill-70B 또는 중대형 MoE 모델 (4장 × 288GB = 1.15TB VRAM 풀)
  • GPU 4~5 (TP=2): Qwen3-32B / Llama 계열 메인 LLM 추론 (2장 × 288GB = 576GB VRAM)
  • GPU 6 (TP=1): 경량 7B~14B 모델 또는 코드 전용 LLM
  • GPU 7 (TP=1): 임베딩(Embedding) + 리랭커(Reranker) 전용 인스턴스 (vLLM 또는 TEI)
  • 케이스 B: DeepSeek-R1 Full (FP8/FP4) 전용 서빙
  • DeepSeek 671B MoE 가중치(FP8 기준 약 700GB)는 단일 노드 8장(TP=8, EP=8)으로 VRAM에 완전히 올릴 수 있으므로, 멀티 테넌트보다는 단일 노드 전체를 전용으로 할당하는 것이 성능상 유리합니다.

CPU & NUMA Affinity 바인딩 (필수)

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

2. 추론 엔진(Engine) 및 메모리 격리 전략

여러 추론 프로세스가 동일 물리 노드에 공존할 때 OOM(Out of Memory) 연쇄 장애를 막는 설정입니다.

  1. CUDA_VISIBLE_DEVICES 하드웨어 격리:
  • 각 서빙 컨테이너(또는 프로세스)에는 사용할 GPU 인덱스만 노출시킵니다.
  • Container A: CUDA_VISIBLE_DEVICES=0,1,2,3
  • Container B: CUDA_VISIBLE_DEVICES=4,5
  1. --gpu-memory-utilization 고정 및 KV Cache 버퍼 제어:
  • vLLM/SGLang은 기본적으로 GPU 메모리의 90% 이상을 가중치 + KV Cache 블록으로 사전 선점(Pre-allocate)합니다.
  • 멀티 프로세스 환경에서는 프레임워크 오버헤드, PyTorch CUDA 컨텍스트, PagedAttention 관리 버퍼를 고려하여 0.85 ~ 0.90 수준으로 설정해 메모리 스파이크를 방지합니다.
  1. MIG(Multi-Instance GPU) vs 프로세스 기반 분할:
  • B300에서는 가급적 MIG 대신 vLLM 단일 컨테이너 단위 분할 권장: MIG를 활성화하면 NVLink 대역폭 활용이 제한되고 Tensor Parallelism 제약이 생깁니다. LLM 추론은 NVLink를 온전히 활용할 수 있도록 베어메탈/컨테이너 레벨에서 GPU 카운트 단위(TP=1, 2, 4)로 물리 카드를 분배하는 것이 좋습니다.

3. 라우팅 및 동적 부하 분산 (Front Gateway)

노드 1대 내부에서 모델별로 각기 다른 포트(8001, 8002, 8003...)로 엔진이 뜨게 되므로, 앞단에 경량 AI 게이트웨이를 배치해야 클라이언트 관리가 단순해집니다.

  • Reverse Proxy / Gateway: Litellm, Envoy, 또는 Traefik
  • 포트 8000 단일 엔드포인트 노출 후 요청 헤더/바디의 model 필드에 따라 로컬 포트로 라우팅:
  • "model": "qwen3-32b" →\rightarrow localhost:8001
  • "model": "deepseek-r1-distill-70b" →\rightarrow localhost:8002
  • "model": "bge-reranker" →\rightarrow localhost:8003
  • Prefix Caching 활성화:
  • 동일 모델에 반복되는 시스템 프롬프트(System Prompt)가 많다면 --enable-prefix-caching을 켜서 중복 토큰 연산과 KV Cache 소비를 40~60% 절감합니다.

4. 모니터링 및 관측성(Observability) 파이프라인

단일 노드 멀티 모델 환경에서는 "어느 모델이 어느 GPU 대역폭을 잠식하고 있는지"를 즉각 분리해 관측해야 합니다.

[B300 GPUs] ──> DCGM Exporter (포트 9400) ──┐
[vLLM #1]   ──> Prometheus 메트릭 (8001)   ──┼──> Prometheus Server ──> Grafana Dashboard
[vLLM #2]   ──> Prometheus 메트릭 (8002)   ──┘

A. 하드웨어 레벨: NVIDIA DCGM Exporter (Blackwell 지원 버전)

  • 포트: 기본 9400
  • 필수 감시 메트릭:
  • 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 노드 고전력/열 부하 감시

B. 서빙 엔진 레벨: vLLM / SGLang 내장 Prometheus 메트릭

각 vLLM 인스턴스는 자체 포트(예: :8001/metrics, :8002/metrics)로 상세 메트릭을 노출합니다.

  • KV Cache 포화도:
  • vllm:gpu_cache_usage_factor: 가장 중요. 0.85 이상 지속 시 새 요청이 큐에 쌓이며 TTFT(Time to First Token) 지연 급증.
  • 대기열 병목:
  • vllm:num_requests_waiting: 요청이 처리되지 못하고 큐에 머무는 수. 0보다 크면 동시 요청 처리 한계 도달을 의미.
  • vllm:num_requests_running: 현재 PagedAttention으로 동시 디코딩 중인 배치 크기.
  • 처리 성능(Latency & Throughput):
  • vllm:time_to_first_token_seconds: 초기 Prefill 지연 시간.
  • vllm:time_per_output_token_seconds: 디코딩 토큰 생성 간격 (사용자 체감 속도).

5. 핵심 운용 체크리스트

점검 영역권장 설정값 / 액션목적
NUMA 바인딩numactl --cpunodebind=X --membind=X소켓 간 PCIe 크로스오버 통신 억제
GPU 격리컨테이너별 CUDA_VISIBLE_DEVICES 명시프로세스 간 VRAM 침범 및 충돌 방지
KV Cache--gpu-memory-utilization 0.85~0.90PagedAttention 할당 버퍼 확보로 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를 배치하는 것입니다.


1. 가장 먼저 GPU를 모델별로 논리적으로 나누는 게 좋습니다

B300 8장이라면 처음부터 모든 GPU를 하나의 거대한 pool로 보고 스케줄링하기보다는 다음처럼 GPU pool을 만드는 것을 추천합니다.

예를 들어:

PoolGPU용도
Pool-A0-370B급 모델
Pool-B48B/14B
Pool-C58B/14B
Pool-D6-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가 영향을 받는 상황을 줄일 수 있습니다.


2. 모델별 GPU 개수는 "모델 크기"보다 TP scaling으로 결정

예를 들어:

70B

TP=4
GPU 0-3

8B

TP=1
GPU 4

32B

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에서 어떤 모델을 동시에 돌릴지 계산할 수 있습니다.


3. 중요한 것은 GPU utilization이 아니라 "GPU memory + KV cache"

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에 맞춰 잡아야 합니다.


4. 작은 모델은 GPU 하나에 여러 모델을 넣을 수도 있음

이 부분이 상당히 중요합니다.

예를 들어:

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             │
└────────────────────────────┘

5. 가능하면 MIG보다 먼저 "process/container isolation"을 검토

B300에서 GPU partitioning을 생각하면 MIG가 먼저 떠오를 수 있습니다.

하지만 LLM inference에서는 무조건 MIG가 좋은 것은 아닙니다.

특히:

70B
TP=4
NVLink/NVSwitch
KV cache
large batch

같은 workload에서는 GPU 전체를 사용하는 편이 훨씬 유리할 수 있습니다.

따라서 저는:

Large model

Full GPU

Small model

Full GPU
+ multiple inference processes

Strict isolation이 필요한 경우

MIG

순서로 검토하겠습니다.


6. Kubernetes에서는 "GPU를 누가 소유하는가"를 명확히 해야 함

지금 환경이 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

7. Triton을 중심으로 잡는 것도 좋은 선택

현재 B300 benchmark를 하고 있으니 저는 실제 서비스 구조에서는 Triton을 상당히 유력하게 봅니다.

구조는:

                API Gateway
                     │
                     ▼
              Model Router
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
     Model A      Model B      Model C
     Triton       Triton       Triton
        │            │            │
      GPU0-3        GPU4         GPU5

Triton의 장점은:

  • model repository
  • dynamic batching
  • concurrent execution
  • metrics
  • model lifecycle
  • Kubernetes integration
  • 여러 backend 지원

등입니다.

다만 LLM 자체의 scheduler/KV cache 최적화는 vLLM/TensorRT-LLM 등 engine의 특성이 더 중요하기 때문에,

Triton
  +
TensorRT-LLM

또는

Triton
  +
vLLM backend

같은 형태를 모델에 따라 비교하는 것이 좋습니다.


8. 모델별로 "서비스 등급"을 만드는 것을 추천

예를 들어:

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

처럼 배치합니다.

이게 실제 운영에서 상당히 중요합니다.


9. Monitoring은 GPU만 보면 안 됨

제가 추천하는 dashboard는 최소한 다음 구조입니다.

GPU dashboard

GPU utilization
HBM utilization
HBM bandwidth
Tensor Core utilization
Power
Temperature
PCIe throughput
NVLink/NVSwitch throughput
ECC/XID

LLM dashboard

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 문제가 더 클 수 있습니다.


10. Prometheus metric을 모델별로 분리

현재 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

등을 모델별로 수집합니다.


11. 가장 중요한 운영 지표는 "GPU utilization"이 아니라 GPU당 throughput

예를 들어:

Model A

GPU utilization = 95%
throughput = 300 tok/s

과

Model A

GPU utilization = 75%
throughput = 420 tok/s

라면 후자가 훨씬 좋은 configuration일 수 있습니다.

따라서 모델별로:

tokens/sec/GPU

를 만들어야 합니다.

예:

ModelGPUThroughputtok/s/GPU
8B11,0001,000
32B2700350
70B4520130
120B4430108

이 지표가 GPU allocation 의사결정의 핵심이 됩니다.


12. Concurrency별 operating point를 정해야 함

모델마다 무한정 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만 증가하기 때문입니다.

이것을 모델별로 자동화하면 좋습니다.


13. 실제 운영에서는 admission control이 매우 중요

예를 들어:

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

를 결정해야 합니다.


14. 여러 모델을 운용한다면 Router가 상당히 중요

예:

                    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하면 훨씬 효율적입니다.


15. 저는 B300 8장을 처음에는 이렇게 운용하겠습니다

현재 말씀하신 목적을 기준으로 하면:

             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를 측정한 뒤 재배치합니다.


16. 그리고 한 가지 중요한 점: "모델별 GPU 고정"도 최종적으로는 바꿀 수 있어야 함

장기적으로는:

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을 구현할 수 있습니다.


특히 지금 하시는 B300 benchmark에서는

제가 보기에는 단순 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로 보내줘"

같은 판단을 맡기지 않는 것이 좋습니다.


1. LiteLLM — 가장 빨리 시작하려면

여러 모델을 한 API endpoint처럼 제공하려면 상당히 편합니다.

예:

/v1/chat/completions

model=llama-70b
model=qwen-32b
model=llama-8b
model=deepseek-r1

Front에서:

Client
  ↓
LiteLLM
  ↓
vLLM / Triton / TensorRT-LLM

형태로 만들 수 있습니다.

장점은:

  • OpenAI-compatible API
  • 여러 inference backend 연결
  • 모델별 routing
  • fallback
  • retry
  • rate limit
  • API key
  • usage tracking
  • 상대적으로 쉬운 운영

입니다.

PoC나 초기 서비스에는 가장 쉽게 접근할 수 있습니다.

다만 B300 8장을 제대로 활용해서 GPU 상태/KV cache/queue 기반의 세밀한 dynamic load balancing까지 하려면 LiteLLM만으로 끝내기보다는 뒤쪽에 별도 scheduler/metrics layer를 두는 편이 낫습니다.


2. NVIDIA Dynamo — 장기적으로는 이쪽을 눈여겨볼 만함

지금 사용자 환경에서는 저는 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으로 운영할 생각이라면 장기적으로 검토 가치가 높습니다.


3. Envoy — Front Gateway 자체는 이걸 추천

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 계약으로 사용하는 것도 좋습니다.


4. NGINX/HAProxy는 "Front"까지만

예를 들어:

/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

쪽을 추천합니다.


5. "동적 부하분산"의 기준을 이렇게 잡는 게 중요

여기가 실제로 가장 중요합니다.

일반 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 결과로 조정하면 됩니다.


6. 모델 선택과 Replica 선택을 분리하세요

이것도 상당히 중요합니다.

예를 들어 사용자가:

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

이렇게 설계하는 게 좋습니다.


7. 그런데 지금은 "Replica"보다 "GPU partition"이 중요

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를 반드시 고려해야 합니다.


8. 특히 B300에서는 GPU topology-aware routing이 중요

이건 일반적인 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로 봅니다.


9. 제가 지금 환경이라면 이렇게 갑니다

Phase 1 — Benchmark

Gateway API
    │
 Envoy Gateway
    │
 LiteLLM
    │
 ├── vLLM
 ├── Triton
 └── TensorRT-LLM

이 단계에서는 성능 측정과 API 통합에 집중합니다.


Phase 2 — Production

                    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

Phase 3 — NVIDIA inference platform

향후:

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 / AuthEnvoy Gateway
초기 multi-model routingLiteLLM
GPU/KV-aware LLM routingNVIDIA Dynamo 계열 검토
Inference enginevLLM / TensorRT-LLM / Triton
MetricsPrometheus + DCGM + inference metrics
DashboardGrafana
Kubernetes networkingCilium + 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 → GatewayOIDC/OAuth2 또는 API Key사용자/앱 인증
Gateway → LLM RoutermTLS 또는 내부 신뢰 + NetworkPolicy서비스 인증
Pod → Kubernetes APIServiceAccount + projected tokenK8s API 인증
Pod → 다른 서비스Cilium identity + NetworkPolicy, 필요 시 mTLS서비스 간 접근제어
Cluster ↔ ClusterCilium ClusterMesh mTLSClusterMesh 자체 보안

Kubernetes ServiceAccount는 원래 Kubernetes API뿐 아니라 신뢰 관계가 있는 다른 시스템에서도 JWT 인증에 사용할 수 있습니다. 다만 현재 Kubernetes에서는 장기-lived Secret token보다는 TokenRequest 기반의 short-lived projected token이 권장됩니다. (Kubernetes)


1. ServiceAccount를 사용자 인증으로 쓰지는 않는 게 좋습니다

예를 들어 사용자가:

Application A
   │
   │ Authorization: Bearer <token>
   ▼
LLM Gateway

라고 호출한다고 해서 이 token을 단순히 Kubernetes ServiceAccount token으로 만드는 것은 권장하지 않습니다.

왜냐하면:

ServiceAccount
= workload identity

이고,

User identity
= 사람/애플리케이션의 API identity

이기 때문입니다.

둘을 섞으면 나중에:

누가 호출했는가?
어떤 application인가?
어떤 namespace인가?
어떤 model을 사용할 수 있는가?

를 관리하기 어려워집니다.


2. 사람이 사용하는 경우 → OIDC

예를 들어 사내 사용자가 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)을 분리하는 것이 핵심입니다.


3. 애플리케이션 → LLM이라면 두 가지 선택

여기가 사용자 환경에서는 더 중요할 것 같습니다.

예를 들어:

Jupyter
Airflow
StarRocks
AI Agent
internal application

등이 LLM Gateway를 호출하는 경우입니다.

선택 A — Workload Identity

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가 필요할 때 좋습니다.


4. 하지만 저는 LLM Gateway에서는 "별도의 workload identity"를 더 선호합니다

조금 더 큰 플랫폼으로 가면:

Kubernetes SA
      │
      │ workload identity
      ▼
Identity Provider
      │
      │ short-lived credential
      ▼
LLM Gateway

형태가 좋습니다.

특히 이미 Keycloak을 쓰고 있다면:

                    Keycloak
                       │
           ┌───────────┼───────────┐
           │           │           │
        user-app    jupyter     airfow
           │           │           │
           └───────────┼───────────┘
                       ▼
                 Envoy Gateway
                       │
                    LLM Router

처럼 통합할 수 있습니다.


5. API Key는 어떠냐?

내부 workload가 매우 많다면 API Key도 상당히 실용적입니다.

예:

Authorization: Bearer sk-xxxx

하지만 API Key를:

Secret
  ↓
Pod
  ↓
LLM Gateway

로 장기간 고정하는 것은 추천하지 않습니다.

장점:

  • 구현이 쉬움
  • OpenAI-compatible client와 잘 맞음
  • 외부 application 연동이 쉬움

단점:

  • rotation
  • leakage
  • revocation
  • identity propagation

문제가 있습니다.

그래서 API Key를 쓰더라도:

short-lived
scoped
revocable
model-specific

하게 만드는 것이 좋습니다.


6. mTLS는 "사용자 인증"보다는 "서비스 인증"

예를 들어:

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)


7. 특히 ClusterMesh 때문에 한 가지를 명확히 구분해야 합니다

이게 중요합니다.

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

입니다.


8. ClusterMesh에서는 오히려 CiliumNetworkPolicy가 중요

예를 들어:

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보다 훨씬 관리하기 쉽습니다.


9. 제가 추천하는 실제 인증 모델

사용자 환경이라면 저는 이렇게 설계하겠습니다.

                         ┌──────────────┐
                         │   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로 사용합니다.


10. 더 구체적으로 SA를 이렇게 만듭니다

예를 들어:

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를 만들 수 있습니다.


11. "ServiceAccount → Gateway"와 "ServiceAccount → K8s API"는 별개

이것도 자주 헷갈립니다.

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

으로 분리하는 게 좋습니다.


12. 특히 하지 않는 것을 추천하는 것

❌ 모든 Pod에 동일한 API Key

all pods
   ↓
same-api-key

❌ 장기-lived ServiceAccount Secret token

예전 방식의:

Secret
  type: kubernetes.io/service-account-token

을 장기간 보관하는 방식은 피하는 게 좋습니다. Kubernetes는 projected, time-bound ServiceAccount token을 기본 방식으로 사용하며, 오래 살아있는 Secret 기반 token보다 안전한 방향입니다. (Kubernetes)

❌ ClusterMesh를 인증으로 간주

ClusterMesh 연결됨
→ 누구나 LLM 호출 가능

이렇게 하면 안 됩니다.

❌ IP allowlist만으로 authorization

특히 ClusterMesh에서는 Pod/Service IP와 topology가 바뀔 수 있기 때문에 identity 기반 정책이 낫습니다.


13. 최종적으로는 "Identity → Model Authorization"까지 연결

제가 생각하는 이상적인 형태는:

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로 제한하는 방향이 가장 자연스럽습니다.

profile
engineer

0개의 댓글