サルでもできるGPUパススルー

公開:2020/12/13
更新:2020/12/14
7 min読了の目安(約6900字TECH技術記事

*これはKCS AdventCalender2020 13日目の記事です*

12日目が欠番なので11日目へのリンクを貼っておきます

←11日目 | 14日目→


これはなに

仮想マシン上でGPUを使いたいとき,ありますよね?
それで調べてみると,情報が色々と錯綜していてつらくなった思い出は誰しも抱いてらっしゃると思います.
(ん?お持ちでない?思い出が無いならつくりましょう.自家製サーバはいいぞ.)

今回はそんなGPUのパススルーについて,ホストマシンでやるべきことを書いていこうと思います.
(GPUに限らずPCIデバイスであれば同じ方法でパススルー可能です)

環境

  • Ubuntu 18.04 LTS (Linux Kernel 4.15)
    • ディストリビューションによってはGRUB2のconfigが配置されているディレクトリが違います
      適宜読み替えてください
  • GRUB2
    • 最近のディストリビューションならデフォルトでGRUB2になっていると思います
  • Intel Xeon Silver 4208 x2
    • CPUのベンダーは後ほど関係してきます
  • NVIDIA RTX 2080Ti x2
  • ASPEED AST2500 BMC (ホストの映像出力用)

別の環境でもテストを行ったため,そちらも載せておきます

  • Ubuntu 18.04 LTS (Linux Kernel 5.4)
  • AMD Ryzen Threadripper 1950X
  • NVIDIA RTX 2070 SUPER
  • NVIDIA GTX 750Ti (ホストの映像出力用)

仮想マシンにパススルーしたGPUはホストから一切使用できなくなるので,ホストの映像出力が必要な場合は別途用意しておきましょう.
(仮想化ソフトウェアによっては一応ホストと共有する方法はあるのですが,パフォーマンスがガタ落ちするためあまりおすすめしません.)
また後述する仮想マシンの作成・起動にはVagrant + libvirt + KVMを使用しています.もちろんVirtualBoxやVMWareなどの別の方法でも大丈夫です

やってみる

やること

  • 仮想化支援機能・IOMMUが使えるようにBIOS / UEFIを設定する
  • プロプライエタリなドライバ(cuda-driversなど)が入ってる場合はアンインストール
  • GPUをホストが使えないようにする(ブラックリストに入れる)
  • 必要に応じてカーネルオプションを変更
  • 起動時に読み込まれるGRUBの設定に,変更したブラックリスト / カーネルオプションを反映させる

おおまかこんな流れです.

では順にやっていきましょう

BIOS / UEFIの設定

え?BIOSよくわからんって?大丈夫です.特に難しいことはしません.
まずはCPUの仮想化支援機能からみていきましょう.

お使いのマシンのCPUが

  • IntelならIntel Virtualization Technology (VT)
  • AMDならAMD-V/SVM mode

というような項目があると思います.まずはこれを有効化しましょう.有効化することで仮想マシンの実行速度が目に見えて速くなります.

次にIOMMU[1]を有効化しましょう.
こちらは

  • IntelならIntel Virtualization Technology for Directed I/O (VT-d)
  • AMDならIOMMU

を有効化してください.
また,同時にAbove 4G Decoding[2]という項目も有効化してください.

また,Secure Bootを有効化しているとトラブることがあります.特に支障がなければ無効化しておきましょう.

設定できたらマシンを再起動し,ログインしてください.

デバイスドライバのアンインストール

nouveauなどの汎用ドライバを使用している,もしくはホストの映像出力に使用するGPUがパススルーするGPUと同じドライバを使用している(上記の2つ目の構成のような場合)のであれば,この手順は飛ばしていただいて大丈夫です.

私の場合,cuda-drivers + nvidia-container-toolkitでコンテナにGPUへのアクセスを与えていたため.まずはこのNVIDIAのドライバをアンインストールします

$ sudo apt remove --purge -y cuda-drivers nvidia-container-toolkit \
  && sudo apt autoremove --purge -y

# プロプライエタリドライバ関連のパッケージが残っていないか確認
$ apt list -i | grep -e nvidia -e cuda 

カーネルオプションの設定

GPUを仮想マシン内から利用するためVFIOを使用します.
先ほどBIOSからIOMMUを有効化したのはそのためです.

まず起動時にGRUBに渡すオプションを変更します.

$ sudo sed -i -e \
    "s/^GRUB_CMDLINE_LINUX=\"\"$/GRUB_CMDLINE_LINUX=\"quiet splash intel_iommu=on vfio_iommu_type1.allow_unsafe_interrupts=1 iommu=pt\"/g" \
    /etc/default/grub

また,以下の設定を書き込んだファイルを/etc/modprobe.d/に配置します.

/etc/modules-load.d/vfio-pci.conf
pci_stub
vfio
vfio_iommu_type1
vfio_pci
kvm
kvm_intel

完了したら設定を反映させましょう.

$ sudo update-grub
$ sudo reboot # マシンを再起動する必要があります

GPUをブラックリストに追加

以下ではNVIDIAのGPUを使用しています.
AMDの場合は随時読み替えてください.

まず,lspci -nn | grep -i nvidiaでGPUのPCI IDを調べます.

$ lspci -nn | grep -i nvidia
3b:00.0 VGA compatible controller [0300]: NVIDIA Corporation GV102 [10de:1e07] (rev a1)
3b:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:10f7] (rev a1)
3b:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1ad6] (rev a1)
3b:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1ad7] (rev a1)
af:00.0 VGA compatible controller [0300]: NVIDIA Corporation GV102 [10de:1e07] (rev a1)
af:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:10f7] (rev a1)
af:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1ad6] (rev a1)
af:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1ad7] (rev a1)

ここで出力の表示形式は以下のようになっています.

<PCIアドレス> <デバイスクラス> [<クラスコード>]: <デバイス名> [ベンダーID:デバイスID] <リビジョン番号>

ベンダーID:デバイスIDの形式で表されたものをPCI IDといいます.
上記の出力が得られた場合,10de:1e0710de:10f710de:1ad610de:1ad7が使用するPCI IDになります.

パススルーするGPUについては,VGA compatible controllerだけでなく付随するPCIデバイス(USB controller等)もすべてパススルーする必要があります.

なお,grepで絞り込みをしなくてもlspci -d <ベンダーID>:<デバイスID>で絞り込むことができるので,例えばNVIDIAのGPUなら

$ lspci -nn -d 10de:

でPCIデバイスを絞り込みことができます.
ここで10deはNVIDIAのベンダーIDです.AMDの場合は1002になります.
PCIデバイスのベンダーID・デバイスIDはあらかじめ決められているため,こちらのサイトなどで検索することもできます.

パススルーするPCIデバイスのPCI IDがわかったら,それらを/etc/modprobe.d/に作成した適当なファイルに書き込んでいきましょう.

/etc/modprobe.d/vfio.conf
options vfio-pci ids=10de:1e07,10de:10f7,10de:1ad6,10de:1ad7

後はinitramfsを更新するだけです.

$ sudo update-initramfs -u

ビルドが完了したらマシンを再起動しましょう.

$ dmesg | grep -i vfio

を実行して出力が得られればOKです.
あとはお好みの仮想化ソフトウェアを使ってGPUのパススルーができます.簡単ですね!

実際にパススルーしてみる

最初に述べたように今回はVagrantでお手軽にやっていきます.
使用したバージョンは

  • Vagrant: 2.2.10
  • vargant-libvirt: 0.2.1
  • QEMU: 2.11

です.
Vagrant等のインストールは済んでいる前提で進めていきます.

まず適当なVagrantfileを用意します.boxはlibvirt用のものを使用してください.

Vagrantfile
Vagrant.configure("2") do |config|

  config.vm.box = "generic/ubuntu1804"

  config.vm.provider "libvirt" do |v|
    v.kvm_hidden = true
    # GPUパススルーを行う際はこのオプションをtrueにする必要があります
  end

  config.vm.define :"gpu-instance-0" do |domain|
    domain.vm.provider "libvirt" do |v|
      v.cpus = 4
      v.memory = 8192
      v.pci :domain => '0x0000', :bus => '0x3b', :slot => '0x00', :function => '0x0'
      v.pci :domain => '0x0000', :bus => '0x3b', :slot => '0x00', :function => '0x1'
      v.pci :domain => '0x0000', :bus => '0x3b', :slot => '0x00', :function => '0x2'
      v.pci :domain => '0x0000', :bus => '0x3b', :slot => '0x00', :function => '0x3'
    end
  end
end

ここで最も重要なのはv.kvm_hiddenオプションを有効化することです.
GPUが仮想マシン上で動いていることを検知すると,プロプライエタリなデバイスドライバをゲスト上でインストールできなくなることがあります.
そのため,仮想マシンであることをゲストOSが検知しないよう,このオプションを有効化します.

また,PCIアドレスは前項でlspciを叩いて表示したものを使用します.
上記のVagrantfileの場合だと

3b:00.0 VGA compatible controller [0300]: NVIDIA Corporation GV102 [10de:1e07] (rev a1)
3b:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:10f7] (rev a1)
3b:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1ad6] (rev a1)
3b:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1ad7] (rev a1)

の4つを使用しています.

パススルーしたいGPUに付随するデバイスもすべてパススルーさせる必要があります.
また,すべてのアドレスは16進数の接頭辞0xを付けて記述する必要があります.

必要な設定項目を書き加えたら,vagrant upで仮想マシンを立ち上げてみましょう.
仮想マシンにログインしてlspciなどを叩いてみるとGPUがパススルーされていることが確認できると思います.

今回の内容はこちらのリポジトリにまとめて置いてあります.ご自由にお使いください.

以上です.


参考:


←11日目 | 14日目→

脚注
  1. IOMMUは,仮想マシン上のメモリが持つ仮想アドレスと,ホストマシンのメモリが持つ物理アドレスの対応づけ(マッピング)を行う機能です. ↩︎

  2. Above 4G Decodingは,仮想アドレスから物理アドレスへのマッピングを4GiB以上の領域でも可能にするためのオプションです. ↩︎