네 환경이라면 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 dataset | AIStor | S3 |
| Preprocessed dataset | AIStor | S3 → local cache |
| Model weights | AIStor | S3 → local NVMe → load |
| Training checkpoint | local NVMe → AIStor | async/주기적 upload |
| Training temporary files | GPU node NVMe | local filesystem |
| Inference model | AIStor → node cache | startup 시 stage |
| KV cache | GPU/HBM/CPU/NVMe/MemKV 계층 | AIStor와 분리 |
| Logs/results/artifacts | AIStor | S3 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
예를 들어 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
앞에서 이야기했던 네 환경처럼 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에는 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를 검토하는 게 맞습니다.
이 부분이 자주 혼동됩니다.
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 서버 쪽은 가능하면 이렇게 가져가는 것이 좋습니다.
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이 운영상 더 깔끔할 가능성이 높습니다.
네 환경에서는 이 부분이 상당히 중요합니다.
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/
이 구조에서는 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를 모두 활용할 수 있습니다.
구성 자체는 단순합니다.
/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 같은 이름은 부팅/하드웨어 변경에 따라 식별 용도로 쓰기 적합하지 않습니다.
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을 줄일 수 있기 때문입니다.
저는 바로 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가 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
로 가져갈 수 있습니다.
제가 추천하는 구조는:
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 문제가 됩니다.
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에 따라 이렇게 결정하겠습니다.
| 주 workload | NVMe 구성 | 이유 |
|---|---|---|
| Pod당 GPU 1~4, 한 NUMA 내 사용 | 4+4 RAID0 | NUMA 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 RAID0 | K8s 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 계층이 훨씬 단순해집니다.
이 환경에서는 다음 구조를 추천합니다.
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에 그대로 들어가는 것을 피할 수 있습니다.
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을 알 필요가 없습니다.
이게 꽤 중요합니다.
모델 시작 시:
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 사용량까지 같이 놓고 보는 것이 정확합니다.