Ubuntu Weekly Recipe

第642回仮想マシン上のmicrok8sからGPUを利用する

第641回ではLXDとmicrok8sでシングルサーバーをKubernetesクラスターにすると題して、より高機能になったmicrok8sについて紹介しました。microk8sで構築したKubernetesがあれば、気軽にたくさんのCPUコアを使ってさまざまなワークロードを動かし、部屋を暖められます。でも、待ってください。そのサーバーにはもっと便利な熱源がありませんか? そうGPUですね。本格的に寒くなる前に、Kubernetes環境からもGPUを使えるようにしましょう。

ホスト側でGPUパススルーの準備

第641回ではLXDの仮想マシンインスタンスの上にmicrok8sをインストールしました。つまりホストマシンのGPUを使うには、LXDの仮想マシンの中からGPUにアクセスできなくてはなりません[1]⁠。コンテナの場合、第532回のLXDのコンテナからGPUを利用するなどで手順を紹介していますが、実はLXD 4.3から仮想マシンインスタンスでも同じ手順でGPUパススルーをサポートするようになったのです。

この方法の注意点はふたつあります。まず、LXD 4.3はフィーチャーリリースであること。LXDにはLTSリリースとフィーチャーリリースの2種類のリリースブランチが存在します。LTSリリースはUbuntuのLTSと同じタイミング(つまり2年周期)でリリースされるリリースブランチで、メンテナンスもそれに合わせて行われます。Ubuntuサーバーをインストールしたときに一緒についてくるのは、LTSリリースのLXDです。現在はLTS 2.0、3.0、4.0のLTSリリースが存在します。

それに対してフィーチャーリリースは、ほぼ1ヶ月周期でリリースされる機能追加ブランチです。次のLTSリリースに向けてさまざまな新機能を追加しているため、どんどんと仕組みが更新されています。このようにフィーチャーリリースは安定性を求める環境には向かない点に注意が必要です。

もうひとつの注意点はGPUが仮想マシンに占有されてしまうことです。仮想マシンの場合、IOMMU(IntelのVT-dやAMDのAMD-Vi)を用いてPCIパススルーで仮想マシン上からGPUをアクセスできるようにするため、そのGPUを使えるのは設定した仮想マシンインスタンスのみとなります。コンテナインスタンスの場合は、GPUを複数のインスタンスで共有できますが、仮想マシンの場合はインスタンスの数だけGPUを用意してください。

よってまずはLXDをフィーチャーリリースに変更します。新規にインストールする際はsnap installのオプションに--channel=latest/stableとつけるだけです。すでにインストール済みの環境であれば、次のようにrefreshしてください。

$ sudo snap refresh --channel=latest/stable lxd
lxd 4.7 from Canonical✓ refreshed

$ lxd --version
4.7

これでLXD側の準備は整いました。次にカーネル側の準備を行います。Intelの場合、IOMMU機能は有効化されていません。よってIntel CPUを使っている場合、カーネルのコマンドラインからIOMMU機能を有効化します。ちなみにAMDの場合は最初から有効化されているようです。

/etc/default/grubGRUB_CMDLINE_LINUXを次のように変更してください。

GRUB_CMDLINE_LINUX="intel_iommu=on iommu=pt"

あとはGRUBの変更を反映します。

$ sudo update-grub

またNVIDIAのGPUを利用している場合、Nouveauドライバーをロードしないようにしておいてください。

$ echo "blacklist nouveau" | sudo tee -a /etc/modprobe.d/blacklist-nouveau.conf

ここまで実行したら、一度ホストシステムを再起動しておきましょう。起動後に次のようにIOMMU enabledが表示されればOKです。

$ dmesg | grep IOMMU
[    0.114164] DMAR: IOMMU enabled
[    0.217696] DMAR-IR: IOAPIC id 2 under DRHD base  0xfed91000 IOMMU 1

もし表示されない場合、チップセットが対応していない可能性もあります。まずはBIOSメニューでIOMMUが有効化されているか確認してみましょう。

仮想マシンインスタンスからGPUを見えるようにする

GPUを使うための仮想マシンインスタンスを用意します。このあたりは第641回とほぼ同じです。

$ lxc launch ubuntu:20.04 kubeflow --vm -c limits.cpu=4 -c limits.memory=16GiB
$ lxc exec kubeflow -- sed -i 's/archive.ubuntu/jp.archive.ubuntu/' /etc/apt/sources.list
$ lxc exec kubeflow -- timedatectl set-timezone Asia/Tokyo
$ lxc exec kubeflow -- sh -c 'apt update && apt full-upgrade -y'

インスタンス名が「kubeflow」ですが残念ながら今回はKubeflowは関係ありません[2]⁠。タイムゾーンの設定やインスタンスのスペックを上げているのも、今回の話とは関係ありません。

次に一度インスタンスを停止して、GPUをインスタンスに追加します。仮想マシンインスタンスの場合このあたりの設定変更の際は、毎回インスタンスを停止しなくてはならないので少し面倒です。

$ lxc stop kubeflow
$ lxc config device override kubeflow root size=150GiB
$ lxc config set kubeflow security.secureboot=false
$ lxc config device add kubeflow nv gpu vendorid=10de
$ echo -n '-cpu host,kvm=off' | lxc config set kubeflow raw.qemu -
$ lxc start kubeflow

ストレージを増やしていますが、これもスペック等と同じで関係ありません。重要なのはその次の行からです。

まずsecurity.secureboot=falseでセキュアブートを無効化しています。LXDの仮想マシンインスタンスは、初期状態だとセキュアブートが有効化されています。普段使う分には問題ありませんが、あとで説明するようにサードパーティのドライバーをインストールしたい場合(つまりはDKMSを使いたい場合)には若干手間がかかります。具体的にはDKMSインストール時にパスワードを設定し、再起動後にコンソールからそのパスワードを入力する必要があるのです。LXDの場合、lxc consoleで仮想マシンインスタンスのコンソールにアクセスできるものの、手間とメリットを考えるとセキュアブートを切ってしまうのもひとつの手でしょう。

次にlxc config device addでベンダーIDが「0x10de」なPCIデバイスをnvという名前でインスタンスに紐づけています。ここで言う「0x10de」はNVIDIAのベンダーIDです。具体的には次のコマンドで確認できます。

$ lspci -nn | grep -i nvidia
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107 [GeForce GTX 1050 Ti] [10de:1c82] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation GP107GL High Definition Audio Controller [10de:0fb9] (rev a1)

他にもproductidのようにデバイスIDやPCIのアドレスなど、より細かい粒度でも指定可能です。複数のGPUが繋がったサーバーだとこれらの指定が必要になるでしょう。詳しいことはLXDのドキュメントのGPUデバイスに関する項目を参照してください。

最後は仮想マシンの中でNVIDIAのGPUを使うためのオプションとなります。raw.qemuオプションに-cpu host,kvm=offを設定しています。LXDではraw.qemuに任意の文字列を設定することで、仮想マシンを起動する際のQEMUコマンドのパラメーターを変更可能です。kvm=offは仮想マシンインスタンスが「仮想マシンであること」を隠すためのオプションです。

どんな仮想マシンシステムもゲストの中からCPUID命令を発行することで、⁠自分自身がどんなハイパーバイザー上で動いているか」がわかる仕組みが存在します。Ubuntuだとcpuidパッケージで提供されているcpuidコマンドを使うと、たとえばhypervisor guest status = truehypervisor_id = "KVMKVMKVM "とセットされることが確認できます。どうやらNVIDIAのツールは「KVMインスタンスであること」を検知すると「Unknown error」を発生させるようです。そこでQEMUにはこのKVMシグネチャを表示しないオプションが追加されました。これがCPUオプションのkvm=offです。

設定後にインスタンスを起動したら、インスタンスの中からもGPUデバイスが見えていることを確認しましょう。

$ lxc exec kubeflow -- lspci -nn | grep -i nvidia
06:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107 [GeForce GTX 1050 Ti] [10de:1c82] (rev a1)
06:00.1 Audio device [0403]: NVIDIA Corporation GP107GL High Definition Audio Controller [10de:0fb9] (rev a1)

サーバー向けNVIDAドライバーのインストール

インスタンスの中からGPUデバイスが見えるようになったので適切なデバイスドライバーをインストールします。AMD系であれば標準のドライバーで問題ないはずですし、第471回第524回でも紹介しているように、プロプライエタリなドライバーも用意されています。それに対してNVIDIAはプロプライエタリーなドライバー一択です。

具体的なインストール方法は第454回のUbuntu 16.04 LTSにNVIDIA製ドライバーをインストールする3つの方法で紹介しているものの、当時と状況が若干異なっています。まずパッケージ名が「nvidia-XXX」から「nvida-driver-XXX」に変更されました。さらに「nvidia-driver-XXX-server」なるパッケージも追加されています。この「nvidia-driver-XXX-server」は中身が少し異なる(Powerサーバー向け対応が入っている)ものの、中身そのものはほとんど「nvidia-driver-XXX」と同じです。

では、何が違うのかと言うと新バージョンが出た時の挙動です。たとえば2020年11月時点での最新は「nvidia-driver-450」でその一つ前は「nvidia-driver-440」となっています。これらは現時点で次のような形になっています。

  • nvidia-driver-440:nvidia-driver-450に依存するだけの移行用ダミーパッケージ
  • nvidia-driver-440-server:NVIDA Driver 440.95.01をインストールする
  • nvidia-driver-450:NVIDA Driver 450.80.02をインストールする
  • nvidia-driver-450-server:NVIDA Driver 450.80.02をインストールする

つまり「nvidia-driver-XXX」をインストールした場合、新しいドライバーがリリースされたら自動的にアップグレードされます。それに対して「nvidia-driver-XXX-server」の場合はアップグレードされません。サーバーの用にドライバーが更新されることを防ぎたい場合に「nvidia-driver-XXX-server」は便利なパッケージなのです。

さらにサーバー向けとして「nvidia-headless-XXX」「nvidia-headless-XXX-server」が追加されました。これらはX Window System/Wayland関連のパッケージに依存しないため、サーバーのようなヘッドレスマシンにおいてGPGPU目的でNVIDIAのドライバーをインストールしたい際に便利です。

ここからはNVIDIAのGPU前提で話を進めます。まずはドライバーのインストールです。次のコマンドでインストールできるバージョンを確認して、必要なものをインストールしてまいましょう。

インストール可能なバージョンを確認する:
$ lxc exec kubeflow -- apt search "^nvidia-headless-[0-9]"

最新もしくは必要なバージョンをインストールする:
$ lxc exec kubeflow -- apt install -y nvidia-headless-450
  => 今回はバージョンアップを許容して「-server」がついていない方を選択

念のため再起動する:
$ lxc restart kubeflow

ロードされていることを確認する:
$ lxc exec kubeflow lsmod | grep nvidia
nvidia_uvm            847872  0
nvidia_modeset       1060864  0
nvidia              18190336  18 nvidia_uvm,nvidia_modeset
nvidia_drm             12288  0

ちなみに実機にNVIDIAドライバーをインストールしたときと比べるとDRMやKMS関連のドライバーがロードされていないことに気がつくかもしれません。これはLXDの仮想マシンインスタンスで使用しているカーネル(linux-image-kvm)は通常のカーネル(linux-image-generic)と異なり、CONFIG_DRMなどサーバーで使うことが少ないコンフィグを軒並み無効化しているためです。もし、何らかの理由で通常のカーネルが必要になった場合は次のようにカーネルを差し替えてください。

$ lxc exec kubeflow -- sh -c 'apt install -y linux-generic'
$ lxc exec kubeflow -- sh -c 'apt remove -y ~nlinux-.*kvm'
  => 実行中のカーネルパッケージを削除することになるので、
     削除を中断するか(Abort kernel removal?)という問い合わせが表示される。
     消すのが正しいのでNoを選択する。
     GRUBメニューから通常のカーネルで起動したあとに消しても良い。

$ lxc restart kubeflow

恒例のnvidia-smiコマンドで動作確認しましょう。

$ lxc exec kubeflow -- apt install nvidia-utils-450
$ lxc exec kubeflow nvidia-smi
Sat Nov 14 20:52:25 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.80.02    Driver Version: 450.80.02    CUDA Version: 11.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  GeForce GTX 105...  Off  | 00000000:06:00.0 Off |                  N/A |
|  0%   49C    P0    N/A /  72W |      0MiB /  4040MiB |      1%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

もしUnable to determine the device handle for GPU 0000:06:00.0: Unknown Errorのようなエラーメッセージが表示される場合、何か設定に問題があります。一番可能性が高いのが、KVM上と判定されたケースなので、前項で説明したようにLXDのraw.qemu設定に-cpu host,kvm=offを追加できているか確認してください。一番手っ取り早いのはホスト上でps -fe | grep kvm=offすることです。

microk8sでGPUを有効化する

前回と同じようにmicrok8sをインストールしましょう。

$ lxc exec kubeflow -- snap install microk8s --classic --channel=1.19
microk8s (1.19/stable) v1.19.2 from Canonical✓ installed
$ lxc exec kubeflow -- usermod -a -G microk8s ubuntu

GPUの有効化もコマンド一発です。

$ lxc exec kubeflow -- microk8s enable gpu
Enabling NVIDIA GPU
NVIDIA kernel module detected
Enabling DNS
Aispplying manifest
serviceaccount/coredns created
configmap/coredns created
deployment.apps/coredns created
service/kube-dns created
clusterrole.rbac.authorization.k8s.io/coredns created
clusterrolebinding.rbac.authorization.k8s.io/coredns created
Restarting kubelet
DNS is enabled
Applying manifest
daemonset.apps/nvidia-device-plugin-daemonset created
NVIDIA  enabled

いろいろ準備するためにそれなりに時間がかかります。気長に待ちましょう。無事にセットアップが完了したら、kube-system名前空間において、NVIDIAデバイスプラグインのPodが動いていることが確認できます。

$ lxc exec kubeflow -- microk8s kubectl get pods -A | grep nvidia
kube-system   pod/nvidia-device-plugin-daemonset-k6ps7      1/1     Running   0          16m

試しにmicrok8sのドキュメントにもあるcuda-vector-addを使ってみましょう。これは単にCUDAのAPIを使ってSIMD演算するだけのPodです。

$ cat <<'EOF' > gpu-sample.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cuda-vector-add
spec:
  restartPolicy: OnFailure
  containers:
    - name: cuda-vector-add
      image: "k8s.gcr.io/cuda-vector-add:v0.1"
      resources:
        limits:
          nvidia.com/gpu: 1
EOF

あとはPodを作成して、状態を確認してみます。

$ cat gpu-sample.yaml | lxc exec kubeflow -- microk8s kubectl create -f -
pod/cuda-vector-add created

$ lxc exec kubeflow -- microk8s kubectl get pod
NAME              READY   STATUS      RESTARTS   AGE
cuda-vector-add   0/1     Completed   0          9s

cube-vector-addはいわゆるデーモンなどではなくCUDAの演算をして終了するだけのコマンドです。kubectl createしたあとはイメージのダウンロードなどでしばらく時間がかかりますが、最終的に「Completed」で完了します。完了したら削除しておきましょう。

$ lxc exec kubeflow microk8s kubectl delete pod cuda-vector-add

たとえば意図的にGPUを削除した上で実行すると、⁠Pendig」で停止します。

GPUを削除する:
$ lxc stop kubeflow
$ lxc config device remove kubeflow nv
$ lxc start kubeflow

もう一度Podを作成する:
$ cat gpu-sample.yaml | lxc exec kubeflow -- microk8s kubectl create -f -

ペンディングになっていることがわかる:
$ lxc exec kubeflow -- microk8s kubectl get pod
NAME              READY   STATUS    RESTARTS   AGE
cuda-vector-add   0/1     Pending   0          22s

kubectl decribeで確認すると、GPUを確保しようとして待っていることがわかります。

$ lxc exec kubeflow -- microk8s kubectl describe pod cuda-vector-add
(中略)
Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  56s (x2 over 56s)  default-scheduler  0/1 nodes are available: 1 Insufficient nvidia.com/gpu.

YAMLファイルの中でnvidia.com/gpu: 1と書いているため、当然と言えば当然です。さらにこの部分をコメントアウトしてGPUがなくても強制的に実行しようとすると、次のようにエラー終了します。

$ lxc exec kubeflow -- microk8s kubectl get pod
NAME              READY   STATUS              RESTARTS   AGE
cuda-vector-add   0/1     RunContainerError   2          23s

kubectl describeを実行すれば、CUDA関連のエラーが残っているはずです。

以上のように正しく設定してあるときちんとGPUがコンテナから見えて、CUDAも使えることがわかりました。

ここから先はGPUが使えるKubernetes環境なので、好きなコンテナイメージで、インスタンス代を気にすることなく好きなだけたくさんGPUを動かして、寒い冬を乗り切りましょう![3]

おすすめ記事

記事・ニュース一覧