TL;DR
- NixOSホストでGPUのPCI Passthroughを設定し,VM上から直接扱えるようにする
- VM上のWindowsのディスプレイ出力をPassthroughしたGPU経由で行う
モチベ
- メインがLinuxだがどうしてもOffice等でWindowsがいる
- Dualbootは面倒なのでDesktopではやりたくない
- KVM + Spice (or RDP) でもいいが,グラフィックの動作が微妙
- OpenGLも使えるがVirt Manager経由だとブラックアウトして正常動作しない
- そうだ,GPUから直接出力させてしまおう!
PCI Passthroughとは?
IOMMUやinterrupt remappingを使用し,VMから直接PCIデバイスを扱えるようにするもの.Intel CPUだとVT-dによって使用できるようになっている.
通常は,Hypervisorがデバイスを制御し,HypervisorがemulateしたデバイスをVM上から触る.
VMからデバイスへのI/O操作をHypervisorがtrapすることで実現するが,これはネイティブの環境と比べて非常にオーバーヘッドが大きい.
trap & emulateのオーバーヘッド削減のため,準仮想化も利用することができるが,こちらもエミュレーションであることに変わりは無いため,まだオーバーヘッドがある.
そこでPassthroughを行うことで,ゲストOSからは通常のデバイスドライバでの操作が可能になり,割り込みも比較的低オーバーヘッドで受けることができる.また,デバイスからのDMAアクセスはIOMMUによってハードウェアレベルでアドレス変換が行われ,こちらも少ないオーバーヘッドでメモリアクセスが可能となっている.
Environment
- OS: NixOS Unstable (25.11) / Linux 6.14.7-zen1
- CPU: Intel Core i9-14900K
- Host GPU: NVIDIA T400 4GB
- Passthrough GPU: NVIDIA Quadro P620
- VMM: libvirt
GPUは2枚挿しで,片方をPassthrough用にする.
VMMはKVMバックエンドでlibvirtを使用し,virt-manager経由で諸々の操作を行う.
Passthroughの設定
以下の設定が必要.
Direct I/Oの有効化
まずはFirmware側でDirect I/Oの機能を有効にする.昨今のマシンでは基本的に仮想化支援機構と一緒にデフォルトで有効になっているはずだ.
Intel, AMDではそれぞれ以下を有効にすれば良い.
- Intel: VT-x / VT-d
- AMD: AMD-V / SVM / NX Mode / IOMMU
IOMMUの有効化
Kernel側でIOMMUを使用するように設定する.
カーネルパラメータに以下を設定すればよい.変更後,nixos-rebuild switch
してRebootする.
# intel
boot.kernelParams = [ "intel_iommu=on" "iommu=pt" ];
# AMD
boot.kernelParams = [ "amd_iommu=on" "iommu=pt" ];
有効になると,以下のスクリプトにて各IOMMU Groupにどのデバイスが割り当てられているか確認できるようになる (要 pciutils
).
#!/bin/bash
for d in $(find /sys/kernel/iommu_groups/ -type l | sort -n -k5 -t/); do
n=${d#*/iommu_groups/*}; n=${n%%/*}
printf 'IOMMU Group %s ' "$n"
lspci -nns "${d##*/}"
done;
IOMMU Group 0 0000:00:00.0 Host bridge [0600]: Intel Corporation Device [8086:a700] (rev 01)
IOMMU Group 1 0000:00:01.0 PCI bridge [0604]: Intel Corporation Raptor Lake PCI Express 5.0 Graphics Port (PEG010) [8086:a70d] (rev 01)
IOMMU Group 2 0000:00:04.0 Signal processing controller [1180]: Intel Corporation Raptor Lake Dynamic Platform and Thermal Framework Processor Participant [8086:a71d] (rev 01)
IOMMU Group 3 0000:00:06.0 System peripheral [0880]: Intel Corporation RST VMD Managed Controller [8086:09ab]
IOMMU Group 4 0000:00:08.0 System peripheral [0880]: Intel Corporation GNA Scoring Accelerator module [8086:a74f] (rev 01)
IOMMU Group 5 0000:00:0e.0 RAID bus controller [0104]: Intel Corporation Volume Management Device NVMe RAID Controller Intel Corporation [8086:a77f]
IOMMU Group 5 10000:e0:06.0 PCI bridge [0604]: Intel Corporation Raptor Lake PCIe 4.0 Graphics Port [8086:a74d] (rev 01)
IOMMU Group 5 10000:e0:17.0 SATA controller [0106]: Intel Corporation Alder Lake-S PCH SATA Controller [AHCI Mode] [8086:7ae2] (rev 11)
IOMMU Group 5 10000:e1:00.0 Non-Volatile memory controller [0108]: Sandisk Corp SN8000S NVMe SSD [15b7:5049] (rev 01)
IOMMU Group 6 0000:00:14.0 USB controller [0c03]: Intel Corporation Alder Lake-S PCH USB 3.2 Gen 2x2 XHCI Controller [8086:7ae0] (rev 11)
IOMMU Group 6 0000:00:14.2 RAM memory [0500]: Intel Corporation Alder Lake-S PCH Shared SRAM [8086:7aa7] (rev 11)
IOMMU Group 7 0000:00:15.0 Serial bus controller [0c80]: Intel Corporation Alder Lake-S PCH Serial IO I2C Controller #0 [8086:7acc] (rev 11)
IOMMU Group 8 0000:00:16.0 Communication controller [0780]: Intel Corporation Alder Lake-S PCH HECI Controller #1 [8086:7ae8] (rev 11)
IOMMU Group 9 0000:00:17.0 System peripheral [0880]: Intel Corporation RST VMD Managed Controller [8086:09ab]
IOMMU Group 10 0000:00:1a.0 PCI bridge [0604]: Intel Corporation Alder Lake-S PCH PCI Express Root Port #25 [8086:7ac8] (rev 11)
IOMMU Group 11 0000:00:1c.0 PCI bridge [0604]: Intel Corporation Alder Lake-S PCH PCI Express Root Port #2 [8086:7ab9] (rev 11)
IOMMU Group 12 0000:00:1f.0 ISA bridge [0601]: Intel Corporation Device [8086:7a88] (rev 11)
IOMMU Group 12 0000:00:1f.3 Audio device [0403]: Intel Corporation Alder Lake-S HD Audio Controller [8086:7ad0] (rev 11)
IOMMU Group 12 0000:00:1f.4 SMBus [0c05]: Intel Corporation Alder Lake-S PCH SMBus Controller [8086:7aa3] (rev 11)
IOMMU Group 12 0000:00:1f.5 Serial bus controller [0c80]: Intel Corporation Alder Lake-S PCH SPI Controller [8086:7aa4] (rev 11)
IOMMU Group 12 0000:00:1f.6 Ethernet controller [0200]: Intel Corporation Ethernet Connection (17) I219-LM [8086:1a1c] (rev 11)
IOMMU Group 13 0000:01:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU117GL [T400 4GB / T400E] [10de:1ff2] (rev a1)
IOMMU Group 13 0000:01:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:10fa] (rev a1)
IOMMU Group 14 0000:02:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107GL [Quadro P620] [10de:1cb6] (rev a1)
IOMMU Group 14 0000:02:00.1 Audio device [0403]: NVIDIA Corporation GP107GL High Definition Audio Controller [10de:0fb9] (rev a1)
IOMMU Group 15 0000:03:00.0 Unassigned class [ff00]: Realtek Semiconductor Co., Ltd. RTS525A PCI Express Card Reader [10ec:525a] (rev 01)
PCI PassthroughはこのIOMMU Groupの粒度で設定可能なので,Passthroughしたいデバイスと同一のGroupのデバイスも一緒にPassthroughされることになる.
今回はQuadro P620をPassthroughするので,Group 14が対象となる.
ここで,Passthrough対象デバイスのVID:PID
をメモしておく.VFIOドライバの割り当ての際に使用する.
ここではGroup 14の以下の2種類が対象である.
10de:icb6
10de:0fb9
VFIOドライバの割り当て
通常,デバイスにはKernelが対応するデバイスドライバを割り当てて初期化するが,Passthroughする場合はゲストでデバイスドライバを割り当てて初期化する必要がある.
そこで,ホストのLinuxではスタブドライバとしてVFIOドライバをPassthrough対象に割り当てる.PCIデバイスであれば,これはvfio-pci
となる.
VFIOはVirtual Function I/Oの略で,IOMMUを用いてユーザーランドからデバイスのメモリ空間やレジスタへアクセスするためのフレームワークである.KVMの仮想マシン用途以外にも,DPDKなどで使われる.
VFIOを使用するためには,起動時に以下の3種類のKernel Moduleをロードさせる.
vfio
vfio_iommu_type1
vfio_pci
古い情報ではvfio_virqfd
も含める必要があるとされるが,これはKernel 6.2でvfio
に一本化されたため,6.2以降では不要である.
Ref: https://wiki.archlinux.org/title/PCI_passthrough_via_OVMF
nixosConfiguration
では以下のようになる.
boot.initrd.kernelModules = [
"vfio_pci"
"vfio_iommu_type1"
"vfio"
];
GPUが1枚で,他にGPUドライバを使用するデバイスがないのであれば,nvidia
やnouveau
をblacklistに含めてもよい.
その上で,カーネルパラメータでVFIOドライバを割り当てるデバイスを明示的に指定する.
vfio_pci.ids=
にカンマ区切りでVID:PID
を記載する.
vfioIds
が対象デバイスのVID:PID
のlistであるとして,以下のようにkernelParams
を設定した.
boot.kernelParams = [
"intel_iommu=on"
"iommu=pt"
] ++ lib.optional (vfioIds != null) ("vfio_pci.ids=" + lib.concatStringsSep "," vfioIds);
Reboot後,lspci
で対象デバイスにvfio-pci
が割り当てられていることを確認する.
[nix-shell:~]$ lspci -nnk -d 10de:1cb6
0000:02:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107GL [Quadro P620] [10de:1cb6] (rev a1)
Subsystem: Hewlett-Packard Company Device [103c:1264]
Kernel driver in use: vfio-pci
Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia
VM作成とManifestへの細工
ここまででお膳立てはできたので,実際にPassthroughを行う.
まずはNixOS Hostでlibvirtの設定をする.Windows 11をGuestとする想定なので,OVMFのSecure Boot対応や仮想TPM 2.0を有効化する.
また,VirtioによるHost-Guest間のフォルダ共有のため,virtiofsd
も有効にしている.
{ pkgs, username, ... }:
{
virtualisation = {
libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
swtpm.enable = true;
ovmf = {
enable = true;
packages = [
(pkgs.OVMFFull.override {
secureBoot = true;
tpmSupport = true;
}).fd
];
};
vhostUserPackages = [ pkgs.virtiofsd ];
};
};
};
}
一旦ここでPassthroughはせずにGuestのWindowsのインストールを行ったが,以下はインストール時にやっても問題ない.
VMへのPCIデバイスの割り当ては,virt-managerのAdd HardwareからPCI Host Deviceを選択する.ここでPassthrough対象のデバイスを指定することでGuest OSから認識できるようになる.
NVIDIAのドライバは,仮想マシン内で実行されていることを検出した場合,エラー43で動作停止するケースがある.
これを回避するため,マニフェストを編集して仮想マシンであることを隠蔽する.
GuestのXMLマニフェストを開き,以下の項目を追加する.
<features>
...
<hyperv mode="custom">
...
<vendor_id state="on" value="1234567890ab"/>
...
</hyperv>
<kvm>
<hidden state="on"/>
</kvm>
...
</features>
ArchWikiによれば,前者のHyper-VのVendor IDに関しては古いドライバの場合に必要とのことだが,念のため追加した.
2つ目についてはKVMであることを隠蔽するものである.
これらはあくまで検出メカニズムをする抜けるためのものであり,完全に仮想マシンであることを隠蔽するものではない.
以上で諸々の設定は完了であり,Guestから直接GPUを使えるようになる.
PassthroughしたGPUにディスプレイを接続すると,Guestの映像出力がされる.
動作の感触
- 画面描画は非常に快適
- SPICEからの出力は無効にしておくとよい (GPU出力と共用すると不安定になる)
- CPUに関しても昨今はベースの性能が非常に高いため,仮想マシンであることを感じさせないレベル
- BluetoothドングルをGuestに渡してキーボード・マウスもシームレスに切り替えできるようにした
- これはよい
- Office等を使うレベルなら全く無問題である
- うまくやればゲーム用途でもいけそう
- ハード性能の進化はすごいね