soobook
GPU

GPU Virtualization

vGPU와 MIG의 차이, Kubernetes에서 GPU를 나눠 쓰는 방식을 정리

GPU를 나눠 쓴다는 말은 두 가지 층으로 갈린다. vGPU는 VM에 GPU처럼 보이는 virtual device를 주는 방식이고, MIG는 물리 GPU 내부 자원을 여러 instance로 나누는 방식이다.

Intro

GPU 한 장을 여러 workload가 같이 쓰고 싶을 때가 있다.

개발용 VM에는 작은 GPU만 주고 싶을 수 있다.

가벼운 inference 서버 여러 개를 한 GPU에 올리고 싶을 수도 있다.

A100, H100 같은 큰 GPU에서는 한 장을 통째로 한 Pod에 주기 아까운 경우도 많다.

이때 나오는 단어가 vGPUMIG다.

둘 다 “GPU를 나눠 쓴다”는 말과 함께 등장하지만 같은 층의 개념은 아니다.

vGPU는 VM에 보이는 장치 모델이고, MIG는 GPU 내부 자원을 나누는 hardware partitioning 기능이다.

{
  "diagram": "html-diagram",
  "variant": "explainer",
  "title": "At a glance: vGPU vs MIG",
  "width": 980,
  "height": 560,
  "mobileWidth": 900,
  "regions": [
    {
      "id": "vgpu-side",
      "label": "vGPU path\\nsoftware partition",
      "x": 36,
      "y": 58,
      "width": 420,
      "height": 444
    },
    {
      "id": "mig-side",
      "label": "MIG path\\nGPU partition layer",
      "x": 524,
      "y": 58,
      "width": 420,
      "height": 444
    }
  ],
  "nodes": [
    {
      "id": "gpu-left",
      "kind": "gpu",
      "label": "Physical GPU",
      "caption": "time-sliced or MIG-backed",
      "x": 246,
      "y": 168,
      "width": 176,
      "height": 66
    },
    {
      "id": "vgpu-manager",
      "kind": "switch",
      "label": "vGPU Manager",
      "caption": "shares one GPU",
      "x": 246,
      "y": 258,
      "width": 170,
      "height": 64
    },
    {
      "id": "vgpu-a",
      "kind": "note",
      "label": "vGPU A",
      "caption": "virtual device",
      "x": 176,
      "y": 348,
      "width": 126,
      "height": 62
    },
    {
      "id": "vgpu-b",
      "kind": "note",
      "label": "vGPU B",
      "caption": "virtual device",
      "x": 316,
      "y": 348,
      "width": 126,
      "height": 62
    },
    {
      "id": "vm-a",
      "kind": "host",
      "label": "VM A",
      "caption": "guest driver",
      "x": 176,
      "y": 438,
      "width": 126,
      "height": 62
    },
    {
      "id": "vm-b",
      "kind": "host",
      "label": "VM B",
      "caption": "guest driver",
      "x": 316,
      "y": 438,
      "width": 126,
      "height": 62
    },
    {
      "id": "gpu-right",
      "kind": "gpu",
      "label": "Physical GPU",
      "caption": "MIG mode",
      "x": 734,
      "y": 168,
      "width": 182,
      "height": 68
    },
    {
      "id": "mig-0",
      "kind": "note",
      "label": "GPU instance 0",
      "caption": "compute + memory slice",
      "x": 636,
      "y": 320,
      "width": 170,
      "height": 64
    },
    {
      "id": "mig-1",
      "kind": "note",
      "label": "GPU instance 1",
      "caption": "compute + memory slice",
      "x": 832,
      "y": 320,
      "width": 170,
      "height": 64
    },
    {
      "id": "pod-a",
      "kind": "host",
      "label": "Pod A",
      "caption": "Kubernetes resource",
      "x": 636,
      "y": 438,
      "width": 142,
      "height": 62
    },
    {
      "id": "pod-b",
      "kind": "host",
      "label": "Pod B",
      "caption": "Kubernetes resource",
      "x": 832,
      "y": 438,
      "width": 142,
      "height": 62
    }
  ],
  "links": [
    {
      "id": "gpu-manager",
      "points": [[246, 201], [246, 226]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "manager-vgpu-a",
      "points": [[246, 290], [176, 317]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "manager-vgpu-b",
      "points": [[246, 290], [316, 317]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "vgpu-a-vm-a",
      "points": [[176, 379], [176, 407]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "vgpu-b-vm-b",
      "points": [[316, 379], [316, 407]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "gpu-to-mig0",
      "points": [[695, 202], [636, 288]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "gpu-to-mig1",
      "points": [[773, 202], [832, 288]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "mig0-pod",
      "points": [[636, 352], [636, 407]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "mig1-pod",
      "points": [[832, 352], [832, 407]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    }
  ]
}

Big Picture

{
  "diagram": "html-diagram",
  "variant": "explainer",
  "title": "Three layers to separate",
  "width": 914,
  "height": 500,
  "mobileWidth": 900,
  "regions": [
    {
      "id": "hardware-layer",
      "label": "1. Hardware",
      "x": 42,
      "y": 63,
      "width": 222,
      "height": 322
    },
    {
      "id": "vm-layer",
      "label": "2. VM layer",
      "x": 324,
      "y": 63,
      "width": 548,
      "height": 154
    },
    {
      "id": "k8s-layer",
      "label": "3. Kubernetes",
      "x": 324,
      "y": 245,
      "width": 548,
      "height": 154
    }
  ],
  "nodes": [
    {
      "id": "physical-gpu",
      "kind": "gpu",
      "label": "Physical GPU",
      "caption": "SM, HBM, copy engine",
      "x": 154,
      "y": 251,
      "width": 154,
      "height": 68
    },
    {
      "id": "vgpu-device",
      "kind": "note",
      "label": "vGPU",
      "caption": "VM-visible device",
      "x": 427,
      "y": 161,
      "width": 126,
      "height": 60
    },
    {
      "id": "vm",
      "kind": "host",
      "label": "VM",
      "caption": "guest driver",
      "x": 598,
      "y": 161,
      "width": 116,
      "height": 60
    },
    {
      "id": "guest-k8s",
      "kind": "host",
      "label": "Guest K8s",
      "caption": "node resource",
      "x": 761,
      "y": 161,
      "width": 142,
      "height": 60
    },
    {
      "id": "mig-instance",
      "kind": "note",
      "label": "MIG instance",
      "caption": "GPU slice",
      "x": 427,
      "y": 349,
      "width": 126,
      "height": 60
    },
    {
      "id": "device-plugin",
      "kind": "switch",
      "label": "Device plugin",
      "caption": "exposes resources",
      "x": 598,
      "y": 349,
      "width": 154,
      "height": 60
    },
    {
      "id": "pod",
      "kind": "host",
      "label": "Pod",
      "caption": "requests GPU",
      "x": 771,
      "y": 349,
      "width": 122,
      "height": 60
    }
  ],
  "links": [
    {
      "id": "gpu-to-vgpu",
      "path": "M 231 231 C 284 189 345 166 364 161",
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "vgpu-to-vm",
      "points": [[490, 161], [540, 161]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "vm-to-guest-k8s",
      "points": [[656, 161], [690, 161]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "gpu-to-mig",
      "path": "M 231 271 C 284 315 333 347 364 349",
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "mig-to-plugin",
      "points": [[490, 349], [521, 349]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "plugin-to-pod",
      "points": [[675, 349], [710, 349]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    }
  ],
  "labels": [
    {
      "text": "VM이 있으면 먼저 device를 받는다",
      "x": 598,
      "y": 227
    },
    {
      "text": "bare metal은 resource로 바로 보인다",
      "x": 598,
      "y": 425
    }
  ]
}

GPU virtualization을 처음 볼 때는 세 층을 분리하면 덜 헷갈린다.

첫 번째는 물리 GPU다. GPU 한 장에는 SM, HBM, L2 cache, copy engine 같은 실제 자원이 있다.

두 번째는 VM에 보이는 GPU다. VM은 물리 GPU를 직접 보는 대신 vGPU 같은 virtual device를 볼 수 있다.

세 번째는 Kubernetes에 보이는 GPU다. Kubernetes는 보통 nvidia.com/gpu나 MIG resource 같은 이름으로 Pod에 GPU 자원을 배정한다.

이 세 층이 항상 같이 등장하는 것은 아니다. Bare metal Kubernetes라면 VM이 없을 수 있고, VM 환경이라면 Kubernetes에 도달하기 전에 VM이 먼저 GPU를 받아야 한다.

vGPU

vGPU는 virtual GPU의 약자다. 물리 GPU 위에 여러 virtual GPU device를 만들고, 각 VM에 하나씩 붙이는 방식이다.

VM 안에서는 guest driver가 vGPU를 GPU처럼 본다. 그래서 사용자는 VM 안에서 nvidia-smi를 실행하고, CUDA application을 돌릴 수 있다.

다만 vGPU는 물리 GPU를 복사해서 새 GPU를 만드는 것이 아니다. 물리 GPU 하나를 여러 VM이 나눠 쓰도록 vGPU Manager 같은 계층이 중간에서 관리한다.

vGPU에서 자주 봐야 하는 것은 profile이다.

예를 들어 어떤 vGPU profile은 framebuffer를 얼마까지 쓸 수 있는지, display 기능이 있는지, 한 물리 GPU에 몇 개까지 만들 수 있는지를 정한다.

즉 vGPU는 “VM에 어떤 모양의 GPU를 보여줄 것인가”에 가까운 개념이다.

MIG

MIG(Multi-Instance GPU)는 NVIDIA Ampere 이후 일부 데이터센터 GPU에서 제공하는 기능이다.

물리 GPU 한 장을 여러 GPU instance로 나눈다.

MIG instance는 단순한 process quota가 아니다. GPU 내부의 compute와 memory 자원을 일정한 단위로 나눈다.

그래서 서로 다른 MIG instance가 같은 GPU 안에 있어도 memory 용량과 일부 실행 자원을 더 예측 가능하게 나눠 갖는다.

이 점이 time-sharing과 다르다.

Time-sharing은 같은 GPU를 시간으로 나눠 쓰는 느낌에 가깝다. 한 workload가 실행되는 동안 다른 workload는 기다릴 수 있다.

MIG는 GPU 내부에 여러 instance를 만들고, workload가 그 instance 중 하나를 받는다. instance 크기는 미리 정해진 profile에 맞춰 고른다.

MIG는 VM 전용 기능이 아니다. Bare metal Kubernetes에서도 MIG instance를 Pod resource로 노출할 수 있다.

vGPU vs MIG

vGPU와 MIG는 비교 대상이지만, 서로 같은 층에 있지는 않다.

vGPU는 VM에 GPU처럼 보이는 장치를 제공한다. VM 중심의 개념이다.

MIG는 물리 GPU 내부 자원을 나눈다. GPU hardware 중심의 개념이다.

그래서 둘은 같이 쓰일 수도 있다. 물리 GPU를 MIG mode로 나눈 뒤, 특정 MIG instance를 기반으로 VM에 vGPU를 제공하는 구성이 가능하다.

이런 경우를 MIG-backed vGPU라고 볼 수 있다.

반대로 Kubernetes bare metal cluster에서는 VM 없이 MIG만 쓸 수 있다. 이때 Pod은 vGPU가 아니라 MIG resource를 받는다.

짧게 정리하면 이렇다.

  • vGPU: VM에 보여주는 virtual GPU device
  • MIG: 물리 GPU 내부를 나눈 GPU instance
  • MIG-backed vGPU: MIG instance를 바탕으로 VM에 제공하는 vGPU

Kubernetes

Kubernetes에서 GPU는 extended resource로 노출된다. 가장 익숙한 형태는 nvidia.com/gpu: 1이다.

이 값은 “GPU 한 장을 Pod에 달라”는 뜻에 가깝다. NVIDIA device plugin이 node의 GPU를 발견하고, kubelet이 Pod에 GPU device를 연결할 수 있게 돕는다.

{
  "diagram": "html-diagram",
  "variant": "explainer",
  "title": "Kubernetes GPU resource path",
  "width": 980,
  "height": 540,
  "mobileWidth": 920,
  "regions": [
    {
      "id": "advertise-path",
      "label": "Advertise resources",
      "x": 42,
      "y": 68,
      "width": 896,
      "height": 178
    },
    {
      "id": "request-path",
      "label": "Request and mount",
      "x": 42,
      "y": 296,
      "width": 896,
      "height": 178
    }
  ],
  "nodes": [
    {
      "id": "gpu",
      "kind": "gpu",
      "label": "GPU / MIG",
      "caption": "whole GPU or slice",
      "x": 178,
      "y": 172,
      "width": 154,
      "height": 64
    },
    {
      "id": "driver",
      "kind": "memory",
      "label": "Driver",
      "caption": "device state",
      "x": 358,
      "y": 172,
      "width": 128,
      "height": 64
    },
    {
      "id": "plugin",
      "kind": "switch",
      "label": "Device plugin",
      "caption": "discovers GPUs",
      "x": 538,
      "y": 172,
      "width": 158,
      "height": 64
    },
    {
      "id": "node-resources",
      "kind": "note",
      "label": "Node resources",
      "caption": "nvidia.com/gpu\\nnvidia.com/mig-*",
      "x": 774,
      "y": 172,
      "width": 210,
      "height": 76
    },
    {
      "id": "pod-spec",
      "kind": "host",
      "label": "Pod spec",
      "caption": "resource request",
      "x": 178,
      "y": 400,
      "width": 154,
      "height": 64
    },
    {
      "id": "scheduler",
      "kind": "switch",
      "label": "Scheduler",
      "caption": "picks a node",
      "x": 358,
      "y": 400,
      "width": 132,
      "height": 64
    },
    {
      "id": "kubelet",
      "kind": "switch",
      "label": "Kubelet",
      "caption": "starts container",
      "x": 538,
      "y": 400,
      "width": 146,
      "height": 64
    },
    {
      "id": "pod",
      "kind": "host",
      "label": "Container",
      "caption": "GPU device mounted",
      "x": 774,
      "y": 400,
      "width": 210,
      "height": 76
    }
  ],
  "links": [
    {
      "id": "gpu-driver",
      "points": [[255, 172], [294, 172]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "driver-plugin",
      "points": [[422, 172], [459, 172]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "plugin-resources",
      "points": [[617, 172], [669, 172]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "pod-spec-scheduler",
      "points": [[255, 400], [292, 400]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "scheduler-checks-resources",
      "path": "M 424 400 C 538 326 638 246 674 210",
      "tone": "muted",
      "dashed": true,
      "flow": { "speed": "slow", "emphasis": "muted" }
    },
    {
      "id": "scheduler-kubelet",
      "points": [[424, 400], [465, 400]],
      "tone": "primary",
      "flow": { "speed": "normal", "emphasis": "soft" }
    },
    {
      "id": "kubelet-container",
      "points": [[611, 400], [669, 400]],
      "tone": "primary",
      "flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
    },
    {
      "id": "resource-to-kubelet",
      "path": "M 774 210 C 748 292 666 358 611 382",
      "tone": "muted",
      "dashed": true,
      "flow": false
    }
  ],
  "callouts": [
    {
      "id": "request-match",
      "text": "Pod request는 node에 보이는 resource 이름과 맞아야 한다",
      "x": 774,
      "y": 270,
      "width": 300,
      "tone": "primary"
    }
  ]
}

MIG를 쓰면 resource 이름이 더 세분화된다.

예를 들어 device plugin의 MIG 전략에 따라 nvidia.com/mig-1g.5gb 같은 resource가 보일 수 있다.

Pod은 전체 GPU 대신 특정 크기의 MIG instance를 요청한다.

GPU Operator를 쓰면 driver, container toolkit, device plugin, DCGM exporter, MIG Manager 같은 구성요소를 Kubernetes 안에서 관리할 수 있다.

MIG Manager는 node의 MIG 구성을 맞추는 역할을 한다.

운영 관점에서는 다음 순서로 보면 된다.

  1. node의 물리 GPU가 어떤 모델인가
  2. MIG를 지원하는가
  3. MIG mode를 켤 것인가
  4. device plugin이 어떤 resource 이름으로 노출하는가
  5. Pod이 whole GPU를 요청하는가, MIG slice를 요청하는가

VM 안에서 Kubernetes를 돌리는 경우에는 한 단계가 더 생긴다.

먼저 hypervisor가 VM에 vGPU나 MIG-backed vGPU를 제공해야 한다.

그 다음 guest OS 안의 Kubernetes가 그 GPU를 Pod에 다시 노출한다.

When to Use

VM 사용자에게 GPU를 나눠 주는 것이 목적이면 vGPU를 먼저 본다. VM마다 GPU처럼 보이는 장치가 필요하기 때문이다.

한 물리 GPU 안에서 여러 workload를 더 예측 가능한 크기로 나누고 싶다면 MIG를 본다. 특히 큰 데이터센터 GPU를 여러 inference Pod이나 개발 workload에 나눠 줄 때 유용하다.

Kubernetes만 쓰고 VM이 없다면 vGPU보다 MIG와 device plugin이 더 직접적인 관심사다. Pod이 보는 것은 VM 장치가 아니라 Kubernetes resource이기 때문이다.

VM도 있고 Kubernetes도 있다면 층을 나눠 봐야 한다. VM이 받은 GPU가 무엇인지, 그 VM 안의 Kubernetes가 다시 어떤 resource를 노출하는지 따로 확인해야 한다.

What to Check

GPU가 기대한 대로 보이지 않을 때는 먼저 어느 층에서 사라졌는지 확인한다.

Host에서 볼 것:

  • 물리 GPU가 driver에 잡혔는가
  • MIG mode가 켜져 있는가
  • 원하는 MIG instance가 만들어졌는가
  • vGPU Manager와 guest driver version이 맞는가

Kubernetes에서 볼 것:

  • NVIDIA device plugin이 떠 있는가
  • node에 nvidia.com/gpu 또는 MIG resource가 보이는가
  • Pod resource request가 실제 resource 이름과 일치하는가
  • container 안에서 nvidia-smi가 보이는가

VM 안에서 Kubernetes를 쓰는 경우:

  • VM이 먼저 vGPU 또는 MIG-backed vGPU를 정상 인식하는가
  • guest OS 안의 driver와 container runtime이 맞게 설치되어 있는가
  • guest Kubernetes가 그 GPU를 node resource로 다시 노출하는가

Summary

vGPU는 VM에 GPU처럼 보이는 virtual device를 제공한다. VM 단위로 GPU를 나눠 주고 싶을 때 보는 개념이다.

MIG는 물리 GPU 내부 자원을 GPU instance로 나눈다. 한 GPU를 여러 workload에 더 예측 가능한 크기로 나눠 주는 데 맞다.

Kubernetes에서는 whole GPU나 MIG instance가 resource로 노출된다. VM이 없다면 MIG와 device plugin을 보면 되고, VM이 있다면 VM이 받은 GPU와 guest Kubernetes가 노출한 GPU를 따로 봐야 한다.

References