我第一次被PF和VF这两个词卡住,是在给一台服务器上的A100做GPU虚拟化的时候。当时的场景很典型:一张卡算力足够,但多个训练任务都想用,谁也不愿意排队等一整张卡。查了一堆资料,所有方案里都绕不开SR-IOV、Physical Function、Virtual Function,可一打开PCIe配置空间文档,全是一堆看不懂的寄存器描述。后来把Linux内核里SR-IOV相关代码和NVIDIA驱动加载日志对着看了一遍,才算是把PF和VF整条链路捋顺。
这篇文章定位在KMD(Kernel Mode Driver)专栏的2.3节,面向的是想搞懂GPU底层虚拟化原理的驱动开发者、虚拟化平台工程师,以及正在为AI训练容器分配GPU资源的人。它不打算复述PCI-SIG规范原文,而是带着你从“一张GPU怎么变成多张GPU”这个问题出发,把PF、VF的硬件身份、驱动加载路径、虚拟化交付方式和常见坑位全部过一遍。
1. PF和VF的硬件身份:理解PCIe设备的分身逻辑
1.1 一个PCIe设备为何需要“功能”这个概念
PCIe设备在总线上以“总线号:设备号:功能号”三元组标识。一个物理PCIe设备可以包含多个“功能”,每个功能有独立的配置空间、BAR(Base Address Register)空间和中断能力。
“功能”这个概念是理解PF/VF的起点。在传统PCIe里,端到端的设备(Endpoint)大多只有一个功能,比如显卡只有一个图形功能,声卡只有一个音频功能。多功能的例子是网卡同时提供管理接口和数据接口,所以常见是两个功能。
SR-IOV规范在单根虚拟化场景下,把“功能”这个概念扩展到了两个层面:
- Physical Function(PF) 是物理功能,承担完整的管理、配置和控制职责,拥有完整的PCIe配置空间,可以像普通设备一样被驱动完整操控。
- Virtual Function(VF) 是虚拟功能,是PF通过SR-IOV能力派生出来的轻量级功能,自身只有部分配置空间,不具备完整的系统资源管理能力,必须由PF驱动代为管理底层资源。
类比一下:PF是整栋楼的物业,掌握楼宇管道、电力和门禁的总控权;VF是租出去的几个单元,住户可以在单元里自由布置家具,但水电总闸、承重墙和消防通道仍然归物业管,住户只能看到自己单元的那块区域。
1.2 SR-IOV能力结构里藏着哪些关键参数
在PCIe的扩展配置空间里,SR-IOV扩展能力(Extended Capability ID 0x0010)定义了一段结构化数据,它告诉系统这个设备最大支持多少VF、如何分配VF、每个VF需要多大的资源窗口。Linux内核头文件include/uapi/linux/pci_regs.h里定义了这些字段的寄存器偏移,日常排查时不需要硬背,但要理解几个关键字段的作用:
| 字段 | 作用 |
|---|---|
| InitialVFs | 设备出厂时默认提供的VF数量 |
| TotalVFs | 设备硬件上限支持的VF总数 |
| NumVFs | 当前软件配置启用的VF数量 |
| First VF Offset | 第一个VF相对于PF的Device编号偏移 |
| VF Stride | 相邻两个VF之间的Device号间隔 |
| VF Device ID | VF在PCI总线上上报的Device ID |
这里最容易忽略的是First VF Offset和VF Stride。它们决定了VF在PCI总线枚举时落在哪个位置。比如PF在device 0,First VF Offset=1,Stride=2,那VF0在device 1,VF1在device 3,VF2在device 5。这个布局对后续IOMMU组的划分和直通配置有直接影响,因为在很多平台上每个VF都会被分到不同的IOMMU group,而IOMMU group是能否安全直通给虚拟机的最小单位。
注意:不同厂商的SR-IOV实现里VF布局差异很大,一定要以
lspci和/sys/bus/pci/devices/下实际读到的为准,不要直接套文档里的示例值。
1.3 为什么GPU的PF和VF跟网卡不一样
网卡的SR-IOV大家已经见多了,VF就是一块“虚拟网卡”,给每个VM一张,性能接近物理网卡。GPU的SR-IOV则复杂得多。
第一,GPU不是一个简单的“数据包转发”设备,它包含算力引擎(SM/CU)、显存控制器、DMA引擎、视频编解码器、显示输出等多个子系统。VF要共享这些子系统,硬件必须提供上下文级的隔离,不只是给个PCIe配置空间就行。每增加一个VF,GPU就需要多预留一份上下文槽位、命令队列和显存窗口。
第二,显存资源的划分。网卡VF之间的资源隔离主要是队列和中断,显存本来就不存在。GPU的显存是稀缺资源,VF能访问哪些显存地址范围、能分配多大容量,需要PF驱动或者管理固件做细粒度控制。NVIDIA vGPU方案里的显存配额,本质上就是PF驱动对VF的显存映射表做限制。
第三,调度模型。网卡的收发队列天然并发,丢一个包也无所谓。GPU中的一个kernel如果被调度器抢占,过程复杂得多:上下文切换代价高,shared memory和寄存器状态都必须保存恢复。所以GPU的VF方案在硬件调度器层面就要决定是按时间片轮转还是按多优先级抢占,这直接影响多租户下的QoS。
我在最开始查资料的时候,一直不理解为什么有的资料把“VF”和“vGPU”混着说。后来理清:VF是PCIe硬件层面的概念,vGPU是产品层面的概念。vGPU方案可以基于SR-IOV的VF实现,也可以在非SR-IOV的API转发层实现,两者不能划等号。但今天绝大多数主机GPU(比如A100、H100、L40S)的vGPU,底层都是靠SR-IOV能力在做硬件切分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. KMD视角下的PF和VF:从驱动加载到资源切分
2.1 PF驱动初始化路径:不只是调用pci_enable_sriov
在Linux内核源码drivers/pci/iov.c里,可以看到SR-IOV的核心框架代码。一个支持SR-IOV的PF驱动,在probe函数里做的主要事情如下:
- 调用
pci_enable_sriov(pdev, nr_virtfn),传入要启用的VF数量。内核会读取SR-IOV capability结构,按First VF Offset和VF Stride计算每个VF的bus/device/function编号,通过PCI核心框架创建对应的pci_dev结构。 - 在驱动内维护每个VF的上下文数据。GPU驱动通常会在
pf_data结构里维护一个vf数组,记录每个VF的状态、显存起始地址、大小、已分配队列等。 - 初始化与固件/GPU硬件的通信。PF驱动要告诉GPU固件“我要启用几个VF”,把VF的配置写入GPU的BAR空间或者通过mailbox命令通知固件。
- 为VF创建中断和DMA重映射资源。这涉及IOMMU页表,一般来说PCI核心和IOMMU框架会自动完成,但GPU驱动需要和
iommu_domain关联起来,否则VF分配的DMA地址可能与IOMMU页表不一致。
重要的一点:pci_enable_sriov()只是创建了PCIe层面的VF设备,它的枚举和普通设备一样,会触发VF设备驱动(或vfio-pci)的probe。如果你调用enable后立刻在sysfs里看到VF,但VF驱动迟迟没有成功绑定,问题往往出在驱动probe路径或者IOMMU上。
2.2 VF驱动的轻量化启动流程
VF驱动比PF驱动轻量得多。一个典型的VF驱动(在NVIDIA驱动里叫vGPU guest驱动,在Linux SR-IOV语境下叫VF driver)的probe流程:
- 通过
pci_is_virtfn()判断当前设备是不是VF。驱动在probe入口处都会做这个检查,因为同一段代码要处理PF和VF两种模式。 - 只映射自己的BAR空间。VF的BAR空间往往是PF BAR中的某个子区间,映射时不做全量映射,只映射该VF对应的窗口。
- 初始化轻量级的命令提交队列。GPU的VF不能直接操作全局GPU寄存器,但可以通过门铃(doorbell)写队列索引,让硬件调度器知道有新任务到来。
- 与PF驱动建立通信。VF驱动在初始化时必须找到宿主机PF的通道,这在虚拟化场景里由Hypervisor或宿主机的vGPU管理组件完成;在裸机SR-IOV场景里,则通过共享内存或mailbox。
这个“轻量”并不是说VF驱动简单,而是它不需要管硬件初始化、固件下载、电源管理、错误恢复这类“物业级”事务。一旦VF驱动试图执行这些操作,硬件会直接拒绝,甚至触发FLR复位。
2.3 PF与VF之间的通信通道:mailbox与doorbell
PF和VF之间的通信,在GPU虚拟化里是绕不开的重点。除了PCIe配置空间之外,它们通常通过两类机制沟通:
- Mailbox(信箱):用于慢速、低频的控制信息,比如VF请求分配显存、PF通知VF发生GPU重置、固件上报错误等。在NVIDIA的vGPU方案里,PF驱动和VF guest驱动之间有一组私有mailbox窗口,通常映射在共享内存或设备的私有BAR区域。
- Doorbell(门铃):用于快速提交工作。GPU的SM/计算引擎前端寄存器、命令队列索引等,都通过doorbell通知对端。
你可以在驱动日志里搜索mailbox相关字样来验证这两个模块是否正常工作。举个例子,NVIDIA vGPU方案中,如果guest驱动加载时PF驱动没有把VF mailbox初始化完成,guest里会看到类似“vGPU device failed to probe”的错误,这种时候优先排查PF侧日志,而不是怀疑guest驱动。
我自己在调试一块VF初始化失败时,翻到PF驱动的日志发现有一条“Mailbox request timeout”。当时PF驱动因为正在做FLR复位,把VF的mailbox请求给丢了。后来通过控制触发FLR的时机,并且在PF驱动里增加握手重试机制,问题才解决。这种问题在纯PCIe层是查不出来的,必须同时看PF侧和VF侧的日志时间线。
3. 把VF交付给虚拟机:IOMMU、VFIO与QEMU的实操链路
3.1 内核参数与BIOS设置:IOMMU是前提
要让VF安全地直通给虚拟机,IOMMU是不能跳过的环节。IOMMU负责DMA重映射,把guest里设备发起的DMA地址翻译到宿主机物理地址,同时防止guest通过设备访问宿主机的任意内存。没有IOMMU,VF直通等于裸奔:恶意的guest可以构造DMA请求读到宿主机内存中的敏感数据,这种风险在云环境中是不可接受的。
在Linux宿主机上,需要保证:
- BIOS/固件里开启VT-d(Intel平台)或IOMMU(AMD平台)。
- 内核启动参数追加
intel_iommu=on iommu=pt(AMD平台为amd_iommu=on iommu=pt)。 - 确认IOMMU已生效,检查
/sys/kernel/iommu_groups目录下是否有非空group。
提示:
iommu=pt是pass-through模式,表示IOMMU只为直通设备做DMA重映射,不介入普通内核设备访问,这样可以避免大部分平台上的性能回退。
3.2 创建VF并绑定vfio-pci的完整操作
假设PF设备位于0000:03:00.0,先查看它的SR-IOV能力:
bash复制lspci -vvv -s 03:00.0 | grep -i sriov
确认支持后,启用4个VF:
bash复制echo 4 > /sys/bus/pci/devices/0000:03:00.0/sriov_numvfs
此时lspci应该能看到类似03:00.1、03:00.2、03:00.3、03:00.4的VF设备,它们的Device ID和PF不同。
接下来把VF交给vfio-pci驱动接管:
bash复制echo vfio-pci > /sys/bus/pci/devices/0000:03:10.0/driver_override
echo 0000:03:10.0 > /sys/bus/pci/drivers/vfio-pci/bind
这里的关键问题是:选择一个具体的VF地址作为直通对象,还是通过Vendor/Device ID让vfio-pci自动匹配所有VF。在GPU场景里,我倾向于只绑定要用的那几张VF,不要用new_id全类绑定,否则宿主机PF自己的管理功能也可能被接管,影响后续控制。
