26O06b1

QK·2일 전

네 환경이라면 AIStor를 “GPU용 공유 filesystem”처럼 취급하기보다 S3 기반의 durable object tier로 두고, GPU 노드에는 local NVMe scratch/cache를 두는 2-tier 구조를 우선 추천합니다. MinIO도 GPU/K8s AI workload에서 S3-native access를 기본 데이터 경로로 설명하고 있고, 최신 AIStor에는 S3 over RDMA도 별도 고성능 경로로 들어가 있습니다. MinIO

                     Kubernetes
 ┌─────────────────────────────────────────────────────────┐
 │                                                         │
 │  GPU Node 1          GPU Node 2          GPU Node N      │
 │ ┌─────────────┐     ┌─────────────┐     ┌─────────────┐ │
 │ │ GPU x8      │     │ GPU x8      │     │ GPU x8      │ │
 │ │ Training /  │     │ Training /  │     │ vLLM/etc.   │ │
 │ │ Inference   │     │ Inference   │     │             │ │
 │ └──────┬──────┘     └──────┬──────┘     └──────┬──────┘ │
 │        │                    │                    │        │
 │ ┌──────▼──────┐      ┌──────▼──────┐      ┌──────▼─────┐│
 │ │ Local NVMe  │      │ Local NVMe  │      │ Local NVMe ││
 │ │ scratch /   │      │ scratch /   │      │ scratch /  ││
 │ │ cache       │      │ cache       │      │ cache      ││
 │ └──────┬──────┘      └──────┬──────┘      └──────┬─────┘│
 │        │                     │                    │      │
 │        └─────────────┬───────┴────────────────────┘      │
 │                      │                                  │
 │               S3 API / HTTPS                            │
 │          (high-speed Ethernet fabric)                   │
 │                      │                                  │
 │          ┌───────────▼───────────┐                      │
 │          │     MinIO AIStor      │                      │
 │          │ dataset / model       │                      │
 │          │ checkpoint / output   │                      │
 │          └───────────────────────┘                      │
 └─────────────────────────────────────────────────────────┘

제가 권장하는 데이터 경로

핵심은 AIStor → GPU memory를 모든 I/O마다 직접 읽는 구조로 만들지 않는 것입니다.

데이터권장 위치GPU workload 접근
Raw datasetAIStorS3
Preprocessed datasetAIStorS3 → local cache
Model weightsAIStorS3 → local NVMe → load
Training checkpointlocal NVMe → AIStorasync/주기적 upload
Training temporary filesGPU node NVMelocal filesystem
Inference modelAIStor → node cachestartup 시 stage
KV cacheGPU/HBM/CPU/NVMe/MemKV 계층AIStor와 분리
Logs/results/artifactsAIStorS3 upload

특히 checkpoint를 매번 AIStor에 synchronous write해서 training iteration을 block시키는 설계는 피하는 편이 좋습니다. 먼저 node-local NVMe에 checkpoint를 만들고 완료된 checkpoint를 AIStor로 flush하는 패턴이 GPU utilization 측면에서 유리합니다.

MinIO의 현재 GPU workload 가이드도 training pod에 local NVMe scratch PVC를 두고 AIStor S3 endpoint를 별도로 사용하는 구조를 제시합니다. MinIO


K8s에서는 CSI mount보다 S3 API를 우선

예를 들어 GPU Pod에서는 대략 이런 논리 구조가 됩니다.

GPU Pod
  |
  +-- /scratch        -> local NVMe PVC
  |
  +-- S3_ENDPOINT     -> https://aistor.ai-storage.svc:9000
  |
  +-- AWS_ACCESS_KEY_ID
  +-- AWS_SECRET_ACCESS_KEY
  +-- AWS_CA_BUNDLE

애플리케이션은 boto3 / MinIO SDK / S3 SDK / framework connector 등을 통해 AIStor에 접근합니다.

즉,

Dataset loading

AIStor
   │
   │ S3 GET (parallel)
   ▼
GPU Node RAM
   │
   ├── optional local NVMe cache
   │
   ▼
GPU memory

반대로 checkpoint는

GPU
 │
 ▼
local NVMe
 │
 │ parallel multipart PUT
 ▼
AIStor

방향을 권장합니다.

MinIO도 AIStor의 GPU workload에서 S3-native API를 primary data path로 설명하고 있으며 large-object transfer에 parallelism과 multipart tuning을 사용하는 방식을 권장합니다. MinIO


중요한 포인트: Ethernet과 InfiniBand/NDR을 구분

앞에서 이야기했던 네 환경처럼 GPU compute fabric에 NDR InfiniBand가 있고 storage/service 쪽 Ethernet fabric이 따로 있다면, 저는 초기 구성에서는 이렇게 분리하겠습니다.

                    ┌───────────────┐
                    │   GPU H100/   │
                    │   B200/B300   │
                    └──────┬────────┘
                           │
             ┌─────────────┴──────────────┐
             │                            │
        NDR InfiniBand              Ethernet
             │                            │
      GPU ↔ GPU traffic              S3 traffic
      NCCL / MPI                    AIStor
      RDMA collectives              K8s
             │                     management
             │                            │
       IB Leaf/Spine                Eth Leaf/Spine
                                          │
                                    ┌─────▼─────┐
                                    │  AIStor   │
                                    └───────────┘

즉 NDR은 GPU collective traffic, Ethernet은 AIStor S3 traffic으로 시작하는 것이 운영 측면에서 가장 명확합니다.

네가 앞서 말한 Cilium native routing + BGP + ECMP 환경과도 잘 맞습니다. AIStor traffic은 Ethernet L3 fabric으로 보내고 GPU-to-GPU NCCL traffic은 IB fabric에 남겨두는 식입니다.


그런데 AIStor + RDMA가 하나 더 있습니다

여기서 조금 재미있는 부분이 있습니다.

현재 AIStor에는 S3 over RDMA가 있습니다. MinIO 설명상 application buffer를 등록한 뒤 object payload를 RDMA로 전송하여 일반적인 kernel network stack 경로의 copy를 줄이는 방식입니다. MinIO AIStor Documentation

따라서 발전 단계는 이렇게 잡는 게 좋습니다.

Phase 1
=======

GPU
 │
 │ S3 / HTTPS / TCP
 ▼
Ethernet
 │
 ▼
AIStor


Phase 2
=======

GPU
 │
 │ S3 over RDMA
 ▼
RDMA Fabric
 │
 ▼
AIStor

다만 여기서 “NDR IB가 있으니 AIStor를 바로 NDR에 붙이자”로 가는 것은 권하지 않습니다.

먼저 baseline S3/TCP 성능을 측정해야 합니다.

예를 들어 GPU node당 Ethernet이 2×100/200/400GbE라면 dataset loader가 실제로 그 bandwidth를 얼마나 사용하는지 확인하고, GPU starvation이 storage path 때문에 발생하는지 본 뒤 RDMA를 검토하는 게 맞습니다.


GPUDirect Storage와도 구분해야 합니다

이 부분이 자주 혼동됩니다.

S3 over TCP
AIStor → NIC → CPU memory → application → GPU

S3 over RDMA
AIStor → RDMA → registered application memory → GPU pipeline

GPUDirect Storage
Storage → NIC/NVMe → GPU memory
             (CPU bounce 감소/회피)

NVIDIA의 GDS는 GPU와 storage 사이 I/O 경로를 최적화하는 기술이고, object-storage 쪽에는 별도로 cuObject 경로도 있습니다. NVIDIA Docs

그래서 “AIStor S3 over RDMA = GDS”라고 보면 안 됩니다.

이 둘은 별개의 계층입니다.


AIStor 자체 storage node 구성

AIStor 서버 쪽은 가능하면 이렇게 가져가는 것이 좋습니다.

AIStor Node

CPU
 │
 ├── 100/200/400GbE NIC
 │
 ├── NVMe
 ├── NVMe
 ├── NVMe
 ├── NVMe
 └── ...

       JBOD
        │
       XFS
        │
   AIStor Server

HW RAID → filesystem → AIStor보다는 직접 연결된 NVMe/SSD JBOD를 AIStor가 관리하게 하는 쪽이 권장됩니다. MinIO도 성능을 위해 direct-attached flash, 특히 NVMe를 권장하고, Kubernetes에서는 host-local PV를 관리하는 CSI 방식을 제시합니다. MinIO AIStor Documentation

그래서 GPU cluster와 AIStor cluster를 물리적으로 분리할 수 있다면 저는 오히려

GPU Nodes                         AIStor Nodes
─────────                         ────────────
8x GPU                            CPU
local NVMe                        NVMe x N
400GbE NIC  ===================  400GbE NIC
NDR IB NIC

처럼 구성하는 쪽을 선호합니다.

AIStor를 GPU node에 같이 배치하면 network hop 하나를 줄일 수 있지만 CPU/RAM/NVMe/network contention이 생길 수 있기 때문에, 대규모 B300 cluster라면 dedicated AIStor nodes + 같은 고속 Ethernet fabric이 운영상 더 깔끔할 가능성이 높습니다.


Air-gap에서는 이것도 설계에 포함해야 합니다

네 환경에서는 이 부분이 상당히 중요합니다.

Internet Zone
     │
     │ approved transfer
     ▼
Air-gap boundary
     │
     ├── Private OCI Registry
     │       ├ NVIDIA images
     │       ├ AIStor images
     │       └ application images
     │
     ├── Private Helm Registry
     │       ├ AIStor Operator
     │       └ AIStor ObjectStore
     │
     └── Internal CA / DNS

AIStor 공식 air-gap 절차도 container image와 Helm chart를 외부에서 준비하여 내부 private registry로 반입하는 2단계 방식을 사용합니다. Production AIStor라면 license도 사전에 준비해야 합니다. MinIO AIStor Documentation

특히 다음은 외부 dependency가 하나라도 남지 않도록 해야 합니다: NVIDIA GPU Operator 이미지, driver/container toolkit 관련 이미지, AIStor Operator/Server/sidecar, Helm charts, training/inference container, Python wheels/Conda packages, model/dataset, CA/CRL 및 필요한 OS repository mirror.


네 환경에 적용한다면

앞선 환경을 기준으로 저는 1차 목표 architecture를 아래처럼 잡겠습니다.

                  ┌─────────────────────┐
                  │ Kubernetes Control  │
                  └─────────────────────┘

          GPU / Compute Nodes
 ┌──────────────────────────────────────────┐
 │ B300 x8                                  │
 │                                          │
 │ local NVMe                               │
 │   ├ dataset cache                        │
 │   ├ model staging                        │
 │   └ checkpoint scratch                   │
 │                                          │
 │ NDR IB ───────── NCCL / GPU traffic      │
 │                                          │
 │ 400GbE x2 ────── S3 / K8s traffic       │
 └────────────────┬─────────────────────────┘
                  │
             Ethernet Fabric
           BGP + ECMP + Cilium
                  │
       ┌──────────┴───────────┐
       │                      │
 ┌─────▼─────┐          ┌─────▼─────┐
 │ AIStor #1 │   ...    │ AIStor #N │
 │ NVMe JBOD │          │ NVMe JBOD │
 └───────────┘          └───────────┘

 Buckets
 ├── datasets/
 ├── models/
 ├── checkpoints/
 ├── inference-artifacts/
 └── results/

그리고 NDR IB에 AIStor traffic을 넣는 것은 Phase 2 최적화 항목으로 남겨두겠습니다. 먼저 400GbE S3/TCP baseline → GPU data-loader throughput → GPU idle/I/O wait → checkpoint throughput을 측정한 뒤, 병목이 object path로 확인되면 S3 over RDMA를 검증하는 순서가 안전합니다.

특히 네가 이전에 정리하던 B300 + AIStor + MemKV 테스트 구조와 연결하면 역할을 아주 깔끔하게 나눌 수 있습니다:

                 GPU HBM
                    ▲
                    │
               KV / tensors
                    │
              ┌─────┴─────┐
              │  MemKV    │  ← hot / low-latency tier
              └───────────┘

              Local NVMe      ← scratch/cache/staging
                    ▲
                    │
              ┌─────┴─────┐
              │  AIStor   │  ← durable/capacity tier
              └───────────┘
                    ▲
                    │
          dataset/model/checkpoint

즉 AIStor = durable object/data lake tier, Local NVMe = GPU staging/scratch tier, MemKV = KV/cache hot tier, NDR = GPU collective/RDMA fabric으로 역할을 분리하면 전체 구조가 상당히 명확해집니다. 이 구분은 이전 v13_1에서 remote_object(AIStor)와 memkv_tcp / memkv_rdma를 분리했던 이유와도 정확히 맞습니다.


GPU 노드의 8개 NVMe가 AIStor의 영구 저장소가 아니라 dataset/model staging, scratch, checkpoint 임시 저장용이라는 전제라면, RHEL 10에서는 RAID5/6 같은 parity RAID보다 JBOD 또는 RAID0 계열을 우선 고려하는 게 좋습니다.

제가 이 환경에서 가장 먼저 검토할 구성은 8개 NVMe를 하나의 mdadm RAID0 + XFS로 묶는 방식입니다.

NVMe0 ─┐
NVMe1 ─┤
NVMe2 ─┤
NVMe3 ─┤
NVMe4 ─┼── mdadm RAID0 ── XFS ── /scratch
NVMe5 ─┤
NVMe6 ─┤
NVMe7 ─┘

                         ├── dataset-cache/
                         ├── model-cache/
                         ├── checkpoints/
                         └── tmp/

왜 RAID0인가?

이 구조에서는 AIStor가 authoritative/durable copy이고 local NVMe는 다시 만들 수 있는 데이터만 담도록 설계하는 게 핵심입니다.

예를 들어:

AIStor
  │
  │ dataset/model
  ▼
400GbE
  │
  ▼
8 x NVMe RAID0
  │
  │ DataLoader
  ▼
CPU RAM
  │
  ▼
GPU HBM

NVMe 하나가 죽으면 RAID0 전체가 깨질 수 있지만, /scratch를 비우고 AIStor에서 다시 stage하면 됩니다.

그 대가로 8개 NVMe의 bandwidth와 IOPS를 모두 활용할 수 있습니다.


RHEL 10에서는 mdadm + XFS를 우선 추천

구성 자체는 단순합니다.

/dev/nvme0n1 ─┐
/dev/nvme1n1 ─┤
/dev/nvme2n1 ─┤
/dev/nvme3n1 ─┤
/dev/nvme4n1 ─┼── /dev/md0 ── XFS ── /scratch
/dev/nvme5n1 ─┤
/dev/nvme6n1 ─┤
/dev/nvme7n1 ─┘

개념적인 생성은 다음과 같습니다.

mdadm --create /dev/md0 \
  --level=0 \
  --raid-devices=8 \
  /dev/nvme0n1 \
  /dev/nvme1n1 \
  /dev/nvme2n1 \
  /dev/nvme3n1 \
  /dev/nvme4n1 \
  /dev/nvme5n1 \
  /dev/nvme6n1 \
  /dev/nvme7n1

mkfs.xfs /dev/md0

mkdir -p /scratch
mount -o noatime /dev/md0 /scratch

다만 실제 서버에서는 OS disk와 scratch NVMe를 정확히 구분한 뒤 WWN/serial 기반으로 자동화해야 합니다. /dev/nvme0n1 같은 이름은 부팅/하드웨어 변경에 따라 식별 용도로 쓰기 적합하지 않습니다.


그런데 B300급 GPU node라면 두 가지를 비교해보는 게 좋습니다

8개를 전부 RAID0으로 묶는 방법 외에 4+4 RAID0도 꽤 좋은 선택입니다.

Option A

NVMe x8
   │
   ▼
 RAID0
   │
   ▼
/scratch

장점
- 가장 단순
- 최대 aggregate bandwidth
- DataLoader에서 사용 편함

단점
- NVMe 1개 장애 → 전체 scratch 소실

반면:

Option B

NVMe0 ─┐
NVMe1 ─┤
NVMe2 ─┤── RAID0 ── /scratch0
NVMe3 ─┘

NVMe4 ─┐
NVMe5 ─┤
NVMe6 ─┤── RAID0 ── /scratch1
NVMe7 ─┘

이 구성은 NUMA topology를 활용할 수 있다는 장점이 있습니다.

예를 들어 8-GPU node가 dual-socket이고 PCIe topology가 다음처럼 되어 있다면:

NUMA 0                         NUMA 1

CPU0                           CPU1
 │                              │
 ├ GPU0                         ├ GPU4
 ├ GPU1                         ├ GPU5
 ├ GPU2                         ├ GPU6
 ├ GPU3                         ├ GPU7
 │                              │
 ├ NVMe0                        ├ NVMe4
 ├ NVMe1                        ├ NVMe5
 ├ NVMe2                        ├ NVMe6
 └ NVMe3                        └ NVMe7

     │                              │
 RAID0                         RAID0
     │                              │
 /scratch0                     /scratch1

이 경우 8개짜리 RAID0 하나보다 NUMA-local 4+4 RAID0가 더 좋은 결과를 낼 가능성이 있습니다.

GPU가 반대쪽 NUMA의 NVMe를 읽으면서 CPU interconnect를 넘어가는 traffic을 줄일 수 있기 때문입니다.

그래서 네 B300 환경이라면

저는 바로 8 x NVMe → RAID0으로 확정하기보다 아래 순서로 결정하겠습니다.

구성추천도용도
8×NVMe 각각 사용★★★application이 직접 striping/cache 관리
4+4 RAID0 + XFS★★★★★dual-NUMA 8-GPU node
8 RAID0 + XFS★★★★★topology가 중요하지 않을 때
RAID10★★★local checkpoint도 어느 정도 보호
RAID5★GPU scratch 비추천
RAID6★GPU scratch 비추천
LVM striping★★★volume 관리가 필요할 때

여기서 중요한 것은 실제 PCIe topology입니다.

RHEL에서 다음 정보를 보면 결정할 수 있습니다.

lscpu -e=CPU,NODE,SOCKET,CORE
numactl --hardware

lspci -tv

nvidia-smi topo -m

for d in /sys/block/nvme*n1; do
    echo "$d"
    cat "$d/device/numa_node"
done

예를 들어 결과가

                NUMA0              NUMA1
GPU             0 1 2 3            4 5 6 7
NVMe            0 1 2 3            4 5 6 7
NIC             eth0               eth1

처럼 깔끔하게 나뉜다면 저는 4+4 RAID0 쪽으로 갑니다.


K8s에서는 그 위에 Local PV

그 위에서 K8s가 raw NVMe를 직접 알 필요는 없습니다.

Physical

NVMe x4 ─ RAID0 ─ XFS ─ /mnt/nvme0
NVMe x4 ─ RAID0 ─ XFS ─ /mnt/nvme1

                    │
                    ▼

Kubernetes

Local PV
  │
  ├── /mnt/nvme0
  └── /mnt/nvme1

                    │
                    ▼

GPU Pod

/scratch0
/scratch1

또는 단순화를 원하면:

NVMe x8
   │
 md RAID0
   │
  XFS
   │
/local-nvme
   │
Local PV
   │
GPU Pod

로 가져갈 수 있습니다.

단, checkpoint 정책은 분리하는 게 좋습니다

제가 추천하는 구조는:

                    GPU HBM
                       │
                       ▼
               Local NVMe RAID0
                /scratch
                 │      │
                 │      └─ model/dataset cache
                 │
                 └─ checkpoint.tmp
                         │
                         │ async upload
                         ▼
                       AIStor
                 checkpoints/

checkpoint가 AIStor에 정상 업로드된 뒤 local copy는 cache처럼 취급합니다.

그러면 RAID0 장애는 데이터 손실 문제가 아니라 해당 GPU node의 재시작/re-stage 문제가 됩니다.


한 가지 더: mdadm RAID0가 항상 최고인 것은 아닙니다

GPU data loader가 대량의 파일을 병렬로 읽는 workload라면, NVMe 8개를 각각 XFS로 두고 application/data loader가 여러 mount point에 데이터를 분산시키는 방법도 테스트할 가치가 있습니다.

        AIStor
           │
     parallel GET
           │
 ┌─────────┼───────────┐
 ▼         ▼           ▼
NVMe0     NVMe1 ...   NVMe7
 XFS       XFS          XFS
 │          │            │
 └──────────┼────────────┘
            │
      DataLoader workers
            │
            ▼
           GPU

이 방식은 md layer를 없애고 failure domain도 NVMe 하나로 제한할 수 있습니다. 대신 데이터 배치와 cache 관리가 복잡해집니다.

따라서 네 환경에서 제가 실제 PoC할 후보는 딱 3개입니다: 8×RAID0, NUMA-aware 4+4 RAID0, 8×JBOD/XFS + application striping. B300 8-GPU 서버라면 평균 throughput만 보지 말고 AIStor→NVMe stage GB/s, NVMe→RAM GB/s, DataLoader→GPU throughput, CPU/NUMA cross-traffic, GPU utilization까지 같이 측정해서 선택하는 게 좋습니다.


기본적으로는 4+4 RAID0를 만들었다고 해서 Linux/K8s가 GPU의 NUMA 위치를 보고 자동으로 /scratch0 또는 /scratch1에 I/O를 분배해주지는 않습니다. 하지만 application이 NUMA를 직접 인식하게 만들 필요도 없습니다. K8s scheduling/resource allocation 계층에서 NUMA locality를 맞춰주는 방식이 가장 깔끔합니다.

예를 들어 물리 구성이 이렇다고 하겠습니다.

NUMA 0                              NUMA 1
────────                            ────────
GPU 0~3                             GPU 4~7
NVMe 0~3                            NVMe 4~7
   │                                   │
 RAID0                               RAID0
   │                                   │
/scratch0                           /scratch1

application에게 단순히 /scratch만 보여주면서 “현재 할당된 GPU가 NUMA0이면 scratch0, NUMA1이면 scratch1”을 자동 선택하게 만드는 것이 이상적입니다.

                    Kubernetes

        Pod A                       Pod B
     GPU 0~3 할당                GPU 4~7 할당
          │                          │
          ▼                          ▼
       /scratch                   /scratch
          │                          │
          ▼                          ▼
   host /scratch0              host /scratch1
          │                          │
          ▼                          ▼
     NVMe 0~3                   NVMe 4~7
       RAID0                      RAID0
          │                          │
        NUMA0                      NUMA1

그러면 application은 둘 다 그냥

/scratch/dataset
/scratch/models
/scratch/checkpoint

만 사용하면 됩니다.

다만 일반적인 Local PV만으로는 GPU와 NVMe의 NUMA affinity까지 자동으로 연결되지 않습니다. 여기서 K8s의 CPU Manager, Topology Manager, device plugin/resource topology 등을 같이 설계해야 합니다.

가장 중요한 함정

만약 Pod 하나가 B300 GPU 8개를 전부 사용하는 distributed training job이라면 이야기가 달라집니다.

Pod / Job

GPU 0 1 2 3        GPU 4 5 6 7
 NUMA0                NUMA1
   │                    │
scratch0             scratch1

이 Pod는 양쪽 NUMA를 동시에 사용합니다. 따라서 “이 Pod가 어느 NUMA인가?”라는 답 자체가 없습니다.

이 경우 /scratch → NUMA-local RAID를 하나 자동 선택하면 오히려 잘못된 설계가 됩니다. GPU 4~7의 workload도 NUMA0 NVMe를 읽게 될 수 있기 때문입니다.

이런 8-GPU single-node training이 주 workload라면 오히려 다음 두 가지가 더 현실적입니다.

A. 8개 NVMe RAID0 하나로 단순화

NVMe x8
   │
 RAID0
   │
/scratch
   │
8-GPU training process

application 입장에서는 가장 편합니다. NUMA locality 일부를 포기하고 aggregate bandwidth와 단순성을 얻습니다.

B. 4+4를 유지하고 training framework/data loader가 locality를 인식

Rank 0~3                         Rank 4~7
GPU 0~3                         GPU 4~7
   │                               │
   ▼                               ▼
/scratch0                       /scratch1
   │                               │
NVMe0~3                         NVMe4~7

예를 들어 PyTorch distributed training에서 LOCAL_RANK에 따라 dataset cache 위치를 결정할 수 있습니다.

if local_rank < 4:
    cache_dir = "/scratch0"
else:
    cache_dir = "/scratch1"

이 방식이 NUMA locality를 가장 확실하게 보장하지만 application/data pipeline이 이를 알아야 한다는 단점이 있습니다.

중간 계층에서 완전히 투명하게 만들 수 있나?

가능은 합니다. 예를 들어 두 filesystem 위에 별도 namespace/분산 filesystem 계층을 얹어 /scratch 하나로 보이게 할 수 있습니다.

Application
     │
 /scratch
     │
 ┌───┴──────┐
 ▼          ▼
scratch0   scratch1
 │           │
NUMA0      NUMA1

하지만 “어느 GPU가 어떤 파일을 읽을지”를 filesystem이 알 수 없기 때문에 진정한 GPU-NUMA-aware routing은 자동으로 해결되지 않습니다. 오히려 layer만 늘어나고 성능 특성이 불투명해질 수 있습니다.

그래서 B300 8-GPU 노드라면 저는 workload에 따라 이렇게 결정하겠습니다.

주 workloadNVMe 구성이유
Pod당 GPU 1~4, 한 NUMA 내 사용4+4 RAID0NUMA locality 효과 큼
Pod당 GPU 8개8 RAID0단순하고 aggregate BW 활용
8 GPU지만 rank별 I/O 제어 가능4+4 RAID0최적 NUMA locality
여러 inference Pod가 GPU 1~2개씩 사용4+4 RAID0K8s topology와 궁합 좋음
복잡한 workload 혼재8 RAID0운영 단순성 우수

특히 네 B300 환경에서 한 노드의 8 GPU를 하나의 training job이 통째로 사용하는 게 주 패턴이라면, 저는 이전 답변의 우선순위를 조금 수정해서 8×NVMe RAID0 + XFS를 기본안, 4+4 RAID0를 NUMA 최적화 PoC 안으로 두겠습니다.

그리고 AIStor → local NVMe staging → GPU 구조에서는 실제 병목이 NUMA보다 400GbE/800GbE NIC 또는 data loader일 수도 있습니다. 따라서 8 RAID0 vs 4+4 RAID0를 결정할 때 fio만 비교하면 안 되고, NIC → NVMe → DataLoader → GPU까지 end-to-end로 측정하는 게 중요합니다.


이 패턴이라면 저는 NUMA 때문에 NVMe를 4+4로 물리적으로 나누기보다는, 8개 NVMe를 하나의 고성능 local tier로 사용하는 쪽을 우선 추천합니다.

이유는 지금 workload의 핵심이 NUMA locality보다 시간대에 따라 서로 다른 대형 모델 세트를 load/unload하고, AIStor에서 local NVMe로 모델을 staging/cache하는 것이기 때문입니다.

이미지 기준으로 보면 대략 이런 패턴입니다.

주간
 ├─ DeepSeek-V4.1-Flash
 ├─ GLM-5.3-Flash
 ├─ PaddleOCR-VL
 └─ Docling 계열

야간
 ├─ GLM-5.3
 ├─ Qwen3.6-35B-A3B
 └─ gpt-oss-20b

여기서 4+4로 나누면 오히려 capacity fragmentation 문제가 생길 수 있습니다.

NUMA0 /scratch0             NUMA1 /scratch1
NVMe x4                     NVMe x4
   │                           │
   ├ GLM-5.3                   ├ Qwen
   └ gpt-oss                   └ ...

남은 공간 2 TB              남은 공간 3 TB

전체적으로 5 TB가 남았더라도 한쪽에 3 TB 모델을 추가로 stage해야 하면 배치가 꼬입니다.

반면 8개를 하나로 만들면:

8 x NVMe
    │
    ▼
 RAID0
    │
   XFS
    │
 /model-cache
    │
    ├── GLM-5.3/
    ├── Qwen3.6-35B-A3B/
    ├── gpt-oss-20b/
    ├── DeepSeek-V4.1-Flash/
    ├── GLM-5.3-Flash/
    ├── PaddleOCR-VL/
    └── docling/

가 되어 K8s와 model serving 계층이 훨씬 단순해집니다.

더 중요한 건 NUMA보다 "모델 캐시" 설계입니다

이 환경에서는 다음 구조를 추천합니다.

                   AIStor
             authoritative copy
                     │
                     │ S3
                     ▼
              ┌─────────────┐
              │ Model Cache │
              │  NVMe x8    │
              │ RAID0 + XFS │
              └──────┬──────┘
                     │
          ┌──────────┴──────────┐
          │                     │
        주간                    야간
          │                     │
 DeepSeek Flash              GLM-5.3
 GLM Flash                  Qwen
 OCR/Docling                gpt-oss
          │                     │
          ▼                     ▼
       GPU HBM                GPU HBM

NVMe는 AIStor와 GPU 사이의 persistent model cache/staging tier가 됩니다.

예를 들어 야간 전환 전에:

AIStor
  │
  ├─ GLM-5.3 ──────────┐
  ├─ Qwen3.6 ──────────┼──> NVMe model cache
  └─ gpt-oss-20b ──────┘
                              │
                       workload 전환
                              │
                              ▼
                            GPU

로 미리 prefetch할 수 있습니다.

주간 전환 전에는 반대로 주간 모델들을 preload하면 됩니다.

그러면 workload 전환 시 AIStor에서 모델 전체를 다시 가져오는 시간이 GPU startup latency에 그대로 들어가는 것을 피할 수 있습니다.


NUMA 최적화는 그 다음 단계에서 하는 게 좋습니다

NVMe filesystem 자체를 NUMA별로 나누지 않고도 CPU/GPU/NIC affinity를 최적화할 수 있습니다.

예를 들어:

                       /model-cache
                       NVMe x8 RAID0
                            │
               ┌────────────┴────────────┐
               │                         │
             NUMA0                     NUMA1
               │                         │
        CPU / GPU 0~3              CPU / GPU 4~7

RAID0 I/O 자체는 양쪽 NVMe에 분산되지만, serving process의 CPU와 GPU affinity는 K8s Topology Manager/CPU Manager를 통해 맞출 수 있습니다.

즉,

Pod A
GPU 0~3
CPU NUMA0
      │
      └──── /model-cache

Pod B
GPU 4~7
CPU NUMA1
      │
      └──── /model-cache

처럼 application에게는 동일한 /model-cache를 보여주는 것입니다.

이 방식이면 serving application도 NUMA0/NUMA1을 알 필요가 없습니다.

특히 모델 serving에서는 local NVMe가 계속 hot path인지를 봐야 합니다

이게 꽤 중요합니다.

모델 시작 시:

AIStor
   │
   ▼
NVMe
   │
   ▼
CPU RAM
   │
   ▼
GPU HBM

으로 weight가 올라간 다음 inference 동안 대부분의 weight가 GPU HBM에 resident한다면, NVMe NUMA locality는 주로 model load/startup 시간에 영향을 줍니다.

추론 hot path는 오히려:

Request
   │
   ▼
CPU
   │
   ▼
GPU HBM
 ├─ model weights
 └─ KV cache

이므로 NVMe NUMA locality보다 GPU 배치, CPU pinning, NIC locality, GPU interconnect/NVLink/NVSwitch, KV-cache 구조가 훨씬 중요해집니다.

반대로 모델이 GPU HBM보다 커서 inference 중에도 CPU/NVMe offloading을 지속적으로 한다면 얘기가 달라집니다. 그 경우에는 NVMe NUMA locality가 다시 중요해집니다.


그래서 현재 환경에서는 이렇게 가져가겠습니다

B300 GPU Node
────────────────────────────────────────

             AIStor
                │
          S3 / Ethernet
                │
                ▼
       ┌─────────────────┐
       │ 8 x Local NVMe  │
       │                 │
       │ RAID0 + XFS     │
       │                 │
       │ /model-cache    │
       │ /dataset-cache  │
       │ /scratch        │
       └────────┬────────┘
                │
        ┌───────┴────────┐
        │                │
      NUMA0            NUMA1
        │                │
     GPU 0~3          GPU 4~7
        │                │
        └──── NVSwitch ──┘

그리고 K8s 쪽에서 시간대별로:

          08:00                         22:00

      Day workload                 Night workload
           │                            │
           ▼                            ▼
   DeepSeek Flash                    GLM-5.3
   GLM Flash                         Qwen
   OCR / Docling                     gpt-oss
           │                            │
           └───────── GPU Pool ─────────┘

                 ↑
           /model-cache
                 ↑
        AIStor prefetch/staging

으로 운영하는 것이 좋겠습니다.

결론적으로 현재 조건에서는 NUMA-aware 4+4 RAID0보다 8×NVMe RAID0 + XFS → 단일 /model-cache를 기본안으로 잡겠습니다. NUMA는 storage를 물리적으로 분할해서 해결하기보다 GPU/CPU/NIC scheduling에서 최적화하고, local NVMe는 capacity와 aggregate bandwidth를 최대한 pooling하는 편이 이 workload에 더 적합합니다.

다만 한 가지 예외가 있습니다. 이 모델들 중 일부가 GPU HBM에 완전히 올라가지 않고 CPU/NVMe offloading을 지속적으로 사용하는 구조라면 판단이 달라집니다. 그 경우 4+4 NUMA-local NVMe의 가치가 크게 올라갑니다. 따라서 다음 설계 단계에서는 각 모델을 B300에서 몇 GPU에 올릴지와 TP/EP 구성, 예상 HBM 사용량까지 같이 놓고 보는 것이 정확합니다.

profile
engineer

0개의 댓글