这两年经常有人问我,学GPU KMD(Kernel Mode Driver,内核模式驱动)到底从哪里切入。我一般会让他们先把PF和VF这两个概念吃透。原因很简单,不管你是做GPU虚拟化、直通、容器共享,还是单纯想读懂厂商驱动里的sriov_configure回调,最后都会绕到这两个词上。这一篇就是“手把手教你学GPU的KMD专栏”里的2.3节,专门聊PF(物理功能)与VF(虚拟功能)在GPU中的应用。目标读者是正在看Linux内核PCI子系统、GPU驱动源码,或者打算做虚拟化场景的开发者,我尽量用做项目时的思路来讲,而不是照搬英文手册。
1. 为什么GPU KMD必须搞懂PF和VF?
1.1 虚拟化是绕不开的坎
现在做GPU驱动开发,已经不是只写一个probe、管几块显存、处理几个中断那么简单了。AI训练、推理、图形渲染,全都跑在虚拟化或者容器化的环境里。云上的GPU实例,底层不管是NVIDIA还是AMD,都逃不掉一个核心问题:一块物理GPU怎么安全地分给多个用户用?
最早大家用的是纯软件模拟,性能和隔离都不行。后来出现了PCIe直通,把整块GPU塞给一个虚拟机,性能是上去了,但一块卡只能服务一个租户,资源利用率很低。再往后才是SR-IOV(Single Root I/O Virtualization)方案,它把PCIe设备拆成PF和VF,由硬件的IO虚拟化能力来保证隔离。这一套机制从网卡开始,后来被GPU厂商继承并扩展,于是KMD开发者就必须知道PF是什么、VF是什么,以及驱动该如何管理它们。
我见过不少新人翻内核源码,看到pci_enable_sriov就以为只是把设备“变魔术”一样多出几个PCI节点,其实完全不是。SR-IOV的核心在于资源切分、中断迁移、DMA隔离和虚拟功能的状态管理。这些工作大部分都由KMD配合固件完成,如果你不理解PF和VF的职责,一旦出现虚拟化环境下的性能问题或者设备丢失,会非常被动。
1.2 PF和VF在KMD里的定位
在PCIe规范里,PF(Physical Function)是物理上真实存在、拥有完整PCIe功能的能力单元。你可以把它理解成一栋楼的物业管理处:它有整栋楼的钥匙,能管理楼道、电梯、水管,也能给每一个房间分配独立钥匙和空间。VF(Virtual Function)则是从PF派生出来的轻量功能单元,它没有完整的管理权限,更多是作为“租户接口”暴露给上层使用。
在GPU场景里,PF通常由主机内核态驱动(KMD)直接管理,它负责设备初始化、固件加载、显存分配、进度跟踪、错误上报等所有系统级操作。VF则是被切分出来的计算通道,可能直接分配给虚拟机,或者绑定到VFIO驱动进入用户态容器。KMD写得好不好,很大程度上取决于它能否在一个PF上安全创建出多个VF,并保证每个VF之间不互相干扰。
这里有个很容易混淆的点:GPU上的MIG(Multi-Instance GPU)和SR-IOV不是一回事。MIG是在GPU内部做硬件分区,每个实例独占一组SM和显存片,但它不一定表现为PCIe VF;而SR-IOV的VF是PCIe层面的虚拟功能,通常配合驱动和虚拟机管理程序使用。我在后面讲到vGPU时会再区分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从PCIe规范看懂PF和VF的分工
2.1 SR-IOV的诞生背景
SR-IOV不是GPU发明的,它最早是为了解决网卡虚拟化性能太差而设计的。想想看,一台物理服务器上可能有几十个虚拟机,每个虚拟机都需要网络接入,如果所有网络包都要经过宿主机软件转发,CPU很快就打满了。SR-IOV的做法是把网卡硬件能力拆成多个可独立分配的功能,让虚拟机直接通过硬件队列收发数据,绕开软件交换。
GPU复用这套机制时面临的挑战比网卡更大。因为GPU不只是简单的IO设备,内部有大量计算单元、显存控制器、DMA引擎、图形或视频编解码硬件,还有一个复杂的调度器。VF怎么设计、怎么切分显存、怎么做上下文切换,每个厂商的实现细节都不一样。但PCIe规范给了一个统一的框架:PF负责管理,VF作为资源切片暴露给客户。
在Linux内核里,SR-IOV的支持主要由两部分组成:PCI核心代码和具体设备的KMD。PCI核心代码负责枚举VF、分配PCI配置空间、管理总线号;KMD则负责告诉PCI核心“我这个物理功能最多支持多少个VF、每个VF需要多大BAR空间、怎么初始化内部资源”。不理解这层关系,就很容易把问题定位到错误模块。
2.2 PF:拥有完整资源的“管理员”
看PCIe配置空间,PF的配置空间是完整的,包含所有标准Capability和厂商自定义Capability。比如电源管理、MSI-X中断、高级错误报告、SR-IOV Capability等。GPU的PF还经常拥有较大的BAR空间,通过这个BAR可以访问显存映射区、寄存器组、门铃(doorbell)和命令队列。
在KMD初始化时,PF是第一个被probe的设备实例。驱动会读取设备ID、修订版本、固件版本,然后完成一系列初始化操作。初始化完成后,PF不仅承担数据面功能(比如提交GPU任务),还要承担管理面功能:创建VF、销毁VF、设置资源配额、升级固件、收集错误状态。
这里有一点特别容易出现理解误差:PF和VF并不是两种不同的芯片,它们通常共享同一个物理PCIe设备端口。区别在于暴露的逻辑视图和权限边界不同。PF可以通过内部寄存器访问所有资源,VF只能访问分配给它的那部分。所以KMD在处理PF和VF时,必须在寄存器偏移和地址翻译上做严格区分,不能让某个VF越权访问到其他VF的显存。
2.3 VF:轻量级的“租户接口”
VF没有完整的配置空间,它只保留必要的部分,比如Vendor ID、Device ID、Class Code、MSI-X Capability和VF BAR。它不能控制整个PCIe链路,也不能做设备重置后从零初始化固件这类操作。VF的创建、销毁、复位都依赖PF。
在GPU上,一个VF大体对应一组计算资源,可能是若干SM或计算单元、一段显存、若干DMA通道。厂商驱动在创建VF时,会在GPU固件里配置一个“虚拟GPU实例”,并把对应的资源映射到VF的BAR空间。上层虚拟机里的GPU驱动通常以VF驱动的方式工作,它以为自己独占了一块GPU,实际上只是拿到了PF授权的一部分资源。
值得注意的是,VF的数量并不是越多越好。每个VF都需要消耗硬件资源,比如门铃中断、MSI-X向量、上下文存储、显存分片。如果创建太多VF,每个VF能分到的资源太少,性能就会很难看。KMD开发的一个核心工作就是确定max VF数量、资源切分配比和触发条件,这些通常会和产品定义、业务场景强相关。
3. GPU KMD里PF/VF的实操要点
3.1 设备枚举与PF初始化
所有SR-IOV工作都从PF初始化开始。我们以Linux内核为背景,KMD的probe回调被触发时,首先拿到struct pci_dev *pdev,这个pdev就是PF对应的PCI设备对象。驱动要做的事情包括:
- 启用PCI设备,设置DMA掩码
- 请求和映射BAR空间
- 注册中断处理函数
- 读取设备能力,检查SR-IOV Capability是否可用
- 初始化固件和硬件队列
- 设置
pci_driver里的sriov_configure
很多人会忽略检查SR-IOV能力这一步。如果设备在硬件层面没有打开ACS(Access Control Services)或者BIOS没有配置好IOMMU,后面创建VF时会遇到各种诡异问题。所以我在写驱动时,会在probe里主动输出SR-IOV Capability的位置、总VF数、初始VF数、VF BAR信息,方便后续排查。
一个简单的检查命令是:
bash复制lspci -vvv -s 01:00.0 | grep -A10 "SR-IOV"
如果输出里没有Single Root I/O Virtualization字样,说明当前设备或BIOS配置没有启用SR-IOV,这时候再往驱动层面排查就没意义了。在GPU服务器里,有时需要在BIOS开启SR-IOV Support、ACS和Intel VT-d或AMD IOMMU。
3.2 配置VF:从sysfs到内核回调
Linux内核通过sysfs暴露了SR-IOV的配置入口,物理设备目录下有一个sriov_numvfs属性。你往里面写0,表示销毁所有VF;写大于0的整数,表示创建对应数量的VF。这个写操作最终会走到驱动注册的sriov_configure回调。
拿一个简化驱动为例:
c复制static int mygpu_sriov_configure(struct pci_dev *pdev, int numvfs)
{
struct mygpu_device *gpu = pci_get_drvdata(pdev);
if (numvfs > gpu->max_vfs)
return -EINVAL;
if (numvfs == 0) {
pci_disable_sriov(pdev);
mygpu_vf_teardown(gpu);
} else {
mygpu_vf_setup(gpu, numvfs);
pci_enable_sriov(pdev, numvfs);
}
return numvfs;
}
static struct pci_driver mygpu_driver = {
.name = "mygpu",
.id_table = mygpu_pci_ids,
.probe = mygpu_probe,
.remove = mygpu_remove,
.sriov_configure = mygpu_sriov_configure,
};
实际操作时,mygpu_vf_setup要做的事情很多:分配内部资源表、初始化每个VF的显存属性、预留门铃页面、为每个VF准备独立的错误报告缓冲区。pci_enable_sriov本身会触发PCI核心创建新的pci_dev,随后每个VF也会有一个对应的probe流程,但这个probe不会再进入mygpu_probe,而是由驱动里专门针对VF的device ID匹配逻辑处理。
从sysfs触发的方式很简单:
bash复制echo 8 > /sys/bus/pci/devices/0000:01:00.0/sriov_numvfs
创建完成后,用lspci -s 01:00.0可以看到新出现的VF设备,例如01:00.1到01:00.8。
3.3 Bar空间、中断和DMA的关键设计
PF和VF在BAR空间上的处理完全不同。PF的BAR通常是几十MB甚至上GB,映射了全部寄存器、显存窗口和上下文管理区域。VF的BAR则要小得多,它只需要映射到该VF绑定的资源子集。PCIe规范允许PF定义VF BAR的起始地址、Bar Offset和Bar Stride,内核和固件通过这些字段把每个VF重定位到不同资源区域。
在KMD实现时,如果BAR分配错误,可能出现两个VF访问到同一段寄存器的情况,轻则驱动报错,重则学术一点叫“DMA越权”——一个虚拟机可以读另一个虚拟机的显存。为了防止这类问题,我在设计时一般会让每个VF拥有一段独立的MMIO窗口,并在固件侧开启地址重映射,确保虚拟机的DMA请求经过IOMMU检查后落在自己允许的地址范围内。
中断通常是MSI-X,每个VF需要分配自己的中断向量。这里有个经验:如果VF数量很大,比如单PF支持64个VF,每个VF又需要多个队列,那么MSI-X向量总需求会非常高。PCIe设备MSI-X Capability里的Table Size是有限的,设计时要留足余量。实战中我遇到过VF创建成功但虚拟机里中断死循环的问题,最后发现是向量重映射配置失效,host侧的irqbalance把多个VF中断挤到了一个CPU上。
DMA部分更复杂。在VF直通给虚拟机时,宿主机的IOMMU页表配置由VFIO驱动和PCI核心处理,KMD不直接参与DMA页表翻译,但KMD需要为每个VF准备硬件上下文地址、初始化doorbell区域、配置GPU内部的内存管理单元。如果这些内部寄存器没有按VF隔离,整个SR-IOV等于没做。
4. 虚拟化场景:PF/VF的落地组合
4.1 直通模式下VF怎么进虚拟机
最常见的应用是把VF通过VFIO直通给虚拟机。用户可以在宿主机上把一个VF绑定到vfio-pci驱动,然后通过QEMU命令行或者libvirt配置文件把它附加给虚拟机。这样做的好处是虚拟机里的GPU驱动可以直接访问硬件,性能接近裸金属,同时因为VF是资源切片,宿主机还能继续使用PF和其他VF,利用率比整卡直通高很多。
命令流程大致是:
bash复制# 解绑默认驱动
echo 0000:01:00.1 > /sys/bus/pci/drivers/mygpu_vf/unbind
# 绑定VFIO
echo 0000:01:00.1 > /sys/bus/pci/drivers/vfio-pci/bind
# 再通过QEMU参数 -device vfio-pci,host=01:00.1 挂给虚拟机
这个过程里,KMD的角色不是消失了,而是退后到PF的管理层。VF被解绑到vfio-pci之后,宿主机不再有厂商KMD管理该VF的日常数据面,但VF的创建、重置、错误上报仍然通过PF管理。也就是说,PF和VF之间的“主从关系”不是重新绑定VFIO就断开的,硬件的管理通道永远在PF侧。
4.2 vGPU与VFIO mdev的取舍
再来说说vGPU。很多GPU厂商在vGPU方案里不会只做单纯的VF直通,而是采用“mdev”(mediated device)方式。mdev是内核里一种软件中间层,它在VF之上又叠加一层接口,往虚拟机里暴露的设备不一定是标准的PCIe VF,而是一个经过软件模拟的PCIe设备。
这种做法的好处是灵活,可以让同一块PF上的多个VM共享图形加速能力,还能做动态显存分配、迁移和快照。坏处是性能不如纯VF直通稳定,因为多了软件模拟层,而且KMD的复杂度又上了一个台阶。NVIDIA的vGPU、Intel的GVT-g或者AMD的mGPU,本质上都是在PF和VF或虚拟功能之间做一层资源编排。
从学习角度,我建议先掌握纯SR-IOV的PF/VF,再去看mdev。因为mdev是在SR-IOV概念之上定义的,底层还是PCIe资源和GPU资源切分。如果你连VF的BAR、MSI-X都不清楚,研究mdev的介导类型只会越看越糊涂。
4.3 如何用PF做GPU资源调度
除了虚拟化,PF和VF还有一个重要用途:容器和裸金属环境的资源调度。比如Kubernetes集群里想为一个Pod分配三分之一块GPU,有些方案会动态创建VF并把它映射进容器。这个过程中PF侧KMD要做的包括动态调整VF数量、动态绑定显存范围、配置计算单元比例。
我做过一个原型,用一小块嵌入式GPU的SR-IOV能力做容器共享。刚开始天真地以为只要在sysfs里写数字就行,实际调优时发现资源配置方案要跟业务方对齐:有人要独占显存,有人只要计算单元不关心显存;有人要图像编解码器,有人只跑CUDA或ROCm推理。如果KMD不提供细粒度配置接口,上层平台只能创建固定规格的VF,造成了很大的浪费。
后来我在驱动里增加了一个自定义配置接口,允许管理员指定每个VF的显存大小、计算资源比例和队列数。这需要和硬件固件配合编写,本质上是在PF的资源池上做切分。这类工作只依赖标准SR-IOV框架是不够的,必须了解自家GPU的硬件调度器能力。
5. 常见问题与排查技巧实录
5.1 VF创建失败,先看这几个地方
echo 4 > sriov_numvfs 之后提示Device or resource busy或者命令回显报错,最可能的原因不是驱动bug,而是IOMMU或ACS没配对。我的排查顺序基本固定:
- 先看dmesg有没有PCIe AER错误或DMAR错误
- 再检查
/sys/bus/pci/devices/.../sriov_totalvfs,确认硬件最大VF数 - 然后确认
CONFIG_PCI_IOV是否编译进内核 - 最后看驱动的
sriov_configure返回值,而不是只看sysfs层的报错
有一个特别容易踩的坑:某些GPU在创建VF之前需要先关闭显存ECC或持久模式,不然资源不足。这类限制厂商文档里可能写得很隐晦,经常只在dmesg里吐一行insufficient resources,如果不加详细日志会很难查。
5.2 MSI-X中断风暴怎么查
VF直通到虚拟机后,虚拟机里GPU负载一高就出现卡顿,宿主机的mpstat能看到某个CPU软中断几乎占满,这基本就是中断风暴。常见原因有两个:一是VF的多个队列共用同一个中断向量,高并发时互相冲刷;二是宿主机侧irqbalance把动态分配的中断固定到了不合适的CPU,导致缓存抖动。
排查时先在宿主机上查看该VF绑定的中断:
bash复制cat /proc/interrupts | grep -i mygpu
cat /sys/bus/pci/devices/0000:01:00.1/msi_irqs/
如果发现多个队列映射到同一个vector,就要考虑增大MSI-X向量数,或者在驱动里显式配置每个队列的CPU亲和性。很多时候不是硬件不够,而是KMD初始化时没有把MSI-X资源按预期切好。
5.3 显存隔离与性能抖动排查
显存隔离是SR-IOV的硬指标。一个VF崩溃不能影响其他VF,更不能让PF本身挂掉。我在做稳定性测试时曾经遇到一个VF的非法DMA地址把整个GPU显存内容搞乱,最后定位是固件没有对VF的地址访问做二次校验,导致它访问到了PF保留区域。
这个问题在KMD层面有三个切入点:第一,VF创建时显存分配列表是否正确;第二,GPU内部地址翻译单元有没有正确配置;第三,VFIO或IOMMU层的DMA映射是否覆盖了所有BAR区。任何一个地方漏掉,都可能出现性能暴跌或者数据损坏。性能抖动还有一种常见情况是显存带宽被打满,一个VF的读写访问挤占其他VF的带宽。这种问题不能用隔离手段消除,只能靠QoS机制限制每个VF的带宽预算。
6. 学习路径与一点经验
经常有人问我做GPU KMD到底该怎么学。我的建议是,先把PCIe的配置空间、BAR、MSI-X和DMA理解透,再结合Linux内核的SR-IOV代码框架去看厂商开源驱动或网卡驱动。然后动手写一个小驱动,在支持SR-IOV的GPU上做创建VF、绑定VFIO、跑一个简单内核态测试程序,整个过程走通了,PF和VF的很多概念就自然串起来了。
有人会觉得厂商KMD代码不开源,学了内核框架也没用。其实不是,NVIDIA和AMD的新驱动都离不开PCI核心提供的SR-IOV基础,很多概念是通用的。而在真正写产品代码时,我个人的体会是:先把资源映射和错误处理的边界画清楚,再优化性能。PF/VF的问题,八成都是资源和边界问题,把这两件事弄明白,后续遇到virtualization相关的bug会少走很多弯路。
