做GPU虚拟化和驱动开发的兄弟,对PF和VF绝对不陌生。这两个缩写经常一起出现,问起来大家也能说上两句:PF是Physical Function,物理功能;VF是Virtual Function,虚拟功能。但你要是真去翻SR-IOV规范,再进KMD(Kernel Mode Driver)源码里走一圈,很多细节就会开始绕:VF的配置空间到底谁初始化?显存是谁分配的?VF发生page fault谁来处理?虚拟机里那个GPU驱动,驱动的到底是个什么东西?
这篇文章是GPU KMD学习专栏的2.3节。我会把PF/VF在GPU里的应用讲成“手把手”级别的实操经验:先讲清楚SR-IOV在GPU里到底解决什么问题,再从KMD视角拆解PF和VF怎么分工,接着走一遍在Linux上拉起PF/VF的真实流程,最后把资源隔离、中断、调度以及我踩过的坑全部整理出来。如果你是做GPU驱动、搞虚拟化平台,或者只是好奇云上那张vGPU是怎么切出来的,这篇都值得读到底。
1. SR-IOV先把概念对齐:PF是“房东”,VF是“租户”
1.1 从PCIe视角看清PF和VF的本质
SR-IOV(Single Root I/O Virtualization)这个标准最早其实是给网卡设计的,目标是让一个物理PCIe设备在硬件层面就能“分身”成多个独立的PCIe设备。在它出现之前,虚拟化环境里要让多个虚机共享网卡,基本都得靠软件在hypervisor层做转发,CPU开销大,延迟也高。SR-IOV的思路特别直接:在设备内部由硬件直接生成一批轻量级“功能”,每个功能对上层软件来说就是一个独立的PCIe设备,有自己的BDF(Bus/Device/Function)、配置空间、BAR、中断能力,可以直接分配给不同的虚机或者容器。数据路径从虚机直接到设备,hypervisor可以靠边站,性能和隔离一下就都上来了。
PF就是这个物理设备上那个完整的功能。它有完整的PCIe配置空间,拥有SR-IOV Capability,负责管理整个设备,包括创建VF、分配资源、处理错误。VF是从PF“派”出来的功能,没有PF那么全的能力,它不能直接管理物理设备层面的资源,而是共享物理GPU里的计算单元、显存、引擎这些硬件资源,能做的操作会被PF严格控制。
拿房子来打比方:整栋楼是物理GPU,PF就是楼里的物业公司,管水电、管门禁、管租户;VF是租出去的房间,租户可以在房间里随心所欲,但改承重墙、改外立面这种事,必须找物业。一个PF可以生成多个VF,具体数量看硬件设计,PCIe规范里用TotalVFs字段给出上限。
这里要特别强调一个容易搞混的点:VF虽然看起来像一个完整的PCIe设备,但它并不是真正的全功能设备。VF没有独立的SR-IOV Capability,很多配置空间字段是只读的,或者直接由PF控制。所以你在虚机里看到的那个“GPU”,本质上是一个受控的硬件视图。KMD代码里对PF和VF的处理逻辑也完全不一样,PF走的是完整的设备管理路径,VF走的是裁剪后的作业提交路径。
1.2 GPU虚拟化演进:为什么绕不开PF/VF
GPU虚拟化其实走了好几步才到今天这个形态。最早最简单的是PCI直通(passthrough):直接把一张物理GPU分配给一个虚机,性能最好,但物理卡只能给一个租户用。大模型训练这种场景,一张卡大几十万,这么搞太浪费,云厂商根本扛不住成本。
后来有了软件模拟,比如virtio-gpu这种纯CPU模拟方案。兼容性好了,但3D能力和CUDA/ROCm这种高性能计算完全没法看,只能当显示设备用,跑AI任务想都别想。
再往后就是硬件辅助虚拟化,典型实现就是SR-IOV,以及和它经常一起出现的mdev(mediated device)框架。NVIDIA的vGPU、AMD的MxGPU、Intel的GVT-g,本质上都是让一张物理GPU在硬件层面拆出多个VF,再把VF包装成可管理的虚拟设备分给多个虚机。这里要严谨一点:Intel的GVT-g严格说走的是mediated passthrough,不是标准SR-IOV,但它对外暴露的设备模型非常类似,NVIDIA vGPU和AMD MxGPU则更依赖SR-IOV。核心思想是一样的:一张物理卡,多个独立设备视图,共享底层硬件。
云厂商卖的那种“XX显卡云主机”,或者大厂把A100切成几份分给不同业务线的,背后基本都是这套路子。相比软件模拟,SR-IOV的虚拟化开销极小,因为数据路径是硬件直通,hypervisor不用在中间搬数据;相比整卡直通,它又能做到多租户共享同一张卡,资源利用率高得多。
所以在GPU KMD里,PF/VF不是学术概念,而是代码里真实存在的两种设备模型。读驱动源码时你会发现,PF和VF在probe流程、资源管理、中断处理、错误恢复上几乎全是两条路径。理解了这一点,后面看代码会顺很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. KMD视角下PF/VF的分工与资源划分
2.1 配置空间里的“合同书”:SR-IOV Capability
在PCIe配置空间里,PF会有一个独立的扩展Capability,叫SR-IOV Capability,这是整件事的源头。关键字段包括TotalVFs、NumVFs、FirstVF Offset、VF Stride、VF BAR等。
TotalVFs表示这块GPU硬件最多能支持多少个VF。NumVFs是当前实际使能的VF数量,写这个寄存器,其实就是让硬件开始“生”VF。FirstVF Offset和VF Stride决定VF在PCI总线域里的BDF怎么排布。比如第一个VF在物理PF的bus/device/function基础上偏移某个值,后面每个VF按Stride递增。你实际用lspci扫到的那些带VF标记的设备,BDF就是这么算出来的。
KMD在启动流程里会读取这些字段,确认硬件能力上限;当管理面请求创建VF时,会写NumVFs,然后逐个初始化VF的配置空间和内部状态。这个过程不是简单地填几个硬件寄存器就够了,还要为VF分配显存、配置GPU页表、初始化mailbox通道,工作量一点都不小。
读Linux下KMD代码时,我建议你从pci_enable_sriov()这个入口往里跟。这是Linux PCI核心提供的标准接口,厂商KMD(NVIDIA、AMD、Intel各自的vendor驱动)都会在这个基础上做扩展,比如mdev设备的注册、显存切分策略等。把这条调用链理一遍,你对PF/VF的理解会扎实很多。
2.2 PF驱动和VF驱动各管什么
在KMD层面,PF驱动和VF驱动的“边界”很清晰。PF驱动运行在宿主机上,是整个设备的管家:负责枚举物理GPU、初始化硬件、处理电源管理、创建和销毁VF、给VF分配显存和引擎配额、处理AER错误上报、响应VF通过mailbox发来的请求。反正物理设备层面的事,全部由PF驱动兜底。
VF驱动运行在虚机里,看到的是一个被裁剪过的PCIe设备视图。VF驱动能做的事情,基本就是向GPU提交作业:分配显存(通常通过mailbox向PF申请)、创建GPU context、提交命令队列、处理自己的中断。它不能直接操作物理设备的全局资源,比如重置整卡、改全局电源状态,这些都是PF的权限。
这里有个很常见的疑问:VF驱动既然能力被限制了,为什么性能还这么好?关键在于VF驱动提交作业的路径很少经过PF。命令提交是直接写到自己VF的doorbell寄存器,DMA也是直接从虚机内存到GPU显存,PF只负责资源分配和异常处理这类“低频”操作。所以时延和吞吐都接近直通,这正是SR-IOV最核心的价值。
2.3 BAR空间、中断与DMA的隔离
一个VF暴露给guest的BAR空间通常很小,一般只放doorbell、命令队列、状态页这类东西。显存要么通过PF BAR的偏移映射过去,要么在mdev框架下通过VFIO的region暴露给guest。NVIDIA vGPU里,guest驱动访问显存大体上还是走自己的BAR映射,但实际物理页面的分配由PF统一管理,这样才能保证两个VF不会踩到同一块显存。
中断方面,VF支持MSI-X,每个VF有自己独立的中断表。guest驱动申请中断向量时,VF通过自己的配置空间拿到MSI-X Table地址,写中断向量。PF层面的KMD会为这些中断做好路由,比如把不同VF的MSI-X中断绑定到不同CPU核上,避免中断都挤在一个核上形成瓶颈。
DMA的隔离则依赖IOMMU。SR-IOV之所以安全,就是因为每个VF有独立的Requester ID,IOMMU可以针对每个VF单独建立地址映射。guest里的驱动即使写坏了自己的页表,也只能在自己VF的地址空间里折腾,够不到别的VF,也碰不到宿主机的内存。
3. 实操:在一个支持SR-IOV的GPU上拉起完整的PF/VF链路
3.1 环境准备:BIOS、IOMMU、内核参数
先说BIOS。SR-IOV和IOMMU(Intel VT-d或AMD-Vi)都必须在BIOS里打开。很多服务器默认关着,尤其是VT-d,经常要进BIOS的System Configuration里翻半天。打开之后,Linux启动参数要加上intel_iommu=on iommu=pt(AMD平台是amd_iommu=on)。iommu=pt的含义是能直通就直通,减少DMA重映射开销,这在SR-IOV场景下几乎是必须的,否则性能会掉得很难看。
启动后先看硬件认不认SR-IOV。用lspci -vvv -s <BDF>看输出里有没有“SR-IOV VF”相关的capability。如果根本看不到,先怀疑BIOS或者板卡固件,而不是驱动。
bash复制# 查看PCIe设备详细信息,重点关注SR-IOV能力
lspci -vvv -s 0000:3b:00.0
再检查IOMMU有没有生效:
bash复制dmesg | grep -i iommu
ls /sys/kernel/iommu_groups/
如果iommu_groups目录是空的,说明IOMMU没起来,后面分配VF给虚机基本免谈。
3.2 创建VF的完整流程拆解
在Linux上创建VF,标准操作是往sysfs接口里写数值:
bash复制# 创建4个VF,数值写入后硬件立即生效
echo 4 > /sys/bus/pci/devices/0000:3b:00.0/sriov_numvfs
执行完以后,lspci大概率会多出几个设备,BDF在物理PF的基础上按SR-IOV Capability里的offset和stride排布。每个VF在/sys/bus/pci/devices/下也会出现对应的目录。
但厂商驱动一般不满足于裸VF。NVIDIA vGPU的做法是底层用SR-IOV创建VF,上层再通过mdev框架创建mediated device。每个mdev实例有自己的UUID,并且对应一个vGPU type(比如A16-4Q这种,定义了显存大小、GPU引擎配额、最大分辨率等)。创建vGPU的命令一般长这样:
bash复制# 在物理PF下按指定type创建vGPU实例
nvidia-smi vgpu -c 4 -t A16-4Q -p 0000:3b:00.0
# 或者用mdevctl,更通用的方式
mdevctl define --parent 0000:3b:00.0 --type vfio-pci --uuid <uuid>
这类工具背后做的事情,就是先让PF驱动准备好VF,再把VF注册为mdev设备,最终在/sys/bus/mdev/devices/<uuid>/路径下暴露给VFIO。
如果只是想快速验证,不依赖厂商vGPU管理工具,也可以直接把VF绑定到vfio-pci驱动:
bash复制# 以NVIDIA的vendor ID和device ID为例,把VF绑定到vfio-pci
echo "10de 2684" > /sys/bus/pci/drivers/vfio-pci/new_id
echo 0000:3b:00.1 > /sys/bus/pci/drivers/nvidia/unbind
echo 0000:3b:00.1 > /sys/bus/pci/drivers/vfio-pci/bind
绑定后这个VF就能直接交给QEMU,配合-device vfio-pci,host=0000:3b:00.1使用。不过要注意,这种方式只能拿到一个裸VF设备,没有厂商vGPU的显存切分和QoS管理。很多GPU的裸VF在guest里没法正常驱动,生产环境还是走厂商方案,这点别搞混。
3.3 VF在虚拟机里的表现和驱动安装
guest侧看到的设备,仍然是一个PCIe设备,只是BDF、Device ID和宿主机原始物理卡不一样。在虚机里,要装对应厂商的guest驱动(比如NVIDIA vGPU guest驱动),它会识别出当前设备是VF,走VF驱动路径。guest驱动继续走标准CUDA/ROCm接口,应用层基本感知不到下面是一块被切过的GPU。
验证的方法也简单:guest里跑nvidia-smi,看到显存大小是vGPU type里定义的值,而不是物理卡全部显存,就说明PF/VF这条链路基本通了。如果显示的还是物理卡全部显存,那就要怀疑guest驱动是不是走错了分支,把VF当PF初始化了。
容器场景下,现在GPU Operator这类方案更多是直接在宿主机上分配物理GPU或MIG实例,真正把VF塞进容器用得少,除非后面接的是SR-IOV网络设备。但如果你在做K8s+GPU虚拟化,思路是一样的:VF或mdev设备通过VFIO暴露给容器,配合device plugin做调度。整个数据路径和VM场景没有本质区别。
4. 资源分配、隔离与调度:PF/VF最容易踩坑的地方
4.1 显存怎么分才安全
显存是GPU虚拟化里最敏感的资源。SR-IOV的VF本身不知道物理GPU有多少显存,它只知道自己被分配的那一块。厂商实现通常会为每个VF预留一段物理显存区域,并且让VF驱动的地址空间只能落在这段区域内。这个预留不是简单地把地址算出来就行,还要配置好GPU内部的显存映射、页表、以及访问权限,任何越界都要被硬件拦截。
不同厂商策略不同。NVIDIA的vGPU会把显存按profile切片,比如一个A16-4Q就是某个固定显存配额;MIG(Multi-Instance GPU)更进一步,把L2缓存、显存控制器、SM都物理切分,隔离性更强。AMD的MxGPU和Intel的GVT-g也有自己的显存管理方式。在KMD开发里,你看到的显存分配代码往往不是简单的kmalloc,而是和GPU硬件单元强相关的:显存控制器有多少个channel、每个channel对应哪些物理地址区间、VF之间的地址空间要不要做别名映射,这些都会影响最终分配策略。
实操中遇到最多的问题是显存碎片化。PF频繁创建销毁VF,显存区域的分配和释放如果不做整理,时间长了会出现大量无法满足新VF请求的碎片,导致创建失败。所以生产环境一般不会动态开很多VF,而是提前规划好vGPU type和数量,静态切好,稳定性优先。
4.2 中断、IOMMU与性能损耗
SR-IOV性能损耗通常很低,但有几个地方容易放大损耗。
第一是IOMMU的TLB。VF数量多、每个VF又频繁做DMA映射和解除映射时,IOMMU页表的TLB miss会成为瓶颈,尤其在高带宽场景。这也是为什么iommu=pt能在SR-IOV场景下带来明显收益,它能减少一层地址转换。
第二是中断。VF的MSI-X中断如果都落在同一个CPU核上,很容易出现软中断风暴。KMD可以给VF的中断设置IRQ affinity,把不同VF的中断分散到不同核上。这个配置在排查性能问题时值得先看一眼。
第三是GPU内部TLB。GPU有自己的页表缓存,多VF并发时,如果页表频繁更新,TLB shootdown的开销会明显增加。做底层优化时,减少页表更新频率往往比单纯提高频率更有效。
4.3 多VF调度:时间片、QoS与公平性
GPU和网卡不一样。网卡的VF彼此之间几乎独立,但GPU的计算单元是共享的。一个VF如果提交了巨大的计算任务,理论上可能把SM占满,其他VF就得排队。所以厂商KMD里会有调度器,负责把GPU引擎(计算、拷贝、图形)按时间片或者优先级分给各个VF。
MIG之所以贵,很大程度上就是因为它在硬件层面就做了物理隔离,不需要软件调度器在那里算时间片,一个实例的计算再重也影响不到别人。如果用的是基于SR-IOV的vGPU,QoS策略就非常关键。比如可以限制某个VF的计算占比、拷贝带宽,防止“邻居吵你”。这些参数在不同厂商工具里名字不一样,但基本思路就是给VF设置权重或者配额。
我在实际项目里见过因为忘记设置vGPU type,导致所有VF都分到默认的低配,大规模训练任务一上来就OOM的情况。所以创建VF之前,先对着业务需求把profile定清楚,这是省心事的做法。
5. 常见问题与排查技巧实录
5.1 创建VF失败怎么办
创建VF不成功,最常见的几个原因:
- BIOS里SR-IOV没开,或者VT-d/AMD-Vi没开。
- IOMMU没生效,
/sys/kernel/iommu_groups/目录为空。 - 物理GPU的固件/驱动版本不支持,或TotalVFs为0。
- 显存碎片化,想创建的vGPU type需要的显存不足。
- 厂商工具和驱动版本不匹配。
排查时先看dmesg,PF驱动一般会把错误原因打出来。比如No free vGPU profile slot或者Failed to allocate framebuffer。再看lspci -vvv确认SR-IOV Capability字段。如果TotalVFs显示0,那大概率是固件层面没开放,不是驱动的问题。记得还要检查有没有其他进程占用了设备,比如NVIDIA的nvidia-smi和vGPU manager版本不匹配,也会导致创建VF静默失败。
5.2 虚机里看不到GPU或者NVML报错
VF分配给虚机后,guest里装了驱动却看不到设备,先别急着怀疑驱动。在host上用virsh dumpxml或者QEMU命令行确认设备是否真的挂上了。接着看guest的lspci,确认BDF、Device ID是否正确。
NVIDIA vGPU场景下,guest驱动和宿主机vGPU manager有严格的版本匹配关系,版本不匹配时会直接报Incompatible driver,这个错误提示很明确,按提示升级对应版本就行。
还有一种情况:VF已经挂进去,但guest里nvidia-smi显示的显存大小不对。这往往是host侧没有按预期创建指定profile的vGPU,或者guest驱动走错了分支。遇到这种情况,回host看一眼mdevctl list和nvidia-smi vgpu,确认UUID和profile匹配,再在guest里卸载重装驱动,基本能解决。
5.3 性能异常的排查路径
VF性能达不到预期,我通常会按下面的顺序排查:
- 确认host BIOS里
iommu=pt是否设置,如果没设,先加上再测。 - 看中断分布:
cat /proc/interrupts,观察VF的MSI-X中断是否集中在某个CPU上,必要时调整/proc/irq/<irq>/smp_affinity。 - 看IOMMU开销:用
perf stat统计DMA映射相关事件,或者直接对比直通和VF的性能差异。如果差距很大,多半是IOMMU TLB、页表更新频率的问题。 - 看GPU内部调度:用厂商工具确认当前VF的引擎占用,是不是被其他VF抢了资源。
性能问题往往不是单点,而是叠加出来的。先跑一轮基准测试(比如DGEMM、大矩阵乘法),再逐步加并发VF,看性能曲线怎么变化,比较容易定位瓶颈。
5.4 问题速查表
| 现象 | 可能原因 | 排查命令/动作 |
|---|---|---|
| 写sriov_numvfs失败 | BIOS未开SR-IOV或TotalVFs为0 | lspci -vvv -s <BDF>,查SR-IOV Capability |
| IOMMU未生效 | 启动参数缺失 | `dmesg |
| guest看不到VF | 设备未挂载或驱动版本不匹配 | host侧virsh dumpxml,guest侧lspci |
| guest驱动报Incompatible | vGPU manager与guest驱动版本不匹配 | 核对厂商版本兼容表 |
| VF性能差 | iommu=pt未开或中断集中 |
cat /proc/interrupts,调整smp_affinity |
| 创建vGPU报显存不足 | 碎片化或profile分配不合理 | 检查显存分配,重新规划profile |
| 一个VF跑任务其他VF卡 | 没有QoS/权重配置 | 调整vGPU profile或调度策略 |
做这一块时间长了,我的体会是:PF/VF的坑,七成不在驱动代码里,而在硬件配置和资源规划上。很多人一上来就翻KMD源码,结果发现卡在BIOS、IOMMU、固件这些“外围”环节。所以如果你也是新手,建议先把PCIe拓扑和IOMMU链路摸清楚,再进代码,会顺利很多。另外一个很实用的经验是:别在生产环境里频繁动态创建销毁VF,事前把profile规划好,稳定性比弹性更重要。希望这篇对你有帮助,有类似的坑也欢迎一起交流。
