Intel iGPUをパススルーして利用する - Kubernetes Pod編

この記事は [TUNA-JP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/tuna-jp) の12月20日分の投稿記事です。

https://qiita.com/advent-calendar/2023/tuna-jp

## はじめに

Intel NUC 13ではモバイル向け[第13世代Coreプロセッサ(Raptor Lake)](https://www.intel.co.jp/content/www/jp/ja/products/docs/processors/core/13th-gen-processors.html)がCPUとして利用されています。第12世代Coreプロセッサ(Alder Lake)から、高性能コア(P-Core)と高効率コア(E-Core)の2種類のCPUを搭載するPerformance Hybrid Architectureで構成されており、内蔵GPU(iGPU)として[Iris Xe Graphics](https://www.intel.com/content/www/us/en/products/docs/discrete-gpus/iris-xe-integrated-graphics/overview.html)が搭載されています。
Intel NUC 13(IntelCore i7-1360P)にESXiをインストールして、Cluster APIが仮想マシンで構成するKubernetesクラスター上でIris Xe (iGPU)を使って、[OpenVINO](https://www.intel.co.jp/content/www/jp/ja/internet-of-things/openvino-toolkit.html)により[Stable Diffusion](https://huggingface.co/stabilityai/stable-diffusion-2-1-base)でText-to-Imageを試してみます。

::: note warn

VMware ESXiはIntel コンシューマー向けCPUのPerformance Hybrid Architectureをサポートしていません。本記事の内容はVMwareがサポートする内容ではありません。

:::

本記事は仮想マシン上のKubernetes PodからIntel iGPUを利用する方法を説明しています。仮想マシンから利用する場合はこちらの記事を参照してください。

https://qiita.com/masanara/items/b68fbdfd38484773ed37

## Image Builderで仮想マシンイメージの作成

Cluster APIでKubernetesクラスターを作成する際に利用する[Ubuntu仮想マシンイメージ](https://image-builder.sigs.k8s.io/capi/providers/vsphere#configuration)はImage Builderによって作成することが可能です。Ubuntuの場合、18.04、20.04、22.04を利用することができますが、最新の22.04もLinuxカーネルとして 5.15 が利用されており、Raptor Lake-PのiGPUに対応していません。Raptor Lakeの-PのiGPUを利用するには、[Linuxカーネル 5.19 以降が必要](https://linux-hardware.org/?id=pci:8086-a7a0-8086-3037)です。

GPUを利用するために、HWEカーネルを使いたいので[Image BuilderのUbuntu 22.04のuser-data](https://github.com/kubernetes-sigs/image-builder/blob/2076886b4915af5f8f94f00185fafc3b735b2e65/images/capi/packer/ova/linux/ubuntu/http/22.04/user-data#L66)の`.autoinstall.packages`に`linux-image-generic-hwe-22.04`を追加し、ビルド時にHWEカーエネルを追加したカスタムイメージを作成しました。

```yaml
#cloud-config
autoinstall:
  ssh:
    install-server: true
    allow-pw: true
  # Customize the list of packages installed.
  packages:
    - open-vm-tools
    - linux-image-generic-hwe-22.04
```

Image Builderを利用してカスタムイメージを作成しました。Image Builderについては@ttani03さんの記事が参考になります。

https://qiita.com/ttani03/items/25f744033733d5b6cacc

また、Stable Diffusionを利用するコンテナイメージは各種ライブラリが含まれるためサイズが大きく、ノードのディスクサイズも大きめの80GBにするため、イメージビルド時にオプション`PACKER_FLAGS=-var=disk_size="8192"`を指定して仮想マシンイメージを作成しました。

```bash
$ PACKER_FLAGS=-var=disk_size="8192" IB_OVFTOOL=1 IB_OVFTOOL_ARGS="--allowExtraConfig" make build-node-ova-vsphere-ubuntu-2204
```

作成されたイメージを利用して、Cluster APIによりKubernetesクラスターを作成しました。

```bash
$ ls -lh output/ubuntu-2204-kube-v1.27.2/
total 4.6G
-rw-rw-r-- 1 nos nos 1.3K Dec  9 19:51 packer-manifest.json
-rw-rw-r-- 1 nos nos 2.3G Dec  9 19:51 ubuntu-2204-kube-v1.27.2-disk-0.vmdk
-rw------- 1 nos nos 2.3G Dec  9 19:53 ubuntu-2204-kube-v1.27.2.ova
-rw-rw-r-- 1 nos nos   64 Dec  9 19:54 ubuntu-2204-kube-v1.27.2.ova.sha256
-rw-rw-r-- 1 nos nos  41K Dec  9 19:51 ubuntu-2204-kube-v1.27.2.ovf
```

## ESXi上のIris Xeの認識

ESXi 8.0u1をインストールすると、Iris Xeは「Intel Corporation VGE compatible Controller」として認識されます。このままでもパススルーデバイスとして構成し、仮想マシンに接続することが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/ff311bac-cde8-2585-f75b-d6882ea5211c.png)

[2023年10月リリースされたESXi 8.0u2](https://docs.vmware.com/jp/VMware-vSphere/8.0/rn/vsphere-esxi-802-release-notes/index.html)以降、「Intel Corporation Raptor Lake-P [Iris Xe Graphics]」として認識されるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/d2c62596-3eb9-207b-49d4-06a73c64fe79.png)

## ノードに対するGPUの接続

ESXiのデバイスとしてRaptor Lake-P [Iris Xe Graphics]のパススルーを有効にして、ESXi上に作成されたノードにPCIデバイスとしてパススルーデバイスを追加します。メモリーの設定も「すべてのゲスト メモリを予約 (すべてロック)」をチェックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/6a42e089-d4e4-1d3c-f8e6-3a7a81b9b7de.png)

## Intel GPU device plugin for Kubernetes

Kubernetes上のPodでIris Xeを利用するには、[Intel GPU device plugin for Kubernetes](https://github.com/intel/intel-device-plugins-for-kubernetes/blob/main/cmd/gpu_plugin/README.md)を利用します。DaemonSetとしてGPUプラグインが作成され、ノードでPCIデバイスとして`i915`が認識されると、GPUドライバーが有効化されます。

[Node Feature Discovery](https://github.com/kubernetes-sigs/node-feature-discovery)をインストールします。

```bash
kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/nfd?ref=v0.28.0'
```

[NodeFeatureRule](https://github.com/intel/intel-device-plugins-for-kubernetes/blob/main/deployments/nfd/overlays/node-feature-rules/platform-labeling-rules.yaml)リソースを作成して、Intel Xeが利用可能なノードにラベルを付与します。

```bash
kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/nfd/overlays/node-feature-rules?ref=v0.28.0'
```

作成されたNodeFeatureRuleの[intel-dp-devices](https://github.com/intel/intel-device-plugins-for-kubernetes/blob/v0.28.0/deployments/nfd/overlays/node-feature-rules/node-feature-rules.yaml#L52-L62)と[intel-gpu-platform-labeling](https://github.com/intel/intel-device-plugins-for-kubernetes/blob/v0.28.0/deployments/nfd/overlays/node-feature-rules/platform-labeling-rules.yaml#L18-L48)により、PCIデバイスとしてIris Xeが存在するノードにGPU機能とGPUの数等がラベルとして付与されます。

```yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureRule
metadata:
  name: intel-dp-devices
spec:
  rules:
    - labels:
        intel.feature.node.kubernetes.io/gpu: "true"
      matchFeatures:
      - feature: pci.device
        matchExpressions:
          class:
            op: In
            value:
            - "0300"
            - "0380"
          vendor:
            op: In
            value:
            - "8086"
      - feature: kernel.loadedmodule
        matchExpressions:
          i915:
            op: Exists
      name: intel.gpu
```

```yaml
apiVersion: nfd.k8s-sigs.io/v1alpha1
kind: NodeFeatureRule
metadata:
  name: intel-gpu-platform-labeling
spec:
  - labelsTemplate: |
      {{ range .pci.device }}gpu.intel.com/device-id.{{ .class }}-{{ .device }}.present=true
      {{ end }}
    matchFeatures:
    - feature: pci.device
      matchExpressions:
        class:
          op: In
          value:
          - "0300"
          - "0380"
        vendor:
          op: In
          value:
          - "8086"
    name: intel.gpu.generic.deviceid
  - labelsTemplate: gpu.intel.com/device-id.0300-{{ (index .pci.device 0).device }}.count={{
      len .pci.device }}
    matchFeatures:
    - feature: pci.device
      matchExpressions:
        class:
          op: In
          value:
          - "0300"
        vendor:
          op: In
          value:
          - "8086"
    name: intel.gpu.generic.count.300
```

Nodeに以下のラベルが追加されたことを確認します。(クラスID:0300とデバイスID:a7a0をキーに「`generic rule for older and upcoming devices`」ルールによりラベルが付与されています。Maxシリーズ等のプロダクト名までラベルが付与できるようです。)

-  "intel.feature.node.kubernetes.io/gpu": "true"
-  "gpu.intel.com/device-id.0300-a7a0.present": "true"
-  "gpu.intel.com/device-id.0300-a7a0.count": "1"

```bash
$ kubectl get node -o json | jq '.items[].metadata.labels'
{
  "beta.kubernetes.io/arch": "amd64",
  "beta.kubernetes.io/os": "linux",
  "gpu.intel.com/device-id.0300-a7a0.count": "1",
  "gpu.intel.com/device-id.0300-a7a0.present": "true",
  "intel.feature.node.kubernetes.io/gpu": "true",
  "kubernetes.io/arch": "amd64",
  "kubernetes.io/hostname": "ec-worker",
  "kubernetes.io/os": "linux"
}
$ kubectl get node -L intel.feature.node.kubernetes.io/gpu
NAME        STATUS   ROLES    AGE   VERSION   GPU
ec-worker   Ready    <none>   30m   v1.27.2   true
```

GPUプラグイン(intel-gpu-plugin)をインストールします。

```bash
kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/gpu_plugin/overlays/nfd_labeled_nodes?ref=v0.28.0'
```

[DaemonSet]((https://github.com/intel/intel-device-plugins-for-kubernetes/blob/5f305b4a96314c5486256baa330434ad48d147e3/deployments/gpu_plugin/overlays/nfd_labeled_nodes/add-nodeselector-intel-gpu.yaml#L5-L9))としてintel-gpu-pluginが作成され、ラベルとして`intel.feature.node.kubernetes.io/gpu=true`が付いているノードでPodが起動します。

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: intel-gpu-plugin
      nodeSelector:
        intel.feature.node.kubernetes.io/gpu: "true"
        kubernetes.io/arch: amd64
```

```bash
$ kubectl get daemonsets
NAME               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                                                        AGE
intel-gpu-plugin   1         1         1       1            1           intel.feature.node.kubernetes.io/gpu=true,kubernetes.io/arch=amd64   18s
$ kubectl get pod -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP           NODE        NOMINATED NODE   READINESS GATES
intel-gpu-plugin-vpql2   1/1     Running   0          2m57s   10.95.0.12   ec-worker   <none>           <none>
```

ノードのAllocatableに`"gpu.intel.com/i915": "1"`が追加され、PodからGPUが利用可能になりました。

```bash
$ kubectl get node ec-worker -o=json | jq '.status.allocatable'
{
  "cpu": "6",
  "ephemeral-storage": "75559189584",
  "gpu.intel.com/i915": "1",
  "hugepages-1Gi": "0",
  "hugepages-2Mi": "0",
  "memory": "20367964Ki",
  "pods": "110"
}
```

## OpenVINOコンテナイメージの作成

Kubernetes上で、OpenVINOの[openvino_notebooks](https://github.com/openvinotoolkit/openvino_notebooks)を参考作成した[Dockerfilie](https://github.com/masanara/openvino_notebooks/blob/masanara/Dockerfile.ubuntu)を利用してコンテナイメージを作成します。

```bash
git clone --depth 1 --branch masanara https://github.com/masanara/openvino_notebooks.git
cd openvino_notebooks
docker build -t ov:2.1 -f Dockerfile.ubuntu .
```

Pod内で[Stable DiffusionのNotebook](https://github.com/openvinotoolkit/openvino_notebooks/blob/main/notebooks/236-stable-diffusion-v2/236-stable-diffusion-v2-text-to-image-demo.ipynb)を実行すると、Stable DIffusionのモデルをHugging Faceからダウンロードしてくるため、Pod再作成のたびに数GBのモデルダウンロードが発生します。Pod内でダウンロードしたモデルのキャッシュをPVCに配置できるよう、Dockerfileでは`ENV HOME /model`として、Kubernetesにデプロイする際に`/model`ディレクトリにPVCをマウントします。

## OpenVINO Podの作成

今回はシングルノードのKubernetesを利用しているため、モデルキャッシュ用のPVCを利用するために[local-path-provisioner](https://github.com/rancher/local-path-provisioner)を有効にします。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.25/deploy/local-path-storage.yaml
$ kubectl get pod -n local-path-storage
NAME                                     READY   STATUS    RESTARTS   AGE
local-path-provisioner-c79c98b44-pkmhp   1/1     Running   0           1m
$ kubectl get sc                                          <aws:m-nara@f22nos-og-ca>
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                    2m
```

PersistentVolumeClaim、Deployment、Serviceを作成します。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/masanara/openvino_notebooks/masanara/manifests/pvc.yaml
$ kubectl apply -f https://raw.githubusercontent.com/masanara/openvino_notebooks/masanara/manifests/openvino_notebooks.yaml
```

リソースが作成されたことを確認します。

```bash
$ kubectl get pod
NAME                                  READY   STATUS    RESTARTS   AGE
intel-gpu-plugin-vpql2                1/1     Running   0          111m
openvino-notebooks-6f44bf6ff9-jkztj   1/1     Running   0          7m55s
$ kubectl get pvc
NAME    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
model   Bound    pvc-e67b4eb6-9372-4ba6-ac3f-0e86cef3e884   20Gi       RWO            local-path     24m
$ kubectl get svc
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
kubernetes           ClusterIP   10.96.0.1        <none>        443/TCP          144m
openvino-notebooks   NodePort    10.109.147.120   <none>        8888:30008/TCP   8m4s
```

## Stable Diffusionによるイメージの生成

NodePort経由でJupyter Notebookにアクセスして、NotebookからGPUを認識できていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/7dbe477a-a1ac-f3a5-9482-883edb7c368a.png)

`/236-stable-diffusion-v2/236-stable-diffusion-v2-text-to-image-demo.ipynb`を開いて、GPUを利用して実行すると、途中で「Kernel Restarting」が発生して画像の生成を完了することができませんでした。(CPU利用時は成功。Podのメモリー割当量を増やしましたが改善せず。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/f31ee8fd-969d-55ff-b21b-cbfd644fc21f.png)

[Notebookの内容をPythonコードに修正](https://github.com/masanara/openvino_notebooks/blob/masanara/notebooks/236-stable-diffusion-v2/236-stable-diffusion-v2-text-to-image-demo.py)して実行してみました。GPUを使った場合74秒程度でイメージ生成できましたが、CPU処理では440秒以上かかっています。

|                  | CPU利用時  | GPU利用時 |
| ---------------- | ---------- | --------- |
| イメージ作成時間 1 | 442.61 sec | 74.50 sec |
| イメージ作成時間 2 | 443.07 sec | 74.70 sec |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/ff8cc573-edc9-477d-9c9c-7a871f8038f2.png)

生成したイメージはNotebook内で表示可能です。

```python
from IPython.display import Image
file="result.png"
Image(file)
```
:::note info
text_prompt = "Shibuya city filled by many cats, epic vista, beautiful landscape, 4k, 8k"
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27668/171ad5d8-6cee-d7e0-e123-66424902a81b.png)

## まとめ

Kubernetes上のPodから、Intel CPUに統合されたGPUを仮想マシンにパススルーして、仮想マシンでKubernetesを構成してコンテナ上でGPUをつかって画像生成することができました。
ESXiでパススルーして、仮想マシン上内のプログラムからGPGPUとして利用する方法は[こちら](https://qiita.com/masanara/items/b68fbdfd38484773ed37)で説明しています。