26O06b2

QK·4일 전

가능합니다. 다만 여기서는 세 층을 분리해서 설계하는 게 좋습니다.

  1. K8s: Pod가 CPU/GPU를 같은 NUMA에 받도록 한다.
  2. NVIDIA/NCCL: 한 모델에 여러 GPU를 쓸 때 NVLink/NVSwitch를 최대한 사용한다.
  3. vLLM: TP/DP/EP와 KV cache를 workload 특성에 맞춘다.

특히 B300 8-GPU 노드라면 “무조건 single-numa-node”가 정답은 아닙니다. 8-GPU 모델은 양쪽 NUMA를 사용해야 하기 때문입니다.

1. K8s에서 CPU ↔ GPU affinity

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를 기본으로 두는 게 더 현실적입니다.


2. NIC locality는 Topology Manager만으로 끝나지 않는다

여기가 중요한 부분입니다.

일반적인 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를 할 때 훨씬 중요해집니다.


3. GPU topology는 조금 다르게 생각해야 합니다

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 구성이 더 중요할 수 있습니다.


4. 그래서 모델별 GPU 구성부터 정해야 한다

네 모델 목록을 보면 크게 Dense / MoE / Vision/OCR로 나눠 접근하는 게 좋습니다.

vLLM도 TP는 큰 모델을 여러 GPU에 shard하는 일반적인 방식이고, MoE에는 EP를 별도로 지원합니다. vLLM

예를 들어 설계 개념은:

workload우선 검토
작은 모델GPU 1 + DP replicas
중형 DenseTP=2 또는 TP=4
큰 DenseTP=4/8
큰 MoEEP + DP/TP
OCR/VisionGPU 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


5. MoE는 특히 EP를 검토해야 한다

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


6. KV cache가 실제 serving 성능에서 매우 중요

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를 측정하는 게 좋습니다.


7. Prefix caching은 적극 검토

네가 말한 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


8. KV cache를 NVMe에 바로 내리는 것은 1순위가 아니다

네 시스템에는 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 테스트와 직접 연결되는 영역입니다.


9. 네 환경에서는 전체를 이렇게 맞추는 게 좋다

제가 현재까지 나온 조건을 합치면 목표 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에 어떻게 나눠서 돌릴 것인가”를 정하는 서로 다른 축입니다.

TP — Tensor Parallelism

한 모델의 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 — Data Parallelism

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 — Expert Parallelism

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에 분산할 것인가

라고 생각하면 됩니다.


TP와 EP의 결정적인 차이

둘 다 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에 민감합니다.


TP + DP + EP를 동시에 사용할 수도 있습니다

예를 들어 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 LLMTP + DP
MoE LLMEP + DP, 필요하면 TP
작은 OCR/VLMGPU 1개 + DP
큰 VLMTP + 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

도 강력한 후보가 됩니다.


이게 NUMA와도 연결됩니다

재미있는 부분이 바로 여기입니다.

서버 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에 배치하는 방향이 됩니다.


NIC이 '어중간한 위치'라는 부분

이건 실제 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을 확인한 뒤 조정해야 합니다.

한 장짜리 권장 배치표

시간모델성격GPUTPDPEP예상 Weight HBM*KV-cache HBM*CPU coreNUMALocal NVMe cache
🌙 야간GLM-5.3대형 MoE41~214checkpoint 확인 필요GPU당 20~40GiB부터32~48NUMA0 우선0.8~1.5TB+ 예상, 확인 필요
🌙 야간Qwen3.6-35B-A3B35B/3B-active MoE2112BF16 약 65GiB totalGPU당 20~40GiB16~24NUMA180~120GB
🌙 야간gpt-oss-20b21B/3.6B-active MoE1111MXFP4 약 13~16GiB급20~40GiB8~16NUMA120~40GB
☀️ 주간DeepSeek-V4.1-Flash552B/8→16B-active MoE41~214precision 따라 수백 GiBGPU당 20~40GiB부터32~48NUMA00.6~1.2TB+
☀️ 주간GLM-5.3-Flash320B/18B-active MoE41~214BF16이면 약 600GiB급 rawGPU당 20~40GiB부터32~48NUMA10.4~0.7TB+
☀️ 주간PaddleOCR-VL-1.6~0.9B VLM112+ 가능-약 1.5~2GiB2~8GiB8어느 쪽도 OK5~10GB
☀️ 주간Docling layout/herondocument/layout0~11N-작음거의 없음4~8자유<5GB
☀️ 주간Docling modelsdocument pipeline0~11N-모델별 상이거의 없음4~8자유<10GB
☀️ 주간DocumentFigureClassifierimage classifierCPU 권장-----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 비율을 보고 결정합니다.


KV cache는 표의 숫자를 고정값으로 보면 안 됩니다

예를 들어 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


OCR/Docling은 대형 LLM GPU와 섞지 않는 게 좋습니다

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


CPU core 배치는 이렇게 연결

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 요구량이 올라갑니다.


Local NVMe는 모델별 quota만 논리적으로 나누기

그래서 앞에서 질문했던 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에서 빠집니다.

따라서 현재 제가 잡을 baseline

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을 확인한 뒤 조정해야 합니다.

한 장짜리 권장 배치표

시간모델성격GPUTPDPEP예상 Weight HBM*KV-cache HBM*CPU coreNUMALocal NVMe cache
🌙 야간GLM-5.3대형 MoE41~214checkpoint 확인 필요GPU당 20~40GiB부터32~48NUMA0 우선0.8~1.5TB+ 예상, 확인 필요
🌙 야간Qwen3.6-35B-A3B35B/3B-active MoE2112BF16 약 65GiB totalGPU당 20~40GiB16~24NUMA180~120GB
🌙 야간gpt-oss-20b21B/3.6B-active MoE1111MXFP4 약 13~16GiB급20~40GiB8~16NUMA120~40GB
☀️ 주간DeepSeek-V4.1-Flash552B/8→16B-active MoE41~214precision 따라 수백 GiBGPU당 20~40GiB부터32~48NUMA00.6~1.2TB+
☀️ 주간GLM-5.3-Flash320B/18B-active MoE41~214BF16이면 약 600GiB급 rawGPU당 20~40GiB부터32~48NUMA10.4~0.7TB+
☀️ 주간PaddleOCR-VL-1.6~0.9B VLM112+ 가능-약 1.5~2GiB2~8GiB8어느 쪽도 OK5~10GB
☀️ 주간Docling layout/herondocument/layout0~11N-작음거의 없음4~8자유<5GB
☀️ 주간Docling modelsdocument pipeline0~11N-모델별 상이거의 없음4~8자유<10GB
☀️ 주간DocumentFigureClassifierimage classifierCPU 권장-----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 비율을 보고 결정합니다.


KV cache는 표의 숫자를 고정값으로 보면 안 됩니다

예를 들어 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


OCR/Docling은 대형 LLM GPU와 섞지 않는 게 좋습니다

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


CPU core 배치는 이렇게 연결

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 요구량이 올라갑니다.


Local NVMe는 모델별 quota만 논리적으로 나누기

그래서 앞에서 질문했던 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에서 빠집니다.

따라서 현재 제가 잡을 baseline

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 관점에서 같이 배치해도 되는가”를 담당합니다.

1. CPU Manager static

Kubernetes 기본 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에 영향을 줄 수 있습니다.


2. 그런데 CPU Manager만으로는 부족합니다

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가 등장합니다.


3. 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에 가깝습니다.


4. 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

즉 성능이 최적이 아니어도 가용성을 우선합니다.


restricted

restricted는 훨씬 엄격합니다.

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에서 유용합니다.


5. 그럼 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으로 추천했던 이유가 이것입니다.


6. restricted라고 해서 반드시 "한 NUMA"라는 뜻은 아니다

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

restricted를:

무조건 CPU/GPU를 한 NUMA에 넣는다.

라고 이해하면 안 됩니다.

그건 single-numa-node에 더 가까운 개념입니다.

restricted의 핵심은:

각 resource provider의 topology hint를 종합했을 때 허용 가능한 topology alignment인가?

입니다.

그래서 8 GPU Pod처럼 본질적으로:

NUMA0 + NUMA1

을 사용하는 workload를 설계할 여지가 있습니다.


7. 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를 검토할 가치가 큽니다.


8. 실제 kubelet 설정은 대략 이런 모습

노드의 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 성능을 만드는 것이라고 보면 정확합니다.

===

profile
engineer

0개의 댓글