Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 117 additions & 51 deletions pkg/nvpassthrough/nvpassthrough.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/NVIDIA/go-nvlib/pkg/nvpci"
Expand Down Expand Up @@ -162,8 +163,8 @@ func (n *nvpassthrough) FindBestVFIOVariant(address string) (string, error) {
// BindToVFIODriver binds the provided NVIDIA PCI device to the
// vfio-pci driver (or a variant VFIO driver if one is preferred).
// This function takes care of additional logic, like making sure
// the vfio-pci driver is loaded first and that an auxiliary graphics
// device also get bound to the vfio-pci driver.
// the vfio-pci driver is loaded first and that every other PCI function
// of the same card also gets bound to the vfio-pci driver.
func (n *nvpassthrough) BindToVFIODriver(address string) error {
device, err := n.nvpciLib.GetNvidiaDeviceByPciBusID(address)
if err != nil {
Expand Down Expand Up @@ -221,25 +222,25 @@ func (n *nvpassthrough) BindToVFIODriver(address string) error {
}
}

// For graphics mode, bind the auxiliary device as well
auxDev, err := getGraphicsAuxDev(device)
// Bind every other function of the same card. The whole IOMMU group must be bound to a
// vfio driver (or to nothing) before VFIO will hand it to a guest.
auxDevs, err := getAuxDevices(pciDevicesRoot, device)
if err != nil {
return fmt.Errorf("failed to get graphics auxiliary device for %s: %w", device.Address, err)
}
if auxDev == nil {
return nil
}
if auxDev.Driver == vfioDriverName {
return nil
return fmt.Errorf("failed to get auxiliary devices for %s: %w", device.Address, err)
}
for _, auxDev := range auxDevs {
if auxDev.Driver == vfioDriverName {
continue
}

n.logger.Infof("Binding graphics auxiliary device %s to driver: %s", auxDev.Address, vfioDriverName)
n.logger.Infof("Binding auxiliary device %s to driver: %s", auxDev.Address, vfioDriverName)

if err := unbind(auxDev.Address); err != nil {
return fmt.Errorf("failed to unbind graphics auxiliary device %s: %w", auxDev.Address, err)
}
if err := bind(auxDev.Address, vfioDriverName); err != nil {
return fmt.Errorf("failed to bind graphics auxiliary device %s to %s: %w", auxDev, vfioDriverName, err)
if err := unbind(auxDev.Address); err != nil {
return fmt.Errorf("failed to unbind auxiliary device %s: %w", auxDev.Address, err)
}
if err := bind(auxDev.Address, vfioDriverName); err != nil {
return fmt.Errorf("failed to bind auxiliary device %s to %s: %w", auxDev.Address, vfioDriverName, err)
}
}

return nil
Expand All @@ -260,7 +261,7 @@ func (n *nvpassthrough) BindToDriver(address string, driver string) error {

// Unbind unbinds the provided NVIDIA PCI Device from
// any driver it is currently bound to. This function also ensures
// an auxiliary graphics device is also unbound.
// that every other PCI function of the same card is unbound.
func (n *nvpassthrough) Unbind(address string) error {
device, err := n.nvpciLib.GetNvidiaDeviceByPciBusID(address)
if err != nil {
Expand All @@ -274,14 +275,14 @@ func (n *nvpassthrough) Unbind(address string) error {
return fmt.Errorf("failed to unbind device %s: %w", address, err)
}

// For graphics mode, unbind the auxiliary device as well
auxDev, err := getGraphicsAuxDev(device)
// Unbind every other function of the same card, mirroring BindToVFIODriver.
auxDevs, err := getAuxDevices(pciDevicesRoot, device)
if err != nil {
return fmt.Errorf("failed to get graphics auxiliary device for %s: %w", address, err)
return fmt.Errorf("failed to get auxiliary devices for %s: %w", address, err)
}
if auxDev != nil {
for _, auxDev := range auxDevs {
if err := unbind(auxDev.Address); err != nil {
return fmt.Errorf("failed to unbind graphics auxiliary device %s: %w", auxDev.Address, err)
return fmt.Errorf("failed to unbind auxiliary device %s: %w", auxDev.Address, err)
}
}

Expand Down Expand Up @@ -327,46 +328,111 @@ func unbind(address string) error {
return nil
}

func getGraphicsAuxDev(device *nvpci.NvidiaPCIDevice) (*nvidiaPCIAuxDevice, error) {
if device.Class != nvpci.PCIVgaControllerClass {
// getAuxDevices returns every other PCI function that belongs to the same physical card as
// device.
//
// VFIO assigns an entire IOMMU group to a guest, and refuses the group unless every device in
// it is bound to a vfio driver or to no driver at all. A discrete NVIDIA GPU is a
// multi-function PCI device: .0 VGA/3D, .1 HDMI audio, and on Turing-era boards .2
// (VirtualLink USB xHCI) and .3 (USB-C UCSI). If any of those is left bound to a host driver
// such as xhci_hcd, the group is not viable and passthrough fails with
// "vfio: group N is not viable".
//
// Functions are discovered two ways, and the results are unioned:
//
// - Sibling PCI functions sharing the same domain:bus:device. This is the only way to find
// the VirtualLink xHCI and USB-C UCSI functions, which do not create a device link back
// to the GPU.
// - "consumer:pci:" device links, which the HDA audio function does create.
//
// Siblings are scoped to the physical card rather than to the IOMMU group on purpose: where
// ACS is unavailable a group can span an entire root port, and unbinding unrelated devices
// from their drivers would be harmful.
func getAuxDevices(devicesRoot string, device *nvpci.NvidiaPCIDevice) ([]*nvidiaPCIAuxDevice, error) {
if !device.IsGPU() {
return nil, nil
}

// Look for consumer symlink
entries, err := os.ReadDir(device.Path)
if err != nil {
return nil, err
}
seen := map[string]bool{device.Address: true}
var auxDevs []*nvidiaPCIAuxDevice

for _, entry := range entries {
if strings.HasPrefix(entry.Name(), "consumer") {
// Extract aux device name from consumer:pci:XXXX:XX:XX.X format
_, address, ok := strings.Cut(entry.Name(), consumerPrefix)
if !ok || address == "" {
continue
}
add := func(address string) error {
if address == "" || seen[address] {
return nil
}
path := filepath.Join(devicesRoot, address)
if _, err := os.Stat(path); err != nil {
// The function is not present on this host; there is nothing to bind.
return nil
}
// Auxiliary functions of a GPU are by definition the same vendor. Checking guards
// against acting on a device we did not intend to touch.
isNvidia, err := isNvidiaVendor(path)
if err != nil || !isNvidia {
return nil
}
Comment on lines +364 to +373

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if _, err := os.Stat(path); err != nil {
// The function is not present on this host; there is nothing to bind.
return nil
}
// Auxiliary functions of a GPU are by definition the same vendor. Checking guards
// against acting on a device we did not intend to touch.
isNvidia, err := isNvidiaVendor(path)
if err != nil || !isNvidia {
return nil
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
// The function is not present on this host; there is nothing to bind.
return nil
}
return fmt.Errorf("failed to stat auxiliary device %s: %w", address, err)
}
// Auxiliary functions of a GPU are by definition the same vendor. Checking guards
// against acting on a device we did not intend to touch.
isNvidia, err := isNvidiaVendor(path)
if err != nil {
return fmt.Errorf("failed to check vendor for auxiliary device %s: %w", address, err)
}
if !isNvidia {
return nil
}

driver, err := getDriver(path)
if err != nil {
return fmt.Errorf("failed to get driver for auxiliary device %s: %w", address, err)
}
seen[address] = true
auxDevs = append(auxDevs, &nvidiaPCIAuxDevice{
Path: path,
Address: address,
Driver: driver,
})
return nil
}

// Check if aux device exists
path := filepath.Join(pciDevicesRoot, address)
if _, err := os.Stat(path); err != nil {
// Sibling functions: same domain:bus:device, differing only in function number.
if slotPrefix, _, ok := strings.Cut(device.Address, "."); ok {
slotPrefix += "."
entries, err := os.ReadDir(devicesRoot)
if err != nil {
return nil, fmt.Errorf("failed to list PCI devices in %s: %w", devicesRoot, err)
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), slotPrefix) {
continue
}

auxDev := &nvidiaPCIAuxDevice{
Path: path,
Address: address,
if err := add(entry.Name()); err != nil {
return nil, err
}
}
}

driver, err := getDriver(path)
if err != nil {
return nil, fmt.Errorf("failed to get driver for graphics auxiliary device %s: %w", address, err)
}
auxDev.Driver = driver
return auxDev, nil
// Consumer device links, e.g. the HDA audio function.
entries, err := os.ReadDir(device.Path)
if err != nil {
return nil, fmt.Errorf("failed to list %s: %w", device.Path, err)
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "consumer") {
continue
}
_, address, ok := strings.Cut(entry.Name(), consumerPrefix)
if !ok {
continue
}
if err := add(address); err != nil {
return nil, err
}
}

return nil, nil
return auxDevs, nil
}

// isNvidiaVendor reports whether the PCI device at devicePath is an NVIDIA device.
func isNvidiaVendor(devicePath string) (bool, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also handle the FileNotFound here? Is the FileNotFound error to be tolerated instead?

contents, err := os.ReadFile(filepath.Join(devicePath, "vendor"))
if err != nil {
return false, fmt.Errorf("failed to read vendor of %s: %w", devicePath, err)
}
vendorID, err := strconv.ParseUint(strings.TrimPrefix(strings.TrimSpace(string(contents)), "0x"), 16, 16)
if err != nil {
return false, fmt.Errorf("failed to parse vendor of %s: %w", devicePath, err)
}
return uint16(vendorID) == nvpci.PCINvidiaVendorID, nil
}

func getDriver(devicePath string) (string, error) {
Expand Down
Loading