GPU 한 장을 여럿이 나눠 쓰는 4가지 방법 - MIG, MPS, 시분할, GPUShare
GPU 4장으로 최대 70개 파드를 동시에 추론시켜야 했을 때, 왜 하드웨어 파티셔닝(MIG)이 아니라 소프트웨어 레벨 공유(GPUShare)를 택했는지. 4가지 공유 방식의 동작 계층과 트레이드오프를 정리했다.
문제: GPU는 4장인데 띄워야 할 모델은 훨씬 많다
위성영상 AI 처리 플랫폼에서 객체탐지·세그멘테이션·초해상도 추론을 각각 다른 모델로 서빙해야 했다. GPU는 4장뿐이었고, 기본 K8s 디바이스 플러그인은 "파드 하나에 GPU 하나 통째로" 붙이는 것 이상을 모른다. 그대로 두면 파드 수가 GPU 수를 넘는 순간 나머지는 전부 Pending이다.
실제로 겪은 문제는 두 가지였다.
- 컨테이너 하나가 GPU 하나를 통째로 점유 - 추론 요청이 간헐적인 모델도 GPU를 계속 물고 있어 대부분의 시간 동안 GPU가 유휴 상태였다.
- 모델을 늘릴 때마다 GPU를 늘려야 하는 구조 - 물리 GPU 수가 곧 동시 서빙 가능한 모델 수의 상한이었다.
GPU를 "나눠 쓰는" 방법이 필요했다. 그런데 "나눠 쓴다"는 표현 하나에 실제로는 완전히 다른 계층에서 동작하는 네 가지 방식이 섞여 있다.
4가지 방법이 어느 계층에서 나누는가
GPU 공유 방식은 어느 계층에서 자르느냐로 성격이 완전히 달라진다.
| 방식 | 자르는 계층 | 격리 강도 | 재구성 비용 |
|---|---|---|---|
| MIG (Multi-Instance GPU) | 하드웨어 - SM·L2·메모리 대역폭까지 물리 분할 | 강함 - 한 인스턴스 장애가 다른 인스턴스에 전파되지 않음 | 높음 - 프로파일 변경 시 재구성 필요, 지원 GPU 한정(A100/H100 등) |
| MPS (Multi-Process Service) | CUDA 컨텍스트 - 여러 프로세스의 커널을 하나의 컨텍스트에서 동시 실행 | 약함 - 메모리 오류가 전체 컨텍스트에 영향 | 낮음 |
| 시분할 (Time-Slicing) | 스케줄러 - 프로세스별로 시간을 쪼개 GPU를 순환 할당 | 없음 - 격리가 아니라 순번 대기 | 낮음 |
| GPUShare (K8s 스케줄러 확장) | K8s 리소스 할당 - 메모리를 fraction 단위로 나눠 스케줄링 | 소프트웨어 신뢰 기반 - 스케줄러가 fraction을 지키도록 조정하되 커널 실행 자체는 격리하지 않음 | 낮음 - 디바이스 플러그인 교체만으로 적용 |
MIG는 GPU 자체를 물리적으로 쪼갠다. 인스턴스마다 SM(Streaming Multiprocessor)과 메모리 대역폭이 독립적으로 배정되기 때문에, 한 인스턴스에서 무슨 일이 나도 다른 인스턴스는 영향을 받지 않는다. 가장 강력하지만 그만큼 유연하지 않다 - 파티션 구성을 바꾸려면 해당 GPU를 드레인하고 재구성해야 하고, 지원하는 GPU 세대도 정해져 있다.
MPS와 시분할은 반대쪽 극단이다. 둘 다 물리적 분할이 아니라 "같은 GPU를 어떻게 시간·컨텍스트 단위로 나눠 쓰게 하느냐"의 문제로, 하드웨어 격리가 없다. 시분할은 그냥 순서대로 돌리는 것이고, MPS는 여러 프로세스의 커널을 하나의 컨텍스트로 묶어 동시 실행 오버헤드를 줄여준다.
GPUShare는 이들과 층이 다르다. GPU 커널 실행 방식 자체를 바꾸는 게 아니라, K8s 스케줄러가 "이 파드는 GPU 메모리 몇 GiB만큼의 fraction을 쓴다"고 인식하게 만드는 디바이스 플러그인이다. 실제 격리는 여전히 소프트웨어 신뢰 기반(파드가 할당량을 넘기면 문제가 될 수 있음)이지만, K8s 리소스 모델 안에서 자연스럽게 스케줄링된다는 게 결정적으로 달랐다.
왜 MIG가 아니라 GPUShare였나
당시 보유한 GPU는 MIG를 지원하지 않는 세대(RTX 시리즈)였다. 그래서 "MIG를 몰라서 안 쓴 것"이 아니라 "MIG 자체가 선택지에 없었던" 상황이었다는 점을 먼저 짚어두고 싶다.
MIG가 지원됐다면 어떻게 접근이 달라졌을까 생각해봤다.
- GPU마다 몇 개의 MIG 프로파일로 나눌지(예: 1g.5gb × 7, 또는 더 큰 슬라이스로 적은 개수) 워크로드 패턴에 맞춰 미리 설계해야 했을 것이다.
- NVIDIA GPU Operator + MIG 매니저로 파티션을 관리하고, 프로파일을 바꿀 때마다 해당 GPU의 워크로드를 드레인해야 했을 것이다.
- fraction 재조정이 거의 없는 안정적인 워크로드였다면 MIG의 하드웨어 격리가 더 안전한 선택이었을 것이다.
실제 환경은 모델별 요청 빈도가 계속 바뀌었고, 파드 수도 늘었다 줄었다 했다. 이 조건에서는 두 가지가 중요했다.
- K8s 스케줄러와 네이티브로 맞물리는 것 - 디바이스 플러그인만 교체하면 기존 파드 스펙(
resources.limits)에 fraction 값을 그대로 쓸 수 있었다. - 재구성 비용이 거의 없는 것 - 모델을 하나 더 얹거나 fraction을 조정할 때 GPU를 드레인할 필요가 없었다.
그래서 Aliyun GPUShare를 선택했다. GPU 4장을 fraction 단위로 나눠 여러 컨테이너가 동시에 추론하도록 구성했고, 최대 70개 파드가 동시에 GPU를 나눠 쓰는 구조까지 갔다. 격리가 MIG보다 약하다는 트레이드오프는 분명히 있다 - 한 파드가 fraction을 넘겨 쓰면 다른 파드에 영향을 줄 수 있다. 이 리스크는 파드별 메모리 요청량을 실제 모델 크기 기준으로 넉넉히 잡고, 추론 서버를 Job이 아니라 Deployment로 돌려 노드 장애 시 자동 복구되게 하는 쪽으로 관리했다.
K8s에서 실제로 마주치는 것들
디바이스 플러그인이 하는 일
기본 NVIDIA 디바이스 플러그인은 GPU를 nvidia.com/gpu: 1 같은 정수 단위 extended resource로만 노출한다. GPUShare 계열 플러그인은 여기에 aliyun.com/gpu-mem 같은 fraction 단위 리소스를 추가로 노출해서, 파드 스펙에 "이 파드는 GPU 메모리 4GiB만큼만 쓴다"를 선언할 수 있게 해준다. K8s 입장에서는 여전히 익숙한 리소스 requests/limits 모델이라, 별도의 오케스트레이션 레이어를 새로 배우지 않아도 된다는 게 체감상 컸다.
파드가 Pending에서 안 움직일 때
fraction 스케줄링을 쓰면서 가장 자주 본 패턴은 "GPU가 남아있는 것처럼 보이는데 파드가 뜨지 않는" 경우였다. 확인 순서는 거의 항상 같았다.
kubectl describe pod로 스케줄링 실패 이유 확인 - 대부분 fraction 부족kubectl describe node <gpu-node>로 해당 노드에 남은 GPU fraction 실제 잔량 확인- 이미 뜬 파드들의 실제 메모리 사용량과 요청량(fraction) 사이 격차 확인 - 요청량을 과다하게 잡아둔 파드가 있으면 여기서 드러남
관측
GPU 자체의 사용률(utilization)만 보면 "쓰고는 있는데 왜 이렇게 느리지"를 설명하지 못하는 경우가 많았다. fraction 기반 공유에서는 GPU 사용률과 함께 파드별 메모리 사용량을 나란히 봐야 실제로 병목이 GPU 연산인지, 특정 파드의 메모리 재할당인지 구분이 됐다.
정리 - 언제 뭘 고를까
- 워크로드가 안정적이고 강한 격리가 필요하다 (멀티테넌시, 과금 단위 분리 등) → MIG. 대신 지원 GPU와 재구성 비용을 감안해야 한다.
- 프로세스는 여러 개인데 커널 실행 오버헤드를 줄이고 싶다 → MPS.
- 격리는 필요 없고 그냥 순서대로 돌리면 된다 → 시분할.
- K8s 스케줄러 안에서 자연스럽게, 재구성 비용 없이 fraction 단위로 나누고 싶다 → GPUShare 계열.
MIG를 실무로 다뤄본 적은 아직 없다. 다만 GPU를 소프트웨어 레벨에서 나눠 쓰는 문제를 실제 프로덕션에서 풀어본 경험은, 어떤 계층에서 왜 나누는지를 구분해서 볼 수 있게 해줬다고 생각한다.