NVIDIA Run:ai
NVIDIA Version MLOps
NVIDIA Run:ai는 쿠버네티스 위에서 GPU를 나눠 쓰고 모아 쓰는 오케스트레이션 계층이다.
Intro
GPU는 비싸고, 대개 논다.
학습이 끝난 노트북 하나가 A100 한 장을 통째로 붙잡고 있는 광경은 흔하다.
그런데 쿠버네티스의 기본 스케줄러는 이 낭비를 막지 못한다. 이유는 구조에 있다.
기본 스케줄러에게 GPU는 nvidia.com/gpu: 1 같은 정수 단위 확장 리소스(extended resource)다. Pod은 GPU를 1개, 2개, 4개 통째로 요청할 뿐, “0.5개”를 요청할 방법이 없다. 그래서 노트북 하나에도 GPU 한 장이 통째로 나간다.
또 기본 스케줄러는 Pod 하나씩 독립적으로 배치한다. 그런데 분산 학습은 worker 8개가 전부 떠야 한 번의 학습이 시작되는 all-or-nothing 작업이다. Pod을 하나씩 놓다 보면, worker 5개만 자리를 잡고 나머지 3개는 자원이 없어 대기하는 상황이 생긴다. 자리를 잡은 5개는 GPU를 붙든 채 나머지를 기다리고, 다른 작업도 그 GPU를 못쓰는 교착 상태가 발생하기도 한다.
여기에 팀이 여럿 얹히면 문제가 하나 더 늘어난다. 기본 스케줄러에는 “A팀은 GPU 10장까지, B팀은 남는 자원을 빌려 쓰되 A팀이 달라고 하면 돌려준다” 같은 조직 단위 쿼터와 공정성 개념이 없다.
Run:ai는 이 세 가지를 정면으로 푼다. GPU를 정수 아래로 쪼개고, 작업 단위로 묶어(gang) 배치하고, 프로젝트 사이에 쿼터와 공정성을 강제한다.
Two Planes
Run:ai는 쿠버네티스 클러스터 위에 두 덩어리로 설치된다.
- control plane은 관리와 조회를 맡는다. 프로젝트, 쿼터, 사용자 권한을 정의하고, 워크로드 제출을 받고, GPU 사용률을 모니터링한다. Web UI, API, CLI가 여기에 붙는다. 클라우드(SaaS)에 둘 수도, 고객 데이터센터 안(self-hosted)에 둘 수도 있다.
- cluster는 실제 스케줄링과 실행을 맡는다. Run:ai 스케줄러와 워크로드 관리 컴포넌트가 여기 있고, 각 쿠버네티스 클러스터 안에 Operator로 설치된다. control plane 하나가 여러 클러스터의 cluster 컴포넌트를 관리한다.
방향이 중요하다. cluster는 control plane으로 outbound-only SSL 연결만 연다. 그 연결로 오가는 건 워크로드, 노드 같은 메타데이터와 운영 지표뿐이다. 모델 가중치나 학습 데이터 같은 실제 데이터는 클러스터 밖으로 나가지 않는다.
이 구조는 앞선 글에서 다룬 Open Cluster Management의 hub-spoke와 같은 발상이다. 중앙이 각 클러스터로 들어가는 대신, 각 클러스터가 밖으로 나와 중앙에 붙는다. 덕분에 control plane 하나로 여러 리전, 여러 클라우드에 흩어진 GPU 클러스터를 한 화면에서 다룬다.
cluster 컴포넌트는 쿠버네티스를 대체하지 않고 확장한다. Run:ai의 워크로드(Workspace, Training, Inference)는 전부 Custom Resource(CRD)로 표현된다. 즉 kubectl get으로 조회되는 1급 쿠버네티스 오브젝트다. 실행 기반으로는 NVIDIA GPU Operator가 클러스터에 미리 깔려 있어야 한다. GPU를 노드에 리소스로 노출하는 건 여전히 GPU Operator의 몫이고, Run:ai는 그 위에서 스케줄링과 분할을 얹는다.
The Scheduler
Run:ai의 핵심은 스케줄러다. 기본 스케줄러가 못 하는 네 가지를 더한다. gang scheduling, 계층적 쿼터, 공정성(fairness), 그리고 이 셋을 매 사이클 다시 계산하는 리밸런싱이다.
Queues, Quota, Over-Quota
자원 분배의 단위는 Project다. Project는 팀이나 개인, 하나의 이니셔티브를 나타내고, 실제로는 쿠버네티스 네임스페이스로 구현된다. Project 여러 개를 Department로 묶어 상위 거버넌스 경계를 만든다. 스케줄러는 이 둘을 계층 큐로 다룬다. Project 큐가 Department 큐에 매여 있고, 이 큐는 node pool마다 하나씩 만들어진다.
각 Project에는 두 종류의 자원이 있다.
- deserved quota(보장 쿼터): Project가 보장받는 최대 자원. 예를 들어 GPU 10장. 이 안에서 스케줄된 작업은 한 번 뜨면 쫓겨나지 않는다.
- over-quota(초과분): 다른 Project가 안 쓰고 남긴 자원을 빌려 쓰는 몫. 단, 빌린 자원은 preemptible이다. 원래 주인이 달라고 하면 언제든 내줘야 한다.
여기서 규칙 하나가 핵심이다. non-preemptible 워크로드는 자기 deserved quota 안에서만 스케줄된다. over-quota 자원은 오직 preemptible 워크로드만 쓸 수 있다. “절대 안 죽어야 하는 추론 서버”는 보장 쿼터 안에, “죽어도 재시작하면 되는 학습 Job”은 빌려 쓰는 자리에 놓인다.
Fairshare
남는 자원을 여러 Project가 동시에 원할 때, 누가 얼마를 가져갈지는 fairshare로 정한다.
over-quota share는 각 Project의 over-quota weight에 비례해 나눈다. node pool의 미사용 자원을, 그 자원을 원하는 Project들의 가중치 비율로 쪼갠다.
중요한 건 이 값이 매 스케줄링 사이클마다 다시 계산된다는 점이다. 한 Project가 빌려 쓰던 자원도, 다음 사이클에 다른 Project의 fairshare가 커지면 회수 대상이 된다. 그래서 over-quota로 뜬 preemptible 작업은 본질적으로 유동적이다. 이걸 완화하려고 관리자는 node pool마다 최소 보장 실행 시간(min-runtime)을 걸 수 있다. 그 시간 동안은 빌려 쓴 작업도 non-preemptible처럼 버틴다.
기본 fairshare는 지금 이 순간의 요구만 본다. 과거에 GPU를 많이 쓴 팀에 페널티를 주고 싶다면 time-based fairshare를 켠다. Prometheus로 수집한 과거 GPU 사용 시간을 지수 감쇠(exponential decay)로 반영하고, 과거와 현재의 가중치는 K 값으로 조절한다.
Gang Scheduling
분산 학습의 교착은 gang scheduling으로 푼다.
Run:ai는 여러 Pod로 이뤄진 워크로드를 하나의 pod group으로 묶는다. 그리고 이 그룹을 원자적으로 다룬다. 전부 스케줄되거나(all pods running), 전부 대기하거나(none running). worker 8개 중 5개만 자리를 잡는 어정쩡한 상태가 원천적으로 없다. 스케줄러는 8개를 한꺼번에 바인딩하거나, 8개를 통째로 pending에 두고 다음 사이클에 다시 시도한다.
NVIDIA Dynamo의 prefill/decode 분리 서빙처럼 작업 안에 다시 하위 그룹이 있는 경우를 위해 multi-level gang도 지원한다. 모든 하위 그룹이 각각 gang을 이루고, 최상위 그룹까지 전부 성립해야 스케줄된다.
Placement, Preemption
node pool마다 배치 전략을 고를 수 있다.
- Bin-pack: 노드와 GPU를 최대한 꽉 채운 뒤 다음 노드로 넘어간다. 빈 노드를 최대한 남겨 큰 작업이 들어올 자리를 확보한다.
- Spread: 워크로드를 최대한 여러 노드와 GPU에 흩뿌린다. 노드당 부하를 낮춰 가용성을 높인다.
deserving 워크로드가 자리를 못 잡으면, 스케줄러는 곧장 남을 죽이지 않고 순서를 밟는다.
- Consolidation (bin packing) — 흩어진 워크로드를 더 적은 노드로 모아 조각(fragmentation)을 없애 자리를 만든다. 이때 옮겨야 하는 작업이 전부 곧바로 재배치 가능할 때만 실제로 실행하고, 그 전에 메모리 안에서 시뮬레이션을 돌려 성공하는 경우에만 옮긴다.
- Reclaim (큐 간 회수) — 자기 deserved quota나 fairshare를 넘겨 쓰고 있는 다른 Project에서 자원을 회수한다. 이건 워크로드 우선순위가 아니라 공정성이 판단 기준이다.
- Preemption (같은 큐 안) — 같은 Project 안에서 우선순위가 낮은 preemptible 작업을 밀어낸다.
우선순위는 사전(dictionary)으로 정의된다. very-low=25부터 very-high=150까지의 값이 있고, 규칙은 단순하다. 125처럼 100 이상이면 non-preemptible, 40처럼 100 미만이면 preemptible이다. 워크로드 종류마다 기본값이 다르다. 실험용 Build는 High(125, non-preemptible), 학습 Train은 Low(40, preemptible), 추론 Deploy는 Very-high(150, non-preemptible)다. 추론이 절대 밀려나지 않도록 설계됐다.
GPU Fractions
스케줄러가 누구에게 줄지를 정한다면, fractions는 GPU 한 장을 어떻게 쪼갤지를 정한다.
핵심 아이디어는 GPU 메모리를 작은 덩어리로 나누는 데 있다. gpu-fraction: "0.5"는 GPU 메모리의 50%를 뜻한다. 절대량이 아니라 비율이라, 같은 0.5라도 A100-40GB에서는 20GB, A100-80GB에서는 40GB가 된다. 정확한 크기가 필요하면 gpu-memory로 MiB를 직접 지정한다.
격리는 어떻게 보장할까. 각 Run:ai worker 노드에서 로컬로 도는 fractions 로직이 요청한 만큼의 메모리를 할당하고, 각 Pod에 독립된 가상 메모리 주소 공간을 준다. 어떤 워크로드도 요청량 이상을 못 쓰고, 남의 메모리 영역을 침범하지 못한다. 사용자 입장에서는 워크로드마다 “논리 GPU”가 하나씩 있는 경험이다.
기본 스케줄러와 충돌하지 않게 하는 장치도 있다. 워크로드가 GPU의 일부를 요청하면, Run:ai는 runai-reservation 네임스페이스에 GPU 한 장을 통째로 잡는 예약 Pod을 만든다. 다른 스케줄러가 봤을 때 “이 GPU는 이미 예약됨”으로 보이게 한다.
여기서 한 걸음 더 나간 게 dynamic fractions다. 쿠버네티스의 request/limit 개념을 빌려, request 0.25에 limit 0.80처럼 쓴다. 0.25만 보장하되, 다른 워크로드가 안 쓰는 동안에는 0.80까지 빌려 쓴다. 빌린 메모리는 주인이 필요해지면 회수되는데, 이때 초과분을 쓰던 프로세스는 GPUOOMKiller가 정리한다. 빌린 메모리는 OOM으로 죽을 수 있으니, 이 기능은 프로세스가 죽어도 커널은 살아남는 노트북 같은 인터랙티브 워크로드에 권장된다.
Time-Slicing and MIG
fractions는 메모리를 나눈다. 그런데 연산 시간(compute)은 어떻게 나눌까. 여기서 세 갈래가 갈린다.
기본값은 NVIDIA의 time-slicing이다. 그런데 이건 메모리 비율과 무관하게 연산 시간을 똑같이 나눈다. 한 워크로드가 25%, 다른 하나가 75% 메모리를 요청해도, 연산 시간은 각각 50%씩 가져간다. 메모리는 비대칭인데 연산은 대칭인, 어긋난 분배다.
Run:ai의 time-slicing은 이걸 메모리 비율에 맞춰 준다. 위 예에서 첫 워크로드는 연산 시간의 25%, 둘째는 75%를 받는다. 한쪽이 자기 몫을 안 쓰면 남는 시간은 나머지가 나눠 갖는다.
작동 방식은 시분할이다. 각 워크로드에 매 스케줄링 주기(plan time)마다 GPU 전체를 짧게(lease time) 독점시킨다. 기본값은 lease 250ms에 5% 정밀도라, 한 주기는 250 / 0.05 = 5000ms, 즉 5초다. gpu-fraction: 0.5인 워크로드는 5초 주기 중 2.5초를 받는다. 두 가지 모드가 있다. Strict는 요청한 비율만 딱 쓰고 초과를 허용하지 않고, Fair는 그 비율을 바닥으로 보장하되 노는 시간을 빌려 쓰게 한다.
세 번째 갈래가 MIG(Multi-Instance GPU)다. MIG는 GPU를 하드웨어 수준에서 고정된 슬라이스로 물리 분할한다. 격리는 가장 단단하지만, 관리자가 슬라이스를 미리 구성해야 하고 크기가 고정이다. 반면 fractions는 메모리 덩어리를 동적으로, 완전히 유연하게 할당한다. 둘은 성격이 다르고, 같은 노드에서 동시에 쓸 수 없다.
| 방식 | 나누는 대상 | 격리 | 유연성 |
|---|---|---|---|
| GPU Fractions | 메모리(가상 주소 공간) | 소프트웨어 | 동적, 임의 크기 |
| Run:ai Time-Slicing | 연산 시간(비율 기반) | 시분할 | 동적 |
| MIG | 메모리+연산(하드웨어 분할) | 하드웨어 | 고정 슬라이스 |
| Memory Swap | GPU↔CPU 메모리 확장 | 컨텍스트 스왑 | 동적 |
GPU Memory Swap
마지막으로 남은 것은 GPU 물리 메모리보다 큰 요구를 다루는 방법이다.
memory swap은 GPU 메모리를 CPU 메모리로 확장한다. 여러 워크로드가 각자 GPU 메모리를 거의 통째로 필요로 해도, 안 쓰는 워크로드의 컨텍스트를 CPU 메모리로 밀어내고(swap out) 쓰는 쪽을 GPU로 불러들여(swap in) 한 장을 돌려 쓴다. 메모리가 부족하다고 워크로드를 죽일 필요가 없어진다.
이 기능이 특히 유용한 곳은 모델 서빙이다. swap은 항상 모델의 컨텍스트를 GPU에 유지하므로, 요청이 뜸한 모델을 GPU에서 잠시 내렸다가 다시 올릴 때 몇 분씩 걸리던 콜드 스타트가 몇 초 이하의 웜 스타트로 줄어든다.
대가는 CPU 메모리다. H100 80GB 8장에 각 GPU마다 40GB짜리 LLM 4개를 스왑하려면 8 × 40 × 4 = 1,280GB, 약 1.2TB의 CPU 메모리가 필요하다. 시스템은 GPU 바이너리와 컨텍스트용으로 GPU RAM 2GiB를 따로 떼어 두기 때문에, 16GiB T4는 swap 노드에서 14GiB로 보인다. 한 가지 제약이 있다. memory swap은 Run:ai의 strict/fair time-slicing과 함께 쓸 수 없고, 기본 NVIDIA time-slicing에서만 동작한다.
Workloads and Inference
지금까지가 스케줄링과 GPU 분할이라면, 그 위에서 사용자가 실제로 던지는 건 세 종류의 워크로드다.
- Workspace: 실험과 개발 단계. Jupyter 노트북이 대표적이다.
- Training: 학습. 단일 노드 multi-GPU와, 여러 노드에 걸치는 distributed training으로 나뉜다. 분산 학습은
distributedFramework로 PyTorch, MPI, TensorFlow를 지원하고, worker들은 앞서 본 gang scheduling으로 묶여 배치된다. - Inference: 추론. 프로덕션 크리티컬로 취급돼 가장 높은 우선순위를 받고, 절대 preempt되지 않는다.
추론에는 오토스케일링이 붙는다. 최소/최대 replica를 정하고, latency, throughput, concurrency 같은 지표에 임계값을 걸어 부하에 따라 자동으로 늘리고 줄인다. minReplicas: 0으로 두면 scale-to-zero, 즉 트래픽이 없을 때 replica를 0으로 내려 GPU를 완전히 반납한다. 단일 노드는 Knative 기반 서버리스로, 한 노드에 안 들어가는 거대 LLM은 Leader-Worker 구조의 분산 추론으로 서빙한다. 서빙 백엔드로는 NVIDIA NIM, vLLM, 커스텀 추론 서버를 그대로 얹을 수 있다.
이 모든 워크로드는 Project(네임스페이스) 안에 놓이고, RBAC로 접근이 통제된다. 접근 규칙은 사용자, 그룹, 애플리케이션에 역할을 부여하되 그 범위(scope)를 Project, Department, Cluster, Account 단위로 정한다. GPU 사용률은 DCGM 기반으로 SM 활동, 메모리 대역폭, 텐서 코어 활동까지 Pod, 워크로드, 노드별로 관측된다.
KAI Scheduler
Run:ai의 스케줄링 엔진은 KAI Scheduler라는 이름으로 오픈소스가 됐다. Apache 2.0 라이선스로 공개됐고, 지금은 CNCF 프로젝트로 옮겨 갔다. 상용 Run:ai 플랫폼에 여전히 패키징돼 함께 배포되면서, 동시에 커뮤니티가 단독으로 쓸 수 있게 열렸다.
구현의 뿌리는 쿠버네티스 배치 스케줄링의 고전인 kube-batch다. 여기에 지금까지 본 기능 — gang scheduling, GPU sharing, 계층 큐, DRF(Dominant Resource Fairness) 기반 공정성, bin-pack/spread, consolidation, preemption — 을 얹었다.
쿠버네티스에 붙는 방식의 중심에는 두 개의 CRD가 있다.
- PodGroup: gang scheduling의 원자 단위. 함께 떠야 하는 Pod들을 묶고, 최소 멤버 수와 큐, 우선순위를 갖는다.
- Queue: 공정성을 강제하는 단위. 쿼터, over-quota weight, limit, 우선순위를 갖고 2단계 계층을 이룬다.
사용자가 이 CRD를 손으로 만들 필요는 없다. PodGrouper라는 컴포넌트가 Kubeflow, Ray, Argo, Training Operator 같은 프레임워크의 워크로드를 자동으로 감지해 PodGroup으로 묶어 준다.
스케줄링은 매 사이클 세 단계를 밟는다.
- Snapshot — 쿠버네티스 클러스터 상태를 통째로 찍어 내부 자료구조로 변환한다. 사이클 중간에 상태가 바뀌어 생기는 불일치를 막는다.
- Fairshare 계산 — 먼저 각 큐에 deserved quota를 배정하고(deserved-quota 단계), 남는 자원을 over-quota weight에 따라 부족한 큐에 나눈다(over-quota 단계).
- Actions — allocate, consolidation, reclaim, preempt를 수행한다. 앞 절에서 본 그 순서다.
즉 유료 Run:ai 플랫폼을 통째로 도입하지 않고도, 스케줄링 엔진만 떼어 자기 쿠버네티스 클러스터에 얹을 수 있다. GPU를 쓰는 배치 워크로드가 많은 클러스터라면 이것만으로도 기본 스케줄러의 한계를 상당 부분 넘어선다.
Summary
Run:ai는 세 층으로 이해하면 깔끔하다.
맨 아래는 GPU 분할이다. fractions로 메모리를 쪼개고, time-slicing으로 연산 시간을 비율대로 나누고, swap으로 CPU 메모리까지 빌려 GPU 한 장에 여러 워크로드를 태운다. 정수 단위로만 GPU를 다루던 쿠버네티스의 벽을 device 수준에서 허문다.
가운데는 스케줄러다. 작업을 gang으로 묶어 분산 학습의 교착을 없애고, Project와 Department의 계층 쿼터와 fairshare로 여러 팀 사이의 자원 경쟁을 조정한다. deserved quota는 보장하고, over-quota는 빌려주되 공정하게 회수한다.
맨 위는 control plane이다. 여러 클러스터를 outbound-only로 끌어모아 한 화면에서 워크로드를 던지고 쿼터를 걸고 사용률을 본다.
이 스케줄링 엔진이 KAI Scheduler로 열렸다는 건, GPU 스케줄링의 좋은 관행 상당수가 오픈소스로 공개돼 검증받게 됐다는 뜻이기도 하다.
References
- NVIDIA Run:ai Documentation — 아키텍처, 스케줄러, GPU fractions/time-slicing/memory swap, 워크로드 공식 문서
- NVIDIA Open Sources Run:ai Scheduler to Foster Community Collaboration — KAI Scheduler 오픈소스화(2025-04), 아키텍처와 스케줄링 사이클
- NVIDIA/KAI-Scheduler (GitHub) — Apache 2.0, PodGroup/Queue, PodGrouper, kube-batch 기반
- NVIDIA to Acquire Run:ai — 인수 배경과 제품 포지셔닝
- kube-batch — KAI Scheduler의 구현적 뿌리인 배치 스케줄러