English日本語

文章

keelOS: 什么,一张显卡切成7张用?我把显卡SR-IOV搞定了!

一台 i5-12600K 的小主机,核显是 UHD 770。今天它同时给两台 Windows 虚拟机各"分"了一张显卡:Win11 和 Win10 里都是真的 Intel 驱动、真的 3D 加速,WebGL 水族馆 500 条鱼 60 fps,浏览器硬件解码,远程桌面的画面由虚拟机自己的显卡编成 H.264 送出来,60 fps、22 Mbit/s、解码 7 毫秒。这张核显最多能切成 7 张。

这事叫显卡 SR-IOV。它在数据中心的卡上存在很多年了,Intel 第 11/12 代以后的核显也有,但一直是"Linux 上能跑、Windows 虚拟机里 code 43"的半成品状态,网上的教程大多是凑出来的。我在 keelOS(我自己做的、基于 FreeBSD bhyve 的虚拟化系统)上把它从头到尾打通了,包括 Windows 为什么 43 的真正原因。这篇写给想用小主机跑几台带显卡的虚拟机的人。

显卡 SR-IOV 是什么

SR-IOV 是 PCIe 的一个标准:一个物理设备(PF)在总线上变出若干个轻量的虚拟设备(VF),每个 VF 有自己的 PCI 地址、寄存器和中断,可以单独直通给一台虚拟机。网卡的 SR-IOV 大家熟,显卡的原理一样,只是显卡里多了一个裁判:Intel 核显里有一颗微控制器叫 GuC,它跑固件,负责给 PF 和各个 VF 排班:谁的命令上执行单元、跑多久、什么时候让位。宿主机上的 PF 驱动负责建 VF、分配每个 VF 的显存配额和上下文数,guest 里的 VF 驱动就是普通的 Intel 显卡驱动,只是跑在"VF 模式",所有和硬件配置有关的事都要通过 GuC 问 PF。

一个常见误解:切成 7 张是不是每张只有 1/7 的性能?不是。静态切开的只有资源配额(显存映射、上下文、门铃),算力是 GuC 按时间片在"有活干的 VF"之间轮转,闲着的 VF 不占片。所以 7 张里 6 张闲着的时候,剩下那张基本拿到整张卡,和一台电脑上开七个窗口是一个道理。要严格平分也可以,PF 里有开关,默认是忙者多得。

这也是它和"整张显卡直通"的区别:直通是一台虚拟机独占,宿主机和其他虚拟机都没显卡了;SR-IOV 是宿主机留着 PF(屏幕还是它的),每台虚拟机各拿一个 VF。代价是 VF 没有显示引擎:它能渲染、能编解码视频,但接不了显示器——这个问题后面会回来。

keelOS 怎么做的

keelOS 的宿主机是 FreeBSD 15 + bhyve,而 Intel 核显的 PF 驱动(支持 SR-IOV 的那个)只有 Linux 的 xe。所以我没有把 PF 驱动移植到 FreeBSD,而是让一台常驻的小 Linux 虚拟机持有 PF:

  1. 宿主机启动时,把核显整个交给一台几十 MB 内存的显示虚拟机(我们叫 dvm)。它装的是自己编的 Linux 7.2 内核 + xe 驱动,负责宿主机的屏幕输出(所以接在机器上的显示器照常是宿主机的控制台)。
  2. dvm 里的 xe 以 PF 身份建出 VF(设置里填几个就建几个,最多 7)。VF 是真正的 PCI 设备,宿主机上看得见。
  3. keel 把 VF 像普通 PCI 设备一样直通给 Windows 或 Linux guest:网页上给虚拟机加一个"核显 VF"即可。
  4. guest 里装普通的 Intel 显卡驱动(Windows 自己从 Windows Update 装的 32.0.101.7084 就行,不用去 Intel 官网找包)。

Linux guest 这条路很快就通了:VF 里跑 xe,EGL、VA-API、H.264 编码 380 fps。难的是 Windows。

Windows 的 code 43:真正的原因

把 VF 直通给 Windows 11,设备管理器里 Intel UHD 770 带个黄叹号,代码 43。搜一圈,答案都是"换个驱动版本试试""用某个内核补丁包",没有人说清为什么。Intel 的 Windows 驱动是闭源的,但 VF 和 PF 之间的对话全部经过 GuC 的邮箱和共享内存,而这些在宿主机上都能看见。我写了一个小工具,在 dvm 里盯着 VF 的邮箱寄存器和通信缓冲区,把 Windows 驱动和 GuC 之间每一条消息解出来。

结果很清楚:Windows 的 VF 驱动初始化时,要在 GuC 上注册一个计算类(compute)的执行上下文,GuC 回了错误 0x100——没有这个引擎。因为上游 Linux 的 xe 驱动对第 12 代这种 Xe-LP 核显不启用单独的计算引擎(CCS0),Linux 自己用渲染引擎跑计算就够了;可 Windows 驱动的设计是计算上下文必须有,注册失败就把设备标成 43。所以这不是驱动版本的问题,是 PF 给 VF 的引擎清单里少了一项。

修法两处,缺一不可:

  • PF 开 CCS0:给 dvm 里的 xe 打了一个小补丁,在 Xe-LP 上也把计算引擎加进引擎掩码(硬件本来就有,只是驱动不暴露),keelOS 默认打开。打开后 GuC 收下了 Windows 的计算上下文,驱动一直在提交命令,再没有复位。
  • VF 不要以 VGA 类出现:Windows 把总线号最小的显卡当开机显卡(POST 设备)。VF 带着 VGA 类码直通进去,Windows 就把桌面放到它上面——而它没有显示器,于是控制台停在启动画面,模拟的显卡变成 code 10。keelOS 的 bhyve 把 VF 的类码改成"其他显示控制器"(03/80),Windows 就老老实实把开机画面留在模拟显卡上,VF 当一张纯渲染卡。

这两条之后,pnputil /enum-devices /class Display /problem 里一个问题设备都没有了。顺带的收获:VF 是 43 的时候 Windows 关机会挂住,修好以后 ACPI 关机 25 秒就完成。

VF 没有显示器:那桌面在哪里

驱动正常了,但打开浏览器跑 WebGL,CPU 93%、GPU 0%——桌面还在模拟显卡(基本显示适配器)上,分辨率锁在 1024x768,应用根本没在 VF 上渲染。道理前面说过:VF 没有显示引擎,接不了显示器,Windows 的桌面只会画在有显示器的卡上。

解法是给 VF 一块虚拟显示器。Windows 有个正式的模型叫 IddCx(间接显示驱动,USB 显示器、远程桌面都用它):一个用户态驱动声称"我这儿有一块显示器",并指定由哪张卡来渲染。我没有自己写驱动(要代码签名证书),而是原封不动地复用了开源的 Virtual-Display-Driver(MIT,SignPath 签名),让 keelOS 的 guest 代理 keelagent 来管它:

  • agent 发现 guest 里有一张没有任何输出的 Intel 显卡(这就是 VF;整卡直通的核显有接口,不会误判),就装上这个驱动,把它的显示器挂到 VF 上,并让它成为唯一激活的显示器(两块屏都亮着的话,远程桌面的鼠标坐标就对不上);
  • 显示器的分辨率跟着客户端窗口走:kRDPclient 窗口拉到多大,guest 的桌面就是多大;
  • agent 用 Desktop Duplication 从这块显示器抓画面,抓到的纹理就在 VF 上,直接交给 VF 的 Quick Sync 编成 H.264,经一条共享内存通道给宿主机的 keel-rdp,再转给客户端。整条路没有一次 CPU 拷贝,宿主机也不用编码。

这样一来,guest 的渲染、编码、显示全部在 VF 上,和微软 GPU-P + 远程显示适配器的做法是同一个模型:DXGI 里 0 号适配器就是 VF,主显示器在它名下,浏览器和游戏不用任何设置就把它当成本机显卡。

结果,和踩过的坑

同一台 i5-12600K,两个 VF 同时给 Windows 11 24H2 和 Windows 10 22H2,都是 Windows 自己装的 Intel 驱动:

Windows 11 (4 vCPU) Windows 10 (10 vCPU)
WebGL 水族馆 500 条鱼 60 fps 60 fps
kRDPclient 画面 1920x1080 H.264 60 fps,22 Mbit/s 1920x1080 H.264 57 fps,21.5 Mbit/s
客户端解码 / 网络 8.1 ms / 9.4 ms 6.7 ms / 9.2 ms
guest 里 GPU 3D / CPU 46% / 70% 48% / 28%

对比修之前:同一个页面在模拟显卡上 26 fps、CPU 93%。用户的评价是"很丝滑",以及"我们现在控制台比 mstsc 好用流畅"。

踩过的坑,按代价排序:

  • 浏览器软件渲染:有一阵 Edge 的 WebGL 全在 CPU 上算,edge://gpu 里只有两块"Microsoft Basic Render Driver"。查了半天 Chromium 的 GPU 选择和黑名单代码,最后发现是启动顺序:Edge 的启动加速在登录瞬间就把 GPU 进程拉起来了,那时虚拟显示器还没成为主屏,它看到的是没有硬件 GPU 的世界,之后不再重新识别。解法不是改浏览器设置,而是让 agent 把显示配置存进 Windows 的数据库,开机时虚拟显示器就是主屏,比任何应用都早。
  • 同时开着 mstsc:Windows 远程桌面登进去会把控制台会话断开,控制台只剩锁屏——agent 抓的是控制台,所以没帧;设置页里分辨率也改不了。这和物理机一样,不是 bug,但我当 bug 查了一下午。
  • IddCx 驱动只在启动时读配置:换分辨率模式表得重启设备(约 3 秒),它的管道命令 RELOAD 其实不重读。agent 现在预先写好常用尺寸,只有全新的尺寸才重启。
  • Windows 11 对 SetDisplayConfig 的要求:不带 virtual-mode-aware 标志一律回"参数错误",Sunshine 的显示库里有现成答案。
  • 虚拟显示器独占后,模拟显卡的控制台就是黑的:UAC 对话框在看不见的屏上。所以 agent 加了自更新:把新版放进一个目录,服务自己装,不用人点。

接下来

  • 剪贴板:现在 kRDPclient 到 guest 的粘贴还是"敲进去"的。agent 已经坐在 guest 会话里,下一步就是把它做成 mstsc 那样的双向剪贴板:文本、图片、然后是文件。
  • Linux guest 的 agent:VF 在 Linux 里早就能用,缺的是同样的抓屏 + VA-API 编码的 agent。
  • 更多 VF、更多机器:7 个 VF 同时跑的调度公平性、其他代的核显(11 代、Arc)、长时间稳定性。
  • 上游:PF 开 CCS0 这个补丁很小,打算整理后提给 xe。

这些都会在 keelOS 里做成"网页上点一下"的事:设置里填 VF 数量,给虚拟机加一张核显 VF,装上 agent,完。

← 文章 · 为什么选 keelOS