Linux KVMにおけるVFIO PCIeパススルー構築ガイド:Windows仮想マシンへのGPU直通割り当て

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

クイックスタート:5分で完了する初期設定

午前2時、障害チケットのアラートが鳴り響く。Blenderレンダリング用のWindows仮想マシンで深刻なボトルネックが発生し、仮想GPU(VirtIO)のビューポートはわずか4 FPSしか出ていません。この状況を打破する唯一の解決策が、VFIOを利用して物理GPUをVMに直接パススルーすることです。

以下は、Ubuntu 22.04またはDebian 12環境で直接実行できる手順です。

ステップ1:ブートローダーでIOMMUを有効化する

# GRUB設定を開く
sudo nano /etc/default/grub

# CPUに応じたパラメータをGRUB_CMDLINE_LINUX_DEFAULT変数に追加:
# Intel CPU: intel_iommu=on iommu=pt
# AMD CPU:   amd_iommu=on iommu=pt
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on iommu=pt"

# GRUBを更新して再起動
sudo update-grub
sudo reboot

ステップ2:GPUのハードウェアIDを確認する

lspci -nn | grep -E "VGA|Audio"
# RTX 3060の出力例:
# 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106 [GeForce RTX 3060] [10de:2504] (rev a1)
# 01:00.1 Audio device [0403]: NVIDIA Corporation GA106 High Definition Audio Controller [10de:228e] (rev a1)
# 記録しておくVendor:Device IDのペア: 10de:2504, 10de:228e

ステップ3:vfio-pciドライバーでGPUをホストから分離する

仮想マシンが起動する前に、ホストLinux側で対象GPUの制御を手放す必要があります。上記で確認したIDペアにvfio-pciドライバーを割り当てる設定ファイルを作成します。

echo "options vfio-pci ids=10de:2504,10de:228e" | sudo tee /etc/modprobe.d/vfio.conf

# 起動時に必要なモジュールをロード
echo -e "vfio\nvfio_iommu_type1\nvfio_pci" | sudo tee -a /etc/modules

# デフォルトのグラフィックドライバーによるデバイスの占有を防止
echo -e "blacklist nouveau\nblacklist nvidia\nblacklist snd_hda_intel" | sudo tee /etc/modprobe.d/blacklist.conf

# initramfsを更新して再起動
sudo update-initramfs -u -k all
sudo reboot

ホストサーバーの再起動後、以下のコマンドで状態を確認します。

lspci -nnk -d 10de:2504
# 「Kernel driver in use」の行が vfio-pci と表示されていれば正常

技術的な仕組み:IOMMUとIOMMUグループの課題

一般的な仮想マシンは、QXLやVirtIO GPUなどの仮想化グラフィックデバイスを経由して描画を行います。そのため、ホストCPUがグラフィック命令のトランスレーション処理をすべて負担することになります。この方式は文書作成やWebブラウジング程度であれば十分ですが、3DレンダリングやAAAタイトルゲームではパフォーマンスが70%以上低下してしまいます。

VFIO(Virtual Function I/O)はこのボトルネックを根本から解消します。物理GPUのPCIeレジスタを仮想マシンが直接制御できるようにすることで、ベアメタル環境と比較して97〜99%の性能を引き出すことが可能になります。

IOMMUグループの正確な理解

IOMMUは、I/Oデバイス専用のMMU(メモリ管理ユニット)として機能します。VMの仮想メモリアドレス空間をハードウェアデバイスのDMAバスに直接マッピングする仕組みです。ただし、マザーボードのハードウェア設計上、各PCIeスロットが個別に完全分離されているわけではなく、複数のデバイスが「IOMMUグループ」としてまとめられています。

KVMにおける絶対的な鉄則は明白です。IOMMUグループはハードウェア分離の最小単位であるため、同じグループに属するすべてのデバイスをまとめて仮想マシンにパススルーするか、あるいは一切パススルーしないかのいずれかを選択しなければなりません。例外は存在しません。

以下のスクリプトを実行して、デバイスのグループ構成を確認します。

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

グラフィックカードとHDMIオーディオコントローラーが同一のGroup 13にある場合、両方をvfio-pciに割り当てる必要があります。逆に、GPUがオンボードのUSBコントローラーやNICと同じグループに含まれている場合、VMを単独で起動することはできません。

高度なチューニング:Code 43の解消とフレームタイム遅延の削減

ホスト層でGPUを正常に認識させても、作業はまだ半分です。Windowsを起動すると、デバイスマネージャーにおなじみの黄色い感嘆符とともにCode 43エラーが表示されることがあります。

1. Code 43の根本的な解消

バージョン465(2021年4月)以降のドライバーで、NVIDIAはGeForceにおける仮想マシンの制限を解除しました。しかし、一部のゲームやグラフィックアプリケーションは依然としてハイパーバイザーの存在を検知すると動作を停止します。確実な解決策は、仮想マシンのXML定義ファイル(virsh edit win10-gpu)でKVMの痕跡を隠蔽することです。

<features>
  <acpi/>
  <apic/>
  <hyperv mode='custom'>
    <relaxed state='on'/>
    <vapic state='on'/>
    <spinlocks state='on' threshold='8191'/>
    <vendor_id state='on' value='GenuineIntel'/>
  </hyperv>
  <kvm>
    <hidden state='on'/>
  </kvm>
  <ioapic driver='kvm'/>
</features>

2. PCIeの割り当てとクリーンなVBIOSのロード

パススルー対象のGPUがホスト起動時のプライマリGPUである場合、ホストのUEFIによってカードのROMメモリが上書きされてしまいます。その結果、仮想マシンがドライバーを読み込む際に画面が真っ暗になる現象が発生します。TechPowerUpからオリジナルのVBIOSファイルをダウンロードするか、事前にダンプしておいたROMファイルを<hostdev>タグ内で明示的に指定します。

<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
  <rom file='/var/lib/libvirt/vbios/rtx3060.rom'/>
  <address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x0' multifunction='on'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
  </source>
  <address type='pci' domain='0x0000' bus='0x06' slot='0x00' function='0x1'/>
</hostdev>

3. CPU PinningとHugePagesによるマイクロスタッターの排除

不意に発生する画面のカクつき(マイクロスタッター)は、ホストCPUの頻繁なコア間移動(コンテキストスイッチング)やメモリの断片化が原因です。物理コア/スレッドのペアを各vCPUに固定(Pinning)することで、フレームタイムのブレを2ms未満に抑制できます。

<vcpu placement='static'>8</vcpu>
<cputune>
  <vcpupin vcpu='0' cpuset='4'/>
  <vcpupin vcpu='1' cpuset='12'/>
  <vcpupin vcpu='2' cpuset='5'/>
  <vcpupin vcpu='3' cpuset='13'/>
  <vcpupin vcpu='4' cpuset='6'/>
  <vcpupin vcpu='5' cpuset='14'/>
  <vcpupin vcpu='6' cpuset='7'/>
  <vcpupin vcpu='7' cpuset='15'/>
  <emulatorpin cpuset='0,8'/>
</cputune>
<memoryBacking>
  <hugepages/>
</memoryBacking>

さらに2MBのHugePagesを設定することで、TLBページテーブルのキャッシュミス率を12%から0.8%未満へと大幅に低減し、極めて安定したフレームレートを維持できます。

トラブルシューティングの実践ノウハウ

  • マザーボードのIOMMUグループ分割が不適切な場合:GPUがSATAコントローラーなどと同じグループにまとめられている場合、暫定的な対策としてカーネルパラメータにpcie_acs_override=downstream,multifunctionを追加します。これによりグループを強制的に細分化できますが、VM間のDMAセキュリティ境界が損なわれるため、ホームラボや開発環境でのみ使用してください。
  • 仮想マシン起動時のブラックアウト:マザーボードのBIOSでCSM(Compatibility Support Module)を無効化してください。また、仮想マシンのファームウェアにはレガシーなSeaBIOSやi440fxチップセットを避け、必ずOVMF (UEFI)とQ35チップセットを組み合わせて作成してください。
  • ヘッドレス(モニター未接続)運用:グラフィックカードのポートにHDMIダミープラグ(安価なダミーコネクタ)を接続します。Windows VM上にSunshineサーバーを導入し、クライアント端末からMoonlight経由でストリーミングします。1GbpsのLAN環境があれば、デコード遅延3ms未満で2K 120Hzの快適なストリーミング環境を実現できます。
  • BAR 0のリソース競合解除:dmesg | grep -i vfioを実行してBAR 0: cannot reserveエラーが出ている場合、LinuxのフレームバッファがGPUを占有しています。GRUBパラメータにvideo=efifb:offまたはinitcall_blacklist=sysfb_initを追加して解決します。
Share: