가능합니다. 다만 여기서는 세 층을 분리해서 설계하는 게 좋습니다.
특히 B300 8-GPU 노드라면 “무조건 single-numa-node”가 정답은 아닙니다. 8-GPU 모델은 양쪽 NUMA를 사용해야 하기 때문입니다.
GPU 노드 kubelet의 기본 방향은 다음입니다.
# /var/lib/kubelet/config.yaml
cpuManagerPolicy: static
topologyManagerPolicy: restricted
topologyManagerScope: pod
CPU Manager=static은 Guaranteed Pod에 exclusive CPU를 할당할 수 있게 하고, Topology Manager는 CPU와 GPU 같은 device의 topology hint를 종합합니다. restricted는 적절한 topology alignment를 만들 수 없으면 Pod admission을 거부합니다. Kubernetes
그리고 serving Pod를 반드시 Guaranteed QoS로 만드는 게 중요합니다.
resources:
requests:
cpu: "16"
memory: "128Gi"
nvidia.com/gpu: "2"
limits:
cpu: "16"
memory: "128Gi"
nvidia.com/gpu: "2"
즉 CPU의 requests == limits로 잡습니다.
예를 들어 물리 topology가:
NUMA 0 NUMA 1
──────────────── ────────────────
CPU 0-63 CPU 64-127
GPU 0 GPU 4
GPU 1 GPU 5
GPU 2 GPU 6
GPU 3 GPU 7
NIC0 NIC1
이라면 2-GPU serving Pod에 대해 목표는:
Pod A
CPU 0-15
│
NUMA 0
├── GPU0
└── GPU1
입니다.
반대로 이것은 피하고 싶은 상태입니다.
CPU NUMA0
│
└──────── PCIe/NUMA crossing ─────── GPU5 NUMA1
single-numa-node를 쓰면 더 강력하지 않나?맞습니다.
topologyManagerPolicy: single-numa-node
이면 CPU/GPU 등을 하나의 NUMA node에 배치할 수 없는 Pod는 거부합니다. Kubernetes
그런데 네 환경에서는 문제가 있습니다.
2 GPU Pod
GPU0 + GPU1
→ NUMA0
→ OK
4 GPU Pod
GPU0~3
→ NUMA0
→ OK
8 GPU Pod
GPU0~7
→ NUMA0 + NUMA1
→ single NUMA 불가능
그래서 다양한 크기의 LLM serving을 같은 B300 node에서 운영한다면 restricted를 기본으로 두는 게 더 현실적입니다.
여기가 중요한 부분입니다.
일반적인 Kubernetes eth0 같은 host networking NIC를 사용한다고 해서 Topology Manager가 자동으로
GPU0 → NUMA0 → NIC0
GPU5 → NUMA1 → NIC1
을 만들어주는 것은 아닙니다.
NIC까지 topology-managed device로 다루려면 SR-IOV/device plugin 같은 구조가 필요합니다.
따라서 네 Ethernet 구성이 예를 들어:
B300 Node
NUMA0 NUMA1
│ │
GPU0-3 GPU4-7
│ │
NIC0 400G NIC1 400G
│ │
└────────── Ethernet Fabric ───────────┘
│
AIStor
라면 가장 강한 locality를 원할 때는:
GPU Device Plugin
+
SR-IOV Network Device Plugin
+
CPU Manager
+
Topology Manager
식으로 CPU/GPU/NIC를 resource로 노출하는 방식을 검토합니다.
다만 AIStor에서 모델을 한 번 NVMe/HBM으로 stage한 후 serving하는 구조라면 NIC locality를 지나치게 최적화할 필요는 없습니다.
실제 inference hot path가:
Client
│
NIC
│
CPU
│
GPU
│
GPU HBM
이기 때문에 request traffic 자체가 모델 weight loading traffic보다 훨씬 작을 가능성이 큽니다.
NIC locality는 오히려 multi-node TP/EP/NCCL 또는 KV-cache remote offload를 할 때 훨씬 중요해집니다.
B300 8-GPU 시스템에서 확인해야 할 첫 번째 명령은:
nvidia-smi topo -m
입니다.
목표는 대략 다음 관계를 파악하는 것입니다.
NVLink / NVSwitch
GPU0 ─┐
GPU1 ─┤
GPU2 ─┤
GPU3 ─┼──── NVSwitch fabric
GPU4 ─┤
GPU5 ─┤
GPU6 ─┤
GPU7 ─┘
여기서 중요한 점은 CPU NUMA와 GPU-to-GPU topology를 동일하게 생각하면 안 된다는 것입니다.
CPU에서는:
CPU NUMA0 → GPU0~3
CPU NUMA1 → GPU4~7
같은 locality가 중요할 수 있지만 GPU끼리 Tensor Parallel communication을 할 때는:
GPU
│
NVLink
│
NVSwitch
│
GPU
경로가 핵심입니다.
따라서 8 GPU를 사용하는 큰 모델에서는 CPU NUMA 때문에 GPU를 억지로 4+4 별도 serving group으로 쪼개는 것보다 NVSwitch fabric 전체를 활용하는 TP/EP 구성이 더 중요할 수 있습니다.
네 모델 목록을 보면 크게 Dense / MoE / Vision/OCR로 나눠 접근하는 게 좋습니다.
vLLM도 TP는 큰 모델을 여러 GPU에 shard하는 일반적인 방식이고, MoE에는 EP를 별도로 지원합니다. vLLM
예를 들어 설계 개념은:
| workload | 우선 검토 |
|---|---|
| 작은 모델 | GPU 1 + DP replicas |
| 중형 Dense | TP=2 또는 TP=4 |
| 큰 Dense | TP=4/8 |
| 큰 MoE | EP + DP/TP |
| OCR/Vision | GPU 1~2 + DP |
| 동시 요청이 많음 | DP 증가 |
Dense model이라면:
TP=4
GPU0 ─┐
GPU1 ─┤
GPU2 ─┼── NVLink/NVSwitch
GPU3 ─┘
한 model instance
가 되고, GPU memory가 충분해서 모델 하나가 GPU 한 장에 들어가고 throughput이 중요하다면:
DP=4
GPU0 → Model replica #0
GPU1 → Model replica #1
GPU2 → Model replica #2
GPU3 → Model replica #3
▲
Load Balancer
가 더 적합합니다.
vLLM의 DP는 model weight를 replica별로 가지고 독립 request batch를 처리합니다. vLLM
DeepSeek/Qwen 계열처럼 MoE 모델이면 이야기가 달라집니다.
예를 들어 8 GPU에서:
MoE model
GPU0 ─ expert set ─┐
GPU1 ─ expert set ─┤
GPU2 ─ expert set ─┤
GPU3 ─ expert set ─┼── NVSwitch
GPU4 ─ expert set ─┤
GPU5 ─ expert set ─┤
GPU6 ─ expert set ─┤
GPU7 ─ expert set ─┘
처럼 expert를 GPU에 분산시키는 Expert Parallelism을 사용할 수 있습니다.
vLLM에서는:
--enable-expert-parallel
을 제공하고, 현재 EP 크기는 TP와 DP 구성으로 결정됩니다. vLLM
따라서 네 workload에서는 단순히
"8 GPU 있으니까 TP=8"
으로 통일하면 안 됩니다.
Dense와 MoE의 최적 parallelism이 다릅니다.
LLM inference에서 GPU HBM은 크게:
GPU HBM
───────────────────────────
Model weights
████████████████
KV cache
████████
Activation / workspace
████
CUDA/NCCL/etc
██
로 사용됩니다.
model weight를 제외하고 남는 HBM을 얼마나 KV cache에 줄 수 있는지가 동시 request 수와 context length에 큰 영향을 줍니다.
vLLM은 이를 --gpu-memory-utilization이나 더 직접적인 --kv-cache-memory-bytes로 제어할 수 있습니다. 현재 기본 gpu-memory-utilization은 0.92이고, KV cache capacity와 max concurrency도 startup log에서 확인할 수 있습니다. vLLM
처음부터 0.99 같은 공격적인 값을 쓰기보다는 실제 모델을 load해서 여유 HBM과 peak usage를 측정하는 게 좋습니다.
네가 말한 workload에는 prefix caching도 꽤 중요할 수 있습니다.
예를 들어 OCR/document workload에서 여러 요청이:
[공통 system prompt]
[공통 instruction]
[document A]
[공통 system prompt]
[공통 instruction]
[document B]
[공통 system prompt]
[공통 instruction]
[document C]
처럼 동일한 긴 prefix를 가지고 있다면:
shared prefix
│
KV cache
█████████
/ | \
/ | \
doc A doc B doc C
로 reuse할 수 있습니다.
vLLM에는 enable_prefix_caching이 KV-cache configuration으로 존재합니다. vLLM
네 시스템에는 NVMe 8개가 있지만 저는 우선:
HBM
│
├── model weights
└── KV cache ← 1순위
로 갑니다.
HBM이 부족해졌다고 바로
HBM → NVMe
를 하기보다 먼저:
HBM
│
▼
CPU RAM
offload 계층을 검토하는 편이 낫습니다.
그리고 훨씬 큰 KV capacity가 필요한 경우:
GPU HBM HOT
│
CPU RAM WARM
│
MemKV/RDMA external cache
│
NVMe capacity tier
같은 계층을 검토할 수 있습니다.
현재 vLLM에는 native CPU KV offloading과 LMCache backend가 있으며 KV offload 크기를 설정할 수 있습니다. vLLM
이 부분이 앞에서 이야기했던 MemKV 테스트와 직접 연결되는 영역입니다.
제가 현재까지 나온 조건을 합치면 목표 architecture는 이쪽입니다.
B300 Node
┌────────────────────────────────────────────────────┐
│ │
│ NUMA0 NUMA1 │
│ ───── ───── │
│ CPU cores CPU cores │
│ │ │ │
│ GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 │
│ \ | | / \ | | / │
│ └──┴───┴───────────────────────┴───┴───┘ │
│ │ │
│ NVLink/NVSwitch │
│ │
│ NIC0 400G NIC1 400G │
│ │ │ │
│ └──────── Ethernet Fabric ───────┘ │
│ │ │
│ AIStor / S3 │
│ │
│ 8 x NVMe RAID0 + XFS │
│ │ │
│ /model-cache │
│ /dataset-cache │
│ /scratch │
└────────────────────────────────────────────────────┘
그리고 Kubernetes는:
CPU Manager
static
│
├─────────────┐
│ │
Topology Manager │
restricted │
│ │
▼ ▼
CPU GPU
│ │
└──── NUMA ───┘
로 시작하겠습니다.
GPU 1~4개짜리 serving Pod와 8-GPU serving Pod를 구분해서 생각하는 것이 좋습니다.
GPU 1~4 Pod
────────────
가능하면
CPU NUMA0 + GPU0~3
또는
CPU NUMA1 + GPU4~7
→ NUMA locality 중요
GPU 8 Pod
─────────
CPU NUMA0 + NUMA1
GPU0~7
NVSwitch 전체
→ NUMA locality보다
NVLink/NVSwitch + TP/EP 최적화 중요
따라서 이 클러스터에서는 Topology Manager를 single-numa-node로 고정하기보다 restricted로 두는 것이 제가 권하는 기본안입니다.
그리고 그 위에서 Dense 모델은 TP/DP, MoE 모델은 TP/DP/EP, KV cache는 HBM 우선 + 필요 시 CPU/MemKV offload로 모델별 profile을 만드는 방식이 좋습니다.
네 모델 목록처럼 주간/야간 workload가 명확하다면, 다음 단계에서는 각 모델별로 GPU 수 / TP / DP / EP / 예상 HBM(weight) / KV-cache HBM / CPU core / NUMA / local NVMe cache를 한 장짜리 배치표로 만드는 것이 가장 유용합니다. 그 표가 나오면 “주간에는 GPU0~7을 어떻게 쪼개고, 야간에는 어떻게 재배치할지”까지 구체적으로 결정할 수 있습니다.
==
맞습니다. NIC이 특정 NUMA에 명확하게 붙지 않는 topology라면 NIC까지 억지로 NUMA affinity 대상으로 묶는 것보다 CPU↔GPU locality를 우선하는 게 좋습니다. 특히 inference 요청 트래픽 자체는 보통 model/KV 통신보다 작으므로, NIC locality는 multi-node TP/EP, remote KV cache, AIStor 대량 staging이 있을 때 더 중요해집니다.
그리고 TP / DP / EP는 “모델을 여러 GPU에 어떻게 나눠서 돌릴 것인가”를 정하는 서로 다른 축입니다.
한 모델의 tensor/weight 계산을 여러 GPU에 나눕니다.
예를 들어 모델 하나가 GPU 한 장에 안 들어가거나, 한 요청의 처리 속도를 높이고 싶다면:
하나의 Model
│
Tensor Parallel = 4
│
┌────────────┼────────────┐
▼ ▼ ▼
GPU0 GPU1 ... GPU3
Weight Weight Weight
1/4 1/4 1/4
│ │ │
└───── collective ────────┘
NVLink/NVSwitch
각 transformer layer를 계산할 때 GPU끼리 자주 통신합니다.
그래서 TP는 GPU interconnect 성능에 매우 민감합니다.
B300 8-GPU NVSwitch 시스템이면 TP에 좋은 환경입니다.
TP=1 → GPU간 TP communication 없음
TP=2 → GPU 2개가 하나의 model
TP=4 → GPU 4개
TP=8 → GPU 8개
단순히 생각하면:
TP = 모델 하나를 몇 GPU로 쪼갤 것인가
입니다.
DP는 반대로 모델을 복제합니다.
예를 들어 모델이 GPU 1장에 충분히 들어간다고 합시다.
Load Balancer
│
┌────────────┼────────────┐
▼ ▼ ▼
Request A Request B Request C
│ │ │
GPU0 GPU1 GPU2
Model A Model A Model A
replica replica replica
GPU마다 동일한 model weight가 있습니다.
그래서:
DP = 같은 모델을 몇 벌 만들어 요청을 병렬 처리할 것인가
입니다.
Serving에서 동시 요청 처리량을 늘리는 데 매우 중요합니다.
예를 들어 8 GPU에서 모델 하나가 2 GPU를 요구한다면:
TP=2
DP=4
GPU0 ─┐
├ Model replica #1
GPU1 ─┘
GPU2 ─┐
├ Model replica #2
GPU3 ─┘
GPU4 ─┐
├ Model replica #3
GPU5 ─┘
GPU6 ─┐
├ Model replica #4
GPU7 ─┘
즉 전체 GPU 수는 간단한 경우:
TP × DP
2 × 4 = 8 GPU
가 됩니다.
이 구성은 serving에서 매우 흔합니다.
EP는 MoE(Mixture of Experts) 모델에서만 특히 중요한 개념입니다.
MoE에서는 transformer 내부에 여러 expert가 있습니다.
Token
│
Router
│
┌─────────┼─────────┐
▼ ▼ ▼
Expert 0 Expert 1 ... Expert N
모든 token이 모든 expert를 계산하는 게 아니라 router가 일부 expert를 선택합니다.
예를 들어:
token A → expert 3, 17
token B → expert 8, 21
token C → expert 3, 11
처럼 됩니다.
Expert가 많으면 한 GPU에 전부 넣기 어려우므로 여러 GPU에 분산합니다.
EP=4
GPU0
├ Expert 0
├ Expert 1
└ Expert 2
GPU1
├ Expert 3
├ Expert 4
└ Expert 5
GPU2
├ Expert 6
├ Expert 7
└ Expert 8
GPU3
├ Expert 9
├ Expert 10
└ Expert 11
이게 Expert Parallelism입니다.
즉:
EP = MoE의 Expert들을 몇 GPU에 분산할 것인가
라고 생각하면 됩니다.
둘 다 model을 여러 GPU에 나누니까 비슷해 보이지만 통신 패턴이 상당히 다릅니다.
TP
────────────────────
GPU0 ←─────→ GPU1
↑ ↑
│ AllReduce │
↓ ↓
GPU2 ←─────→ GPU3
Tensor 계산 결과를
GPU들이 계속 교환
EP
────────────────────
Router
│
token dispatch
│
┌──────────┼──────────┐
▼ ▼ ▼
GPU0 GPU1 GPU2
Expert Expert Expert
All-to-All
TP는 AllReduce 계열 통신, EP는 All-to-All token dispatch가 중요합니다.
그래서 둘 다 NVLink/NVSwitch가 매우 유용하지만 EP는 특히 All-to-All network/interconnect 성능과 load balancing에 민감합니다.
예를 들어 8 GPU MoE serving을 생각해봅시다.
개념적으로:
8 GPU
┌─────────┴─────────┐
│ │
DP replica 0 DP replica 1
│ │
GPU0~3 GPU4~7
│ │
EP=4 EP=4
라면:
DP = 2
EP = 4
형태입니다.
각 replica 내부의 expert를 4 GPU에 분산하고, 그런 serving replica를 두 개 만드는 겁니다.
요청은:
Requests
│
Load Balance
/ \
▼ ▼
DP replica 0 DP replica 1
GPU0~3 GPU4~7
│ │
EP=4 EP=4
로 갑니다.
이 구조는 네 환경처럼 8 GPU B300 + NVSwitch에서 검토할 가치가 큽니다.
TP × DP × EP = GPU 수라고 이해하면 안 됩니다이건 중요한 부분입니다.
TP와 EP는 항상 독립적으로 GPU를 곱해서 사용하는 관계가 아닙니다.
예를 들어 vLLM에서 MoE expert parallel을 사용하면 TP/DP world size와 EP의 관계가 framework 내부에서 구성되므로 단순하게:
TP × DP × EP = GPU
라고 계산하면 틀릴 수 있습니다.
개념적으로는 이렇게 이해하는 게 안전합니다.
DP
│
├─ Model replica #0
│ │
│ ├─ TP : dense tensor를 GPU에 분산
│ │
│ └─ EP : MoE expert를 GPU에 분산
│
└─ Model replica #1
│
├─ TP
└─ EP
즉 DP가 replica 축, TP/EP는 replica 내부 모델 분할 축입니다.
정확한 모델 architecture와 실제 serving engine 지원 여부를 확인한 뒤 확정해야 하지만, 설계 원칙은 다음과 같습니다.
| 모델 특성 | 우선 고려 |
|---|---|
| Dense LLM | TP + DP |
| MoE LLM | EP + DP, 필요하면 TP |
| 작은 OCR/VLM | GPU 1개 + DP |
| 큰 VLM | TP + DP |
| GPU 한 장에 충분히 들어감 | TP=1, DP 증가 |
| GPU HBM보다 weight가 큼 | TP/EP 증가 |
| latency가 중요 | 적절한 TP |
| 전체 throughput이 중요 | DP 증가 |
예를 들어 야간에 8 GPU를 GLM 하나가 독점한다고 해서 무조건 TP=8로 하는 것보다는,
Case A
TP=8
DP=1
GPU0~7
│
Model 1개
→ 큰 모델
→ 단일 요청 latency
→ NVSwitch 활용
와
Case B
TP=4
DP=2
GPU0~3 GPU4~7
│ │
Replica 0 Replica 1
→ 동시 요청 throughput ↑
를 비교해야 합니다.
모델이 MoE라면:
Case C
DP=2
┌───────────────────┐
│ │
GPU0~3 GPU4~7
│ │
EP group EP group
│ │
Replica 0 Replica 1
도 강력한 후보가 됩니다.
재미있는 부분이 바로 여기입니다.
서버 topology가 대략:
NUMA0 NUMA1
───── ─────
CPU0 CPU1
│ │
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
\ /
└──── NVSwitch ───────┘
라고 하면 TP=4, DP=2 구성은 NUMA와 매우 예쁘게 맞출 수 있습니다.
NUMA0 NUMA1
CPU cores CPU cores
│ │
GPU0~3 GPU4~7
│ │
TP=4 TP=4
│ │
DP replica #0 DP replica #1
이 경우 K8s CPU Manager + Topology Manager가 상당히 효과적입니다.
반면:
TP=8
DP=1
이면:
하나의 model
GPU0 1 2 3 │ GPU4 5 6 7
NUMA0 │ NUMA1
\ /
NVSwitch
이므로 single-NUMA를 고집할 이유가 없습니다. GPU-GPU 통신은 NVSwitch에 맡기고 CPU thread만 가능한 한 각 GPU/NCCL processing과 가까운 CPU에 배치하는 방향이 됩니다.
이건 실제 topology를 다시 확인하는 게 좋습니다. Linux에서:
cat /sys/class/net/<NIC>/device/numa_node
lspci -vv -s <NIC PCI address>
lspci -tv
numactl --hardware
nvidia-smi topo -m
을 같이 보세요.
numa_node = -1이 나오는 경우도 있습니다. 이걸 단순히 "NIC가 NUMA와 무관하다"라고 해석하면 안 됩니다. firmware/PCIe topology 또는 NUMA 정보 노출 방식 때문에 kernel이 특정 NUMA node를 지정하지 못하는 경우도 있기 때문입니다.
특히 nvidia-smi topo -m의 NIC/GPU affinity와 PCIe tree를 같이 보는 게 중요합니다.
네 환경에서 NIC가 정말 양 NUMA 사이에 애매하게 위치한다면 저는 우선순위를:
1. GPU ↔ GPU
NVLink/NVSwitch topology
↓
2. CPU ↔ GPU
NUMA affinity
↓
3. CPU ↔ NIC / GPU ↔ NIC
PCIe locality
↓
4. NVMe NUMA locality
정도로 두겠습니다.
다만 multi-node inference/training이나 remote KV cache를 NDR/RDMA로 붙이면 3번의 우선순위가 확 올라갑니다. 그때는 GPU ↔ NIC의 GPUDirect RDMA 경로까지 같이 봐야 합니다.
지금 구조에서는 결국 nvidia-smi topo -m 실제 출력이 가장 중요합니다. 그 결과를 주시면 GPU0~7 / NIC / CPU NUMA / NVLink를 실제 topology에 대입해서 각 모델의 TP/DP/EP 배치와 K8s CPU pinning을 표로 구체화할 수 있습니다.
===
좋습니다. B300 8-GPU/node + local NVMe 8개 + AIStor + K8s + vLLM 계열 serving을 기준으로 잡겠습니다. 아래 숫자는 운영 확정값이 아니라 1차 배치/PoC baseline입니다. 특히 GLM-5.3과 Docling 일부 모델은 정확한 checkpoint/precision을 확인한 뒤 조정해야 합니다.
| 시간 | 모델 | 성격 | GPU | TP | DP | EP | 예상 Weight HBM* | KV-cache HBM* | CPU core | NUMA | Local NVMe cache |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 🌙 야간 | GLM-5.3 | 대형 MoE | 4 | 1~2 | 1 | 4 | checkpoint 확인 필요 | GPU당 20~40GiB부터 | 32~48 | NUMA0 우선 | 0.8~1.5TB+ 예상, 확인 필요 |
| 🌙 야간 | Qwen3.6-35B-A3B | 35B/3B-active MoE | 2 | 1 | 1 | 2 | BF16 약 65GiB total | GPU당 20~40GiB | 16~24 | NUMA1 | 80~120GB |
| 🌙 야간 | gpt-oss-20b | 21B/3.6B-active MoE | 1 | 1 | 1 | 1 | MXFP4 약 13~16GiB급 | 20~40GiB | 8~16 | NUMA1 | 20~40GB |
| ☀️ 주간 | DeepSeek-V4.1-Flash | 552B/8→16B-active MoE | 4 | 1~2 | 1 | 4 | precision 따라 수백 GiB | GPU당 20~40GiB부터 | 32~48 | NUMA0 | 0.6~1.2TB+ |
| ☀️ 주간 | GLM-5.3-Flash | 320B/18B-active MoE | 4 | 1~2 | 1 | 4 | BF16이면 약 600GiB급 raw | GPU당 20~40GiB부터 | 32~48 | NUMA1 | 0.4~0.7TB+ |
| ☀️ 주간 | PaddleOCR-VL-1.6 | ~0.9B VLM | 1 | 1 | 2+ 가능 | - | 약 1.5~2GiB | 2~8GiB | 8 | 어느 쪽도 OK | 5~10GB |
| ☀️ 주간 | Docling layout/heron | document/layout | 0~1 | 1 | N | - | 작음 | 거의 없음 | 4~8 | 자유 | <5GB |
| ☀️ 주간 | Docling models | document pipeline | 0~1 | 1 | N | - | 모델별 상이 | 거의 없음 | 4~8 | 자유 | <10GB |
| ☀️ 주간 | DocumentFigureClassifier | image classifier | CPU 권장 | - | - | - | - | - | 2~4 | 자유 | <1GB |
* Weight HBM은 precision/quantization/serving engine에 따라 크게 달라집니다. KV-cache HBM은 모델 고정값이 아니라 동시성, max context, KV dtype 등에 따라 우리가 할당하는 budget으로 보는 게 맞습니다.
Qwen3.6-35B-A3B는 약 35B total / 3B active인 MoE로 알려져 있고, gpt-oss-20b는 공식적으로 21B total / 3.6B active, 32 experts 중 token당 4 experts를 사용합니다. GitHub
DeepSeek-V4.1-Flash는 공식 발표 기준 552B MoE이며 input 8B/output 16B active, GLM-5.3-Flash는 NVIDIA 자료 기준 320B total/18B active, 288 routed experts/top-8입니다. DeepSeek
B300 8개에 이 모델을 전부 동시에 올린다는 의미는 아닙니다.
특히 주간에는:
DeepSeek-V4.1-Flash GPU 4
GLM-5.3-Flash GPU 4
─────────────────────────────
8 GPU
OCR / Docling
→ 별도 small GPU / CPU worker
가 1차 후보입니다.
하지만 이것도 실제 checkpoint precision에 따라 4 GPU에 weight가 들어가는지 검증해야 합니다.
예를 들어 GLM-5.3-Flash 320B를 BF16 그대로 올리면 weight만 단순 계산으로 약:
320B × 2 byte
≈ 640 GB
입니다.
반면 FP8이면 대략 절반 수준으로 내려갑니다. 따라서 B300 HBM 용량 × 4와 framework overhead/KV cache를 같이 계산해야 합니다.
GLM-5.3-Flash가 320B라고 해서 active parameter 18B만 HBM에 올리면 되는 것은 아닙니다. MoE의 전체 expert weight 저장 공간이 필요하기 때문입니다. NVIDIA NGC
B300 8-GPU NVSwitch node라고 하면:
B300 Node
8 × GPU / NVSwitch
NUMA0 NUMA1
──────────────────── ────────────────────
CPU 32~48 cores CPU 32~48 cores
GPU0 ─┐ GPU4 ─┐
GPU1 ─┤ GPU5 ─┤
GPU2 ─┤ DeepSeek V4.1 Flash GPU6 ─┤ GLM-5.3-Flash
GPU3 ─┘ GPU7 ─┘
EP/TP group EP/TP group
\ /
└──────── NVSwitch ──────────┘
즉 NUMA가 실제로 GPU 0~3 / 4~7 식으로 대응한다면 4-GPU serving group을 NUMA 경계와 맞추는 것입니다.
이게 앞에서 얘기했던 4+4 NVMe RAID0와는 다릅니다.
NVMe는 여전히:
NVMe0
NVMe1
NVMe2
NVMe3
NVMe4 ─── RAID0 ─── XFS ─── /model-cache
NVMe5
NVMe6
NVMe7
8개 하나로 유지하는 것을 권합니다.
야간에는 모델 크기 차이가 큽니다.
B300 Node
NUMA0 NUMA1
──────────────────── ───────────────────
GPU0 ─┐
GPU1 ─┤
GPU2 ─┤ GLM-5.3
GPU3 ─┘
EP=4 후보
GPU4 ─┐
GPU5 ─┘ Qwen3.6
EP=2
GPU6 ─ gpt-oss-20b
GPU7 ─ spare /
DP replica /
KV capacity
여기서 GPU7을 무조건 채울 필요가 없습니다.
오히려 측정 결과 Qwen request가 많다면:
GPU4-5 → Qwen replica #1
GPU6-7 → Qwen replica #2
TP/EP group × 2
DP = 2
로 만드는 것이 serving throughput 면에서는 훨씬 유리할 수 있습니다.
반대로 gpt-oss 요청량이 많다면 GPU6/7을 gpt-oss replica로 두는 식입니다.
즉 DP는 모델 크기가 아니라 실제 traffic 비율을 보고 결정합니다.
예를 들어 GPU당 HBM을 개념적으로 이렇게 사용합니다.
B300 GPU HBM
┌─────────────────────────────┐
│ │
│ Model Weight Shard │
│ █████████████████████ │
│ │
├─────────────────────────────┤
│ KV Cache │
│ █████████ │
│ │
├─────────────────────────────┤
│ activation/workspace │
│ ████ │
│ │
├─────────────────────────────┤
│ CUDA / NCCL / fragmentation │
│ ██ │
└─────────────────────────────┘
그래서 실제 계산 순서는:
GPU HBM
-
weight shard
-
CUDA/NCCL/workspace
-
safety margin
────────────────
KV cache budget
입니다.
처음부터 KV cache를 80GB 같은 값으로 박아놓는 것보다 20~40GiB/GPU 정도의 보수적인 baseline으로 시작해서 concurrency/context test를 하면서 늘리는 방식을 권합니다.
특히 DeepSeek-V4.1-Flash는 이전 세대 대비 KV cache를 크게 줄인 architecture라고 공식 발표에서 설명합니다. DeepSeek GLM-5.3-Flash 역시 GLM-5.3 대비 KV cache가 약 4.4배 감소했다고 공개되어 있어서 long-context serving에서 상당히 중요한 차이입니다. GitHub
PaddleOCR-VL-1.6은 community 측정 기준 weight가 BF16에서 약 1.82 GiB 정도인 작은 모델입니다. Hugging Face
그래서:
GPU0~3
DeepSeek
GPU4~7
GLM
↑
│ GPU resource 경쟁시키지 말고
CPU worker / 별도 small GPU
│
├─ Docling layout
├─ Figure classifier
└─ preprocessing
방향이 좋습니다.
특히 DocumentFigureClassifier는 checkpoint 자체가 수십 MB 수준이라 B300 한 장을 점유시키는 것은 낭비입니다. 공개된 v2.5 모델 파일도 safetensors 약 16.4MB, ONNX 약 16.9MB 수준입니다. Hugging Face
K8s에서는 대략:
Pod: DeepSeek
requests:
cpu: 40
memory: ...
nvidia.com/gpu: 4
limits:
cpu: 40
memory: ...
nvidia.com/gpu: 4
로 Guaranteed QoS + exclusive CPU를 만들고,
kubelet
CPU Manager
static
Topology Manager
restricted
로 두겠습니다.
목표는:
NUMA0
CPU 0~39
│
├──────── GPU0
├──────── GPU1
├──────── GPU2
└──────── GPU3
DeepSeek Pod
입니다.
반대쪽은:
NUMA1
CPU 64~103
│
├──────── GPU4
├──────── GPU5
├──────── GPU6
└──────── GPU7
GLM Pod
가 됩니다.
CPU core 수 32~48은 시작점이지 필수값이 아닙니다. tokenizer, multimodal preprocessing, request handling을 많이 하면 CPU 요구량이 올라갑니다.
그래서 앞에서 질문했던 NVMe 설계와 연결하면:
AIStor
│
S3 / TCP
│
▼
8 × NVMe RAID0
│
XFS
│
/model-cache
│
┌──────────────┼──────────────┐
│ │ │
deepseek/ glm/ qwen/
│ │ │
├──────────────┼──────────────┤
│ │ │
gpt-oss/ paddle/ docling/
처럼 하고 NUMA별 filesystem partition은 하지 않겠습니다.
그리고 주/야간 모델이 정해져 있으므로 cache eviction도 일반 LRU보다 훨씬 똑똑하게 할 수 있습니다.
21:00
AIStor
│
├── GLM-5.3
├── Qwen3.6
└── gpt-oss
│
▼
/model-cache prefetch
↓
22:00
야간 serving 전환
그리고 아침에는:
07:00
AIStor
│
├── DeepSeek V4.1 Flash
├── GLM-5.3 Flash
├── PaddleOCR
└── Docling
│
▼
/model-cache prefetch
↓
08:00
주간 serving 전환
이렇게 하면 AIStor→NVMe 다운로드가 serving startup critical path에서 빠집니다.
Storage
──────────────────────────────
8 NVMe RAID0 + XFS
/model-cache 하나
Kubernetes
──────────────────────────────
CPU Manager = static
Topology Manager= restricted
Guaranteed QoS
GPU allocation
──────────────────────────────
가능하면 4-GPU group을
CPU NUMA boundary와 align
GPU communication
──────────────────────────────
TP → NVLink/NVSwitch
EP → NVLink/NVSwitch
DP → 독립 replica
KV
──────────────────────────────
1. GPU HBM
2. 필요시 CPU RAM
3. 필요시 MemKV/RDMA
4. NVMe는 마지막 capacity tier
Model source
──────────────────────────────
AIStor = authoritative
NVMe = local model cache
HBM = serving hot tier
다만 GLM-5.3의 정확한 checkpoint와 사용 precision(BF16/FP8/INT8 등)이 정해져야 위 표의 가장 중요한 두 숫자인 GPU 수와 Weight HBM을 확정할 수 있습니다. 그 두 정보까지 확보되면 위 표를 단순 가이드가 아니라 실제 B300 8-GPU 노드의 GPU0~7 배치표 + 주간→야간 전환표 수준으로 확정할 수 있습니다.
===
좋습니다. B300 8-GPU/node + local NVMe 8개 + AIStor + K8s + vLLM 계열 serving을 기준으로 잡겠습니다. 아래 숫자는 운영 확정값이 아니라 1차 배치/PoC baseline입니다. 특히 GLM-5.3과 Docling 일부 모델은 정확한 checkpoint/precision을 확인한 뒤 조정해야 합니다.
| 시간 | 모델 | 성격 | GPU | TP | DP | EP | 예상 Weight HBM* | KV-cache HBM* | CPU core | NUMA | Local NVMe cache |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 🌙 야간 | GLM-5.3 | 대형 MoE | 4 | 1~2 | 1 | 4 | checkpoint 확인 필요 | GPU당 20~40GiB부터 | 32~48 | NUMA0 우선 | 0.8~1.5TB+ 예상, 확인 필요 |
| 🌙 야간 | Qwen3.6-35B-A3B | 35B/3B-active MoE | 2 | 1 | 1 | 2 | BF16 약 65GiB total | GPU당 20~40GiB | 16~24 | NUMA1 | 80~120GB |
| 🌙 야간 | gpt-oss-20b | 21B/3.6B-active MoE | 1 | 1 | 1 | 1 | MXFP4 약 13~16GiB급 | 20~40GiB | 8~16 | NUMA1 | 20~40GB |
| ☀️ 주간 | DeepSeek-V4.1-Flash | 552B/8→16B-active MoE | 4 | 1~2 | 1 | 4 | precision 따라 수백 GiB | GPU당 20~40GiB부터 | 32~48 | NUMA0 | 0.6~1.2TB+ |
| ☀️ 주간 | GLM-5.3-Flash | 320B/18B-active MoE | 4 | 1~2 | 1 | 4 | BF16이면 약 600GiB급 raw | GPU당 20~40GiB부터 | 32~48 | NUMA1 | 0.4~0.7TB+ |
| ☀️ 주간 | PaddleOCR-VL-1.6 | ~0.9B VLM | 1 | 1 | 2+ 가능 | - | 약 1.5~2GiB | 2~8GiB | 8 | 어느 쪽도 OK | 5~10GB |
| ☀️ 주간 | Docling layout/heron | document/layout | 0~1 | 1 | N | - | 작음 | 거의 없음 | 4~8 | 자유 | <5GB |
| ☀️ 주간 | Docling models | document pipeline | 0~1 | 1 | N | - | 모델별 상이 | 거의 없음 | 4~8 | 자유 | <10GB |
| ☀️ 주간 | DocumentFigureClassifier | image classifier | CPU 권장 | - | - | - | - | - | 2~4 | 자유 | <1GB |
* Weight HBM은 precision/quantization/serving engine에 따라 크게 달라집니다. KV-cache HBM은 모델 고정값이 아니라 동시성, max context, KV dtype 등에 따라 우리가 할당하는 budget으로 보는 게 맞습니다.
Qwen3.6-35B-A3B는 약 35B total / 3B active인 MoE로 알려져 있고, gpt-oss-20b는 공식적으로 21B total / 3.6B active, 32 experts 중 token당 4 experts를 사용합니다. GitHub
DeepSeek-V4.1-Flash는 공식 발표 기준 552B MoE이며 input 8B/output 16B active, GLM-5.3-Flash는 NVIDIA 자료 기준 320B total/18B active, 288 routed experts/top-8입니다. DeepSeek
B300 8개에 이 모델을 전부 동시에 올린다는 의미는 아닙니다.
특히 주간에는:
DeepSeek-V4.1-Flash GPU 4
GLM-5.3-Flash GPU 4
─────────────────────────────
8 GPU
OCR / Docling
→ 별도 small GPU / CPU worker
가 1차 후보입니다.
하지만 이것도 실제 checkpoint precision에 따라 4 GPU에 weight가 들어가는지 검증해야 합니다.
예를 들어 GLM-5.3-Flash 320B를 BF16 그대로 올리면 weight만 단순 계산으로 약:
320B × 2 byte
≈ 640 GB
입니다.
반면 FP8이면 대략 절반 수준으로 내려갑니다. 따라서 B300 HBM 용량 × 4와 framework overhead/KV cache를 같이 계산해야 합니다.
GLM-5.3-Flash가 320B라고 해서 active parameter 18B만 HBM에 올리면 되는 것은 아닙니다. MoE의 전체 expert weight 저장 공간이 필요하기 때문입니다. NVIDIA NGC
B300 8-GPU NVSwitch node라고 하면:
B300 Node
8 × GPU / NVSwitch
NUMA0 NUMA1
──────────────────── ────────────────────
CPU 32~48 cores CPU 32~48 cores
GPU0 ─┐ GPU4 ─┐
GPU1 ─┤ GPU5 ─┤
GPU2 ─┤ DeepSeek V4.1 Flash GPU6 ─┤ GLM-5.3-Flash
GPU3 ─┘ GPU7 ─┘
EP/TP group EP/TP group
\ /
└──────── NVSwitch ──────────┘
즉 NUMA가 실제로 GPU 0~3 / 4~7 식으로 대응한다면 4-GPU serving group을 NUMA 경계와 맞추는 것입니다.
이게 앞에서 얘기했던 4+4 NVMe RAID0와는 다릅니다.
NVMe는 여전히:
NVMe0
NVMe1
NVMe2
NVMe3
NVMe4 ─── RAID0 ─── XFS ─── /model-cache
NVMe5
NVMe6
NVMe7
8개 하나로 유지하는 것을 권합니다.
야간에는 모델 크기 차이가 큽니다.
B300 Node
NUMA0 NUMA1
──────────────────── ───────────────────
GPU0 ─┐
GPU1 ─┤
GPU2 ─┤ GLM-5.3
GPU3 ─┘
EP=4 후보
GPU4 ─┐
GPU5 ─┘ Qwen3.6
EP=2
GPU6 ─ gpt-oss-20b
GPU7 ─ spare /
DP replica /
KV capacity
여기서 GPU7을 무조건 채울 필요가 없습니다.
오히려 측정 결과 Qwen request가 많다면:
GPU4-5 → Qwen replica #1
GPU6-7 → Qwen replica #2
TP/EP group × 2
DP = 2
로 만드는 것이 serving throughput 면에서는 훨씬 유리할 수 있습니다.
반대로 gpt-oss 요청량이 많다면 GPU6/7을 gpt-oss replica로 두는 식입니다.
즉 DP는 모델 크기가 아니라 실제 traffic 비율을 보고 결정합니다.
예를 들어 GPU당 HBM을 개념적으로 이렇게 사용합니다.
B300 GPU HBM
┌─────────────────────────────┐
│ │
│ Model Weight Shard │
│ █████████████████████ │
│ │
├─────────────────────────────┤
│ KV Cache │
│ █████████ │
│ │
├─────────────────────────────┤
│ activation/workspace │
│ ████ │
│ │
├─────────────────────────────┤
│ CUDA / NCCL / fragmentation │
│ ██ │
└─────────────────────────────┘
그래서 실제 계산 순서는:
GPU HBM
-
weight shard
-
CUDA/NCCL/workspace
-
safety margin
────────────────
KV cache budget
입니다.
처음부터 KV cache를 80GB 같은 값으로 박아놓는 것보다 20~40GiB/GPU 정도의 보수적인 baseline으로 시작해서 concurrency/context test를 하면서 늘리는 방식을 권합니다.
특히 DeepSeek-V4.1-Flash는 이전 세대 대비 KV cache를 크게 줄인 architecture라고 공식 발표에서 설명합니다. DeepSeek GLM-5.3-Flash 역시 GLM-5.3 대비 KV cache가 약 4.4배 감소했다고 공개되어 있어서 long-context serving에서 상당히 중요한 차이입니다. GitHub
PaddleOCR-VL-1.6은 community 측정 기준 weight가 BF16에서 약 1.82 GiB 정도인 작은 모델입니다. Hugging Face
그래서:
GPU0~3
DeepSeek
GPU4~7
GLM
↑
│ GPU resource 경쟁시키지 말고
CPU worker / 별도 small GPU
│
├─ Docling layout
├─ Figure classifier
└─ preprocessing
방향이 좋습니다.
특히 DocumentFigureClassifier는 checkpoint 자체가 수십 MB 수준이라 B300 한 장을 점유시키는 것은 낭비입니다. 공개된 v2.5 모델 파일도 safetensors 약 16.4MB, ONNX 약 16.9MB 수준입니다. Hugging Face
K8s에서는 대략:
Pod: DeepSeek
requests:
cpu: 40
memory: ...
nvidia.com/gpu: 4
limits:
cpu: 40
memory: ...
nvidia.com/gpu: 4
로 Guaranteed QoS + exclusive CPU를 만들고,
kubelet
CPU Manager
static
Topology Manager
restricted
로 두겠습니다.
목표는:
NUMA0
CPU 0~39
│
├──────── GPU0
├──────── GPU1
├──────── GPU2
└──────── GPU3
DeepSeek Pod
입니다.
반대쪽은:
NUMA1
CPU 64~103
│
├──────── GPU4
├──────── GPU5
├──────── GPU6
└──────── GPU7
GLM Pod
가 됩니다.
CPU core 수 32~48은 시작점이지 필수값이 아닙니다. tokenizer, multimodal preprocessing, request handling을 많이 하면 CPU 요구량이 올라갑니다.
그래서 앞에서 질문했던 NVMe 설계와 연결하면:
AIStor
│
S3 / TCP
│
▼
8 × NVMe RAID0
│
XFS
│
/model-cache
│
┌──────────────┼──────────────┐
│ │ │
deepseek/ glm/ qwen/
│ │ │
├──────────────┼──────────────┤
│ │ │
gpt-oss/ paddle/ docling/
처럼 하고 NUMA별 filesystem partition은 하지 않겠습니다.
그리고 주/야간 모델이 정해져 있으므로 cache eviction도 일반 LRU보다 훨씬 똑똑하게 할 수 있습니다.
21:00
AIStor
│
├── GLM-5.3
├── Qwen3.6
└── gpt-oss
│
▼
/model-cache prefetch
↓
22:00
야간 serving 전환
그리고 아침에는:
07:00
AIStor
│
├── DeepSeek V4.1 Flash
├── GLM-5.3 Flash
├── PaddleOCR
└── Docling
│
▼
/model-cache prefetch
↓
08:00
주간 serving 전환
이렇게 하면 AIStor→NVMe 다운로드가 serving startup critical path에서 빠집니다.
Storage
──────────────────────────────
8 NVMe RAID0 + XFS
/model-cache 하나
Kubernetes
──────────────────────────────
CPU Manager = static
Topology Manager= restricted
Guaranteed QoS
GPU allocation
──────────────────────────────
가능하면 4-GPU group을
CPU NUMA boundary와 align
GPU communication
──────────────────────────────
TP → NVLink/NVSwitch
EP → NVLink/NVSwitch
DP → 독립 replica
KV
──────────────────────────────
1. GPU HBM
2. 필요시 CPU RAM
3. 필요시 MemKV/RDMA
4. NVMe는 마지막 capacity tier
Model source
──────────────────────────────
AIStor = authoritative
NVMe = local model cache
HBM = serving hot tier
다만 GLM-5.3의 정확한 checkpoint와 사용 precision(BF16/FP8/INT8 등)이 정해져야 위 표의 가장 중요한 두 숫자인 GPU 수와 Weight HBM을 확정할 수 있습니다. 그 두 정보까지 확보되면 위 표를 단순 가이드가 아니라 실제 B300 8-GPU 노드의 GPU0~7 배치표 + 주간→야간 전환표 수준으로 확정할 수 있습니다.
===
네 환경에서 이 두 설정은 “LLM serving Pod에 CPU core를 전용으로 주고, 그 CPU를 가능하면 GPU와 가까운 NUMA에 배치한다”고 이해하면 됩니다.
kubelet
├─ CPU Manager
│ └─ static
│
└─ Topology Manager
└─ restricted
둘은 역할이 다릅니다. CPU Manager는 “어떤 CPU를 줄 것인가”, Topology Manager는 “CPU/GPU 같은 리소스를 topology 관점에서 같이 배치해도 되는가”를 담당합니다.
staticKubernetes 기본 CPU 할당은 CPU core를 Pod에 전용으로 고정하는 방식이 아닙니다. 여러 Pod가 Linux scheduler를 통해 CPU를 공유할 수 있습니다.
CPU Manager를:
cpuManagerPolicy: static
으로 설정하면 특정 조건을 만족하는 Pod에 exclusive CPU set을 할당할 수 있습니다.
예를 들어 B300 노드가:
NUMA 0 NUMA 1
CPU 0~63 CPU 64~127
GPU 0~3 GPU 4~7
이고 LLM Pod가:
resources:
requests:
cpu: "32"
memory: "128Gi"
nvidia.com/gpu: 4
limits:
cpu: "32"
memory: "128Gi"
nvidia.com/gpu: 4
처럼 CPU를 integer로 요청하고 request == limit인 Guaranteed Pod라면 static CPU Manager가 전용 CPU를 배정할 수 있습니다.
예를 들어:
DeepSeek Pod
│
├─ GPU0
├─ GPU1
├─ GPU2
├─ GPU3
│
└─ CPU 8~39
↑
exclusive
이 CPU들은 다른 일반 workload와 공유하는 CPU pool과 분리됩니다.
이게 inference에서 좋은 이유는 CPU scheduling jitter를 줄일 수 있기 때문입니다. Tokenization, request handling, CUDA launch, NCCL thread, preprocessing 등이 CPU를 사용하므로 CPU contention이 tail latency에 영향을 줄 수 있습니다.
CPU Manager가:
CPU 32개 주세요.
→ OK, 전용 CPU 32개 할당
까지는 할 수 있지만 우리가 원하는 것은 단순히 CPU 32개가 아닙니다.
예를 들어:
NUMA0 NUMA1
CPU0~63 CPU64~127
│ │
GPU0~3 GPU4~7
에서 GPU0~3을 받은 Pod에:
GPU0~3 → NUMA0
CPU80~111 → NUMA1
이렇게 CPU를 주면 좋지 않습니다.
CPU가 GPU와 통신하면서 NUMA/PCIe topology를 넘어가게 될 수 있기 때문입니다.
우리가 원하는 것은:
NUMA0
CPU8~39
│
├─ GPU0
├─ GPU1
├─ GPU2
└─ GPU3
DeepSeek Pod
입니다.
여기서 Topology Manager가 등장합니다.
Topology Manager는 여러 resource manager/device plugin이 제공하는 Topology Hint를 모아서 서로 잘 맞는 배치를 선택합니다.
개념적으로:
CPU Manager
│
└─ "CPU 32개를 NUMA0에서 줄 수 있어"
GPU Device Plugin
│
└─ "GPU0~3은 NUMA0 쪽이야"
기타 Device Plugin
│
└─ "이 device는 NUMA0 쪽이야"
│
▼
Topology Manager
│
▼
NUMA0 선택
즉 Topology Manager 자체가 CPU나 GPU를 관리하는 것은 아닙니다.
각 manager/device plugin의 topology 정보를 조정하는 coordinator에 가깝습니다.
restricted는 무슨 뜻인가?Topology Manager에는 여러 policy가 있습니다.
none
best-effort
restricted
single-numa-node
네 환경에서 중요한 것은 뒤의 세 개입니다.
best-effort는:
가능하면 NUMA를 맞춰봐. 안 되면 그냥 Pod 실행해.
입니다.
예를 들어:
GPU0~3 → NUMA0
그런데 NUMA0에
free CPU가 16개밖에 없음
Pod 요구:
CPU 32
이면 완벽한 alignment가 불가능할 수 있습니다.
best-effort는 그래도 Pod를 실행할 수 있습니다.
NUMA0 CPU 16
+
NUMA1 CPU 16
GPU0~3
즉 성능이 최적이 아니어도 가용성을 우선합니다.
restrictedrestricted는 훨씬 엄격합니다.
Resource manager들이 acceptable하다고 판단하는 topology alignment를 만들 수 없으면 Pod를 admit하지 않는다.
즉:
GPU0~3 = NUMA0
CPU 32개 필요
│
▼
NUMA0에 적절한 CPU resource 있음
│
YES
│
▼
Pod 실행
반대로 조건을 만족하지 못하면:
Pod
│
│ CPU/GPU topology
│ alignment 불가능
▼
Topology Manager
│
└── Reject
가 될 수 있습니다.
그래서 AI inference처럼 성능 예측 가능성이 중요한 workload에서 유용합니다.
single-numa-node가 더 좋은 것 아닌가?더 엄격합니다.
topologyManagerPolicy: single-numa-node
는 말 그대로 모든 NUMA-aware resource를 하나의 NUMA node로 제한하려는 정책입니다.
예를 들어:
NUMA0
CPU 32
GPU0~3
→ OK
하지만 네 B300 node에서:
8 GPU Pod
NUMA0 NUMA1
GPU0~3 GPU4~7
\ /
└──── NVSwitch ────┘
처럼 8 GPU 전체를 사용하는 모델은 하나의 NUMA node에 넣을 수 없습니다.
그래서 문제가 됩니다.
네 workload는:
2 GPU model
4 GPU model
8 GPU model
이 섞일 가능성이 있으므로 제가 restricted를 baseline으로 추천했던 이유가 이것입니다.
restricted라고 해서 반드시 "한 NUMA"라는 뜻은 아니다이 부분이 상당히 중요합니다.
restricted를:
무조건 CPU/GPU를 한 NUMA에 넣는다.
라고 이해하면 안 됩니다.
그건 single-numa-node에 더 가까운 개념입니다.
restricted의 핵심은:
각 resource provider의 topology hint를 종합했을 때 허용 가능한 topology alignment인가?
입니다.
그래서 8 GPU Pod처럼 본질적으로:
NUMA0 + NUMA1
을 사용하는 workload를 설계할 여지가 있습니다.
topologyManagerScope: pod도 중요LLM serving에서는 저는 같이 검토하겠습니다.
topologyManagerScope: pod
기본 scope와 차이가 있습니다.
Pod 안에 container가 여러 개 있다고 합시다.
LLM Pod
├─ vLLM container
│ ├ GPU x4
│ └ CPU 30
│
└─ sidecar
└ CPU 2
container scope라면 각각의 container topology를 독립적으로 볼 수 있습니다.
반면 pod scope는 Pod 전체 resource를 하나의 topology 단위로 조정합니다.
LLM serving처럼 Pod 전체가 하나의 serving unit이라면 pod scope를 검토할 가치가 큽니다.
노드의 kubelet config에서 개념적으로:
cpuManagerPolicy: static
topologyManagerPolicy: restricted
topologyManagerScope: pod
가 됩니다.
그리고 LLM Pod는:
resources:
requests:
cpu: "32"
memory: "256Gi"
nvidia.com/gpu: 4
limits:
cpu: "32"
memory: "256Gi"
nvidia.com/gpu: 4
처럼 합니다.
관계를 그림으로 보면:
Kubernetes Scheduler
│
▼
B300 Node
│
kubelet
│
┌─────────┴─────────┐
│ │
CPU Manager Topology Manager
static restricted
│ │
│ topology hints 조정
│ │
└──────────┬────────┘
│
▼
┌──────────────────┐
│ LLM Serving │
│ Pod │
└────────┬─────────┘
│
┌─────────┴────────┐
│ │
CPU 32 GPU x4
exclusive │
│ │
└──── NUMA0 ───────┘
현재까지 이야기한 B300 node라면 저는 먼저 실제 topology를:
lscpu -e=CPU,NODE,SOCKET,CORE
numactl --hardware
nvidia-smi topo -m
cat /sys/class/net/<NIC>/device/numa_node
로 확인한 다음,
CPU Manager = static
Topology Manager = restricted
Topology Scope = pod
를 1차 PoC 설정으로 잡겠습니다.
그 다음 TP=4/DP=2처럼 4-GPU replica 두 개를 만드는 workload에서는 가능하면 각 replica의 CPU+GPU를 NUMA 경계에 맞추고, TP=8 또는 EP=8처럼 8 GPU 전체를 쓰는 모델에서는 NUMA를 억지로 하나로 제한하지 않고 NVLink/NVSwitch 전체를 활용하도록 합니다.
즉 이 세 설정의 목적은 GPU를 빠르게 만드는 것 자체라기보다, CPU/GPU 배치를 우연에 맡기지 않고 topology를 고려해 예측 가능한 inference 성능을 만드는 것이라고 보면 정확합니다.
===