GPUが1枚のマシンでもPCIパススルーして、ゲームができるWindowsのVMを作る

投稿者: | 2022年9月2日

OpenJDKを読む時間がStableDiffusionに奪われてしまいました。
部屋が熱くなるのでこの手のものは冬場にやるほうが賢そうです。

今回は自分の計算機の構成について解説してみます。GPUをPCIパススルーしてしまい、GPUをホストOSとゲストOSで共有するような環境です。redditとか見ても解説している記事が多くはなく日本語に至っては見つけられませんでした。一定の価値があるかなと思って記録がてら書いています。

環境を作った動機

普段Linuxを使っていることが多いのですが、以下の場合にはWindowsを利用する必要があります。

  • ゲームをやりたい場合
  • Microsoft Officeを利用する場合
  • Windowsでなければソフトウェアが無い場合

ゲームについては、Protonなどの進化もあってLinux上で困ることは少なくなっているのですが、アンチチートシステムを採用しているゲームなどは基本的に動きません。Officeも同様でMicrosoft 365のお陰でブラウザ上でほとんど完結することが多いのですが、稀にネイティブ版のOfficeでなければレイアウトが崩れるなどの場合もあります。

というわけで、Linuxだけで生活が完結しておらずWindowsを利用することがそれなりにあります。デュアルブートしてしまう、複数台PCを持つなどの手段で解決しても良いとは思います。

今、自分もLinuxデスクトップとWindowsデスクトップの両方を利用しています。ただ、この手段は自分のコンテキストスイッチの負荷が高いです。両方のマシンで同じ入力デバイスを使いたいので、USB切替器で切り替えたりディスプレイの切り替えなどが発生します。

デュアルブートだとこれらは改善されますが、これは環境の維持コストが結構かかります。やっている方はご存知かと思いますが、OSの更新時や追加でOSをインストールするときなどにefi環境を適当に修正されてしまってデュアルブート環境が壊れてしまうことがままあります。また、デュアルブートの場合は片方のOSに入っているデータへのアクセスが大変です。共有領域を用意してコピーと再起動を繰り返すことになったりします。

仮想マシンであれば、気軽に作成できてホストOS側に何等影響を与えず両者が同時に起動しているのでデータのコピーも手軽に可能です。嬉しいことだらけですね。

じゃあ、仮想マシンでやろうと思ったときに課題が発生します。自分はWindowsでゲームをやりたいのです。しかし普通に仮想マシンを作るとゲームが出来るような仮想マシンは作れません。そこで登場するのがPCIパススルーです。

PCIパススルーとは?

仮想マシンなどにホストOSのPCIデバイスを直接利用させる仕組みのことです。
CPUの仮想化関連の機能(IOMMU)が必要で、IntelだとVT-dやAMDの場合はAMD-VIが必要ですが、最近のCPUであれば使えます。今回は、この機能を使って、GPUをPCIパススルーして仮想マシンの中で利用してみようというものです。

自分の環境の課題

PCIパススルーは便利そうですね。では自分もやってみようと考えるわけですが、ここで別の問題が浮上してきます。自分が生活しているマシンに問題があります。スペックは以下のとおりです。

CPUAMD Ryzen 9 3900X
メモリ32GB
GPUGeForce RTX 2060 SUPER
OSArch Linux
自分の環境

ゲームなどをするには十分なスペックのマシンです。何が問題なのでしょうか?
この構成だと、GPUがGeForce RTX 2060 SUPERの1つだけということです。IntelのCPUやAMDでもAPUというGPU付きのCPUがあったりしますが、3900XにはGPUは付いていません。このような環境の場合、PCIパススルーでGPUを仮想マシン側に渡してしまうと、ホストOSからGPUが無くなってしまうので画面が映らなくなります。設定に失敗すると他のマシンからsshしたりして復旧したりする必要があります。

お金がある人は1つのマシンに複数のGPUを刺してしまって仮想マシンに専用のGPUを割り当ててしまって解決したり、CPUにGPUが内蔵されていれば、それを利用させつつPCIに刺さっているGPUを仮想マシン側にPCIパススルーして利用するということが可能です。ちなみに、そういった用途だと、https://looking-glass.io/ というプロジェクトがあります。

以降で、具体的な手順について説明していきます

仕組みの概要

まず、前提としていくつか知らないといけない知識があります。それは libvirt には仮想マシン起動時にhookを書くことができるということです。自分は今回初めて知りました。これを使ってGPUが1つでもどうにか仮想マシンにGPUをPCIパススルーを実現します。

vm起動時には

  • GPUを利用しているプログラムの停止
    • Xを利用しているプログラム(GDM)、仮想コンソール、Framebufferなどを停止
  • GPU関連のカーネルモジュールのunload
  • ホストOSからGPUをdetachする
  • vfioをロードする

VM停止時には

  • vfioをアンロード
  • ホストOSにGPUをattachする
  • GPU関連のカーネルモジュールをloadする
  • GPUを利用しているプログラムの復旧
    • GDMなどの起動、仮想コンソール、Framebufferなどの起動

ということを実施する必要があります。雑に言うと、仮想マシンを起動、停止する度にホストOSからGPUをPCIから引っこ抜いたり刺したりしてるという感じになります。

事前準備

PCIパススルーする対象は、IOMMUグループ内の全てをパススルーしないと上手くいかないことがあります。GPUを刺すとHDMIのオーディオデバイスなどが付いてきますが、そういったものも一緒にPCIパススルーする必要があるという話です。実は回避できたりするようですが、ここではそういうものとして扱います。

#!/bin/bash
shopt -s nullglob
for d in /sys/kernel/iommu_groups/*/devices/*; do
    n=${d#*/iommu_groups/*}; n=${n%%/*}
    printf 'IOMMU Group %s ' "$n"
    lspci -nns "${d##*/}"
done;

上記のスクリプトを実行すると、IOMMU GROUPと対応するPCIデバイスが列挙されると思います。この中からGPUのものを見つけて、同じIOMMU GROUPになっているデバイスを見つけて記録しておきます。
自分の場合は、以下のような感じでGPUが含まれるIOMMU GROUPが設定されていました。

IOMMU Group 25 09:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU106 [GeForce RTX 2060 SUPER] [10de:1f06] (rev a1)
IOMMU Group 25 09:00.1 Audio device [0403]: NVIDIA Corporation TU106 High Definition Audio Controller [10de:10f9] (rev a1)
IOMMU Group 25 09:00.2 USB controller [0c03]: NVIDIA Corporation TU106 USB 3.1 Host Controller [10de:1ada] (rev a1)
IOMMU Group 25 09:00.3 Serial bus controller [0c80]: NVIDIA Corporation TU106 USB Type-C UCSI Controller [10de:1adb] (rev a1)

これらをPCIパススルーの対象にすれば良さそうです。

実際のスクリプト

まず、libvirtは起動するVMMの種類(qemu)とかの名前のスクリプトを起動します。qemuの場合は、/etc/libvirt/hooks/qemu になります。$1、$2、$3に色々情報が入っているので、それを上手くつかって後続の処理を呼びだすようにしておきます。

qemuのhookスクリプト

/etc/libvirt/hooks/qemu

#!/bin/bash

GUEST_NAME="$1"
HOOK_NAME="$2"
STATE_NAME="$3"
MISC="${@:4}"

BASEDIR="$(dirname $0)"

HOOKPATH="$BASEDIR/qemu.d/$GUEST_NAME/$HOOK_NAME/$STATE_NAME"
set -e # If a script exits with an error, we should as well.

if [ -f "$HOOKPATH" ]; then
eval \""$HOOKPATH"\" "$@"
elif [ -d "$HOOKPATH" ]; then
while read file; do
  eval \""$file"\" "$@"
done <<< "$(find -L "$HOOKPATH" -maxdepth 1 -type f -executable -print;)"
fi

VMの起動時のhookスクリプト

起動時に実行するスクリプトはこういう感じになります。
/etc/libvirt/hooks/qemu.d/win10/prepare/begin/start.sh という名前にしたので、仮想マシンの名前が win10 の時だけ実行されるスクリプトになります。

virsh nodedev-detach をする際に、先ほど調べたPCIデバイスのIDが必要になります。09:00.0 は、0000:09:00.0 が正式な記述で、virsh側では:_に置き換えられるようです。この辺は、/sys/bus/pci/devices/ の下を覗いてみたり、virsh nodedev-list --cap pci で利用できるデバイスを見てみたりして決めて下さい。

#!/bin/bash
set -x

# ディスプレイマネージャを止める(環境によってこれは違うはずです)
systemctl stop gdm

# VTconsolesを止める(不要かも)
echo 0 > /sys/class/vtconsole/vtcon0/bind
echo 0 > /sys/class/vtconsole/vtcon1/bind

# フレームバッファも止める
echo efi-framebuffer.0 > /sys/bus/platform/drivers/efi-framebuffer/unbind

# nvidiaのカーネルモジュールをunloadする
modprobe -r nvidia_uvm
modprobe -r nvidia_drm
modprobe -r nvidia_modeset
modprobe -r nvidia
modprobe -r i2c_nvidia_gpu

sleep 2

# Host OSからGPUをdetachする
virsh nodedev-detach pci_0000_09_00_0
virsh nodedev-detach pci_0000_09_00_1
virsh nodedev-detach pci_0000_09_00_2
virsh nodedev-detach pci_0000_09_00_3

#  vfio をロードする
modprobe vfio
modprobe vfio_pci
modprobe vfio_iommu_type1

VMの終了時のhookスクリプト

終了時に実行するスクリプトはこういう感じになります。/etc/libvirt/hooks/qemu.d/win10/release/end/stop.sh という名前にしておきます。

#!/bin/bash
set -x

# vfioを unloadする
modprobe -r vfio
modprobe -r vfio_pci
modprobe -r vfio_iommu_type1

# Host OSからGPUをattachする
virsh nodedev-reattach pci_0000_09_00_0
virsh nodedev-reattach pci_0000_09_00_1
virsh nodedev-reattach pci_0000_09_00_2
virsh nodedev-reattach pci_0000_09_00_3

# nvidiaのカーネルモジュールをloadする
modprobe i2c_nvidia_gpu
modprobe nvidia
modprobe nvidia_modeset
modprobe nvidia_drm
modprobe nvidia_uvm

nvidia-xconfig --query-gpu-info > /dev/null 2>&1

sleep 2

# framebuffer を復旧
echo "efi-framebuffer.0" > /sys/bus/platform/drivers/efi-framebuffer/bind

# VTconsolesを復旧
echo 1 > /sys/class/vtconsole/vtcon0/bind
echo 1 > /sys/class/vtconsole/vtcon1/bind

# gdm を起動
systemctl start gdm

仮想マシン作成

あとは、VirtManagerなりで仮想マシンを作っていきます。この時名前を「win10」という名前にすることを忘れないようにします。

作る時にはいくつか注意点があります。

  • チップセットをQ35にして、ファームウェアはUEFIを選ぶ。
    • これがデフォルトのはず
  • CPU設定で、CPUのモデルはhost-passthroughを選択する
  • virtioを使いたい場合は、NICとdiskでvirtioを指定する

次に、PCIデバイスなどの設定を行ないます。最初にSPICE関連のデバイスや不要そうなデバイス(Channel Spice、Display Spice、Video QXL、Sound ich*など)を全て削除していきます。そして Add Hardware で PIC Deviceを追加します。ここでGPUを指定して追加します。

ここまで、設定したら後は仮想マシンを起動してWindowsのインストールを行なうだけです。仮想マシンの作成は先に実施しても構いません。その場合、PCIデバイスの設定をインストール作業を行なったあとに実施すると良いと思います。上手く動かない場合に、PCIパススルーの問題なのか仮想マシンの設定誤りなのかが分からない場合などは先に仮想マシンを作ってみることをお勧めします。

VM内でベンチマークをとってみる

試しにGPUを使うようなベンチマークプログラムを動作させてみました。結果としては文句のない性能が出ていそうですね。

残念なこと

残念ながらいくつか問題が残っていたりします。

  • VMだとチートだと認識するゲームがある
  • 仮想コンソールの復旧に失敗している

この 2点 です。前者は具体的に言うとValorantなのですが、このゲームのアンチチートシステムはOSが動いている状況も確認しているようで、VM上のWindowsでは動作しないようです。ここまでしないと防げない攻撃があるということなのでしょうね。迷惑なのでチーターは滅んでほしいですね。

後者は自分の設定漏れなんだと思いますが、なぜかVMを起動して終了する際の復旧手順に不備があるらしくAlt+Ctrl+F2-6 とかで起動する仮想コンソールが復旧できていないようです。ほとんど困らないのですが、たまに欲しいときはあるので改善できるなら改善したいです。

まとめ

というわけで、GPUが1つしかないマシンでもGPU付きの仮想マシンを起動する方法についてまとめてみました。

なかなか便利な環境が作れたと思っています。Xが終了してしまうので、tmuxか何かでセッション保持するような形で作業しておけば、作業の途中でWindowsが必要になったりしても大丈夫という環境になりました。Windowsからホスト側にSSHしてしまえばホスト側の環境で作業することもできるので、いろいろ面白い環境が構築できたと思います。

個人的には全てのマシンのHost OSをLinuxにできるかと思ったのですが、まさかVMで起動できないゲームがあるとは思っていなくて頓挫したのが残念です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です