diff --git a/manager/qemu/config.go b/manager/qemu/config.go index 8ce9aca2..96e81469 100644 --- a/manager/qemu/config.go +++ b/manager/qemu/config.go @@ -72,6 +72,14 @@ type IGVMConfig struct { File string `env:"IGVM_FILE" envDefault:"/root/coconut-qemu.igvm"` } +type GPUConfig struct { + EnableGPU bool + GPUBDF string `env:"GPU_BDF" envDefault:""` + PCIeRootPort string `env:"GPU_PCIE_ROOT_PORT" envDefault:"pci.1"` + PCIeBus string `env:"GPU_PCIE_BUS" envDefault:"pcie.0"` + FWCfgPciMmio string `env:"GPU_FW_CFG_MMIO_MB" envDefault:"262144"` +} + type Config struct { EnableSEVSNP bool EnableTDX bool @@ -108,6 +116,9 @@ type Config struct { // vTPM IGVMConfig + // GPU passthrough + GPUConfig + // display NoGraphic bool `env:"NO_GRAPHIC" envDefault:"true"` Monitor string `env:"MONITOR" envDefault:"pty"` @@ -179,6 +190,23 @@ func (config Config) ConstructQemuArgs() []string { config.VirtioNetPciConfig.Addr, config.VirtioNetPciConfig.ROMFile)) + // GPU passthrough via VFIO + if config.GPUConfig.EnableGPU { + args = append(args, "-device", + fmt.Sprintf("pcie-root-port,id=%s,bus=%s", + config.GPUConfig.PCIeRootPort, + config.GPUConfig.PCIeBus)) + + args = append(args, "-device", + fmt.Sprintf("vfio-pci,host=%s,bus=%s", + config.GPUConfig.GPUBDF, + config.GPUConfig.PCIeRootPort)) + + args = append(args, "-fw_cfg", + fmt.Sprintf("name=opt/ovmf/X-PciMmio64Mb,string=%s", + config.GPUConfig.FWCfgPciMmio)) + } + // SEV-SNP if config.EnableSEVSNP { sevSnpType := "sev-snp-guest" @@ -267,5 +295,13 @@ func NewConfig() (*Config, error) { cfg.EnableSEVSNP = SEVSNPEnabledOnHost() cfg.EnableTDX = TDXEnabledOnHost() + bdf, detected := GPUPassthroughAvailable() + if cfg.GPUConfig.GPUBDF != "" { + cfg.GPUConfig.EnableGPU = true + } else if detected { + cfg.GPUConfig.EnableGPU = true + cfg.GPUConfig.GPUBDF = bdf + } + return &cfg, nil } diff --git a/manager/qemu/config_test.go b/manager/qemu/config_test.go index d49d96c8..991dd936 100644 --- a/manager/qemu/config_test.go +++ b/manager/qemu/config_test.go @@ -151,6 +151,79 @@ func TestConstructQemuArgs(t *testing.T) { "-monitor", "pty", }, }, + { + name: "GPU passthrough configuration", + config: Config{ + QemuBinPath: "qemu-system-x86_64", + EnableKVM: true, + Machine: "q35", + CPU: "EPYC", + SMPCount: 4, + MaxCPUs: 64, + MemID: "ram1", + MemoryConfig: MemoryConfig{ + Size: "2048M", + Slots: 5, + Max: "30G", + }, + OVMFCodeConfig: OVMFCodeConfig{ + If: "pflash", + Format: "raw", + Unit: 0, + File: "/usr/share/OVMF/OVMF_CODE.fd", + ReadOnly: "on", + }, + OVMFVarsConfig: OVMFVarsConfig{ + If: "pflash", + Format: "raw", + Unit: 1, + File: "/usr/share/OVMF/OVMF_VARS.fd", + }, + NetDevConfig: NetDevConfig{ + ID: "vmnic", + HostFwdAgent: 7020, + GuestFwdAgent: 7002, + }, + VirtioNetPciConfig: VirtioNetPciConfig{ + DisableLegacy: "on", + IOMMUPlatform: true, + Addr: "0x2", + }, + DiskImgConfig: DiskImgConfig{ + KernelFile: "img/bzImage", + RootFsFile: "img/rootfs.cpio.gz", + }, + GPUConfig: GPUConfig{ + EnableGPU: true, + GPUBDF: "0000:02:00.0", + PCIeRootPort: "pci.1", + PCIeBus: "pcie.0", + FWCfgPciMmio: "262144", + }, + KernelCommandLine: "quiet console=null", + NoGraphic: true, + Monitor: "pty", + }, + expected: []string{ + "-enable-kvm", + "-machine", "q35", + "-cpu", "EPYC", + "-smp", "4,maxcpus=64", + "-m", "2048M,slots=5,maxmem=30G", + "-drive", "if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on", + "-drive", "if=pflash,format=raw,unit=1,file=/usr/share/OVMF/OVMF_VARS.fd", + "-netdev", "user,id=vmnic,hostfwd=tcp::7020-:7002", + "-device", "virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic,addr=0x2,romfile=", + "-device", "pcie-root-port,id=pci.1,bus=pcie.0", + "-device", "vfio-pci,host=0000:02:00.0,bus=pci.1", + "-fw_cfg", "name=opt/ovmf/X-PciMmio64Mb,string=262144", + "-kernel", "img/bzImage", + "-append", "quiet console=null", + "-initrd", "img/rootfs.cpio.gz", + "-nographic", + "-monitor", "pty", + }, + }, } for _, tt := range tests { diff --git a/manager/qemu/vm.go b/manager/qemu/vm.go index bf603a54..2fd9587f 100644 --- a/manager/qemu/vm.go +++ b/manager/qemu/vm.go @@ -231,3 +231,36 @@ func TDXEnabledOnHost() bool { return TDXEnabled(string(cpuinfo), string(kernelParam)) } + +// GPUPassthroughAvailable scans for NVIDIA GPU devices bound to the vfio-pci +// driver and returns the BDF of the first one found. +func GPUPassthroughAvailable() (string, bool) { + const vfioPCIPath = "/sys/bus/pci/drivers/vfio-pci" + entries, err := os.ReadDir(vfioPCIPath) + if err != nil { + return "", false + } + + for _, entry := range entries { + bdf := entry.Name() + if !strings.Contains(bdf, ":") { + continue + } + + vendor, err := os.ReadFile(fmt.Sprintf("/sys/bus/pci/devices/%s/vendor", bdf)) + if err != nil || strings.TrimSpace(string(vendor)) != "0x10de" { + continue + } + + class, err := os.ReadFile(fmt.Sprintf("/sys/bus/pci/devices/%s/class", bdf)) + if err != nil { + continue + } + classStr := strings.TrimSpace(string(class)) + // 0x0302xx = 3D Controller (e.g. H100), 0x0300xx = VGA Compatible Controller + if strings.HasPrefix(classStr, "0x0302") || strings.HasPrefix(classStr, "0x0300") { + return bdf, true + } + } + return "", false +}