Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dff3ab0
Generalize vGPU device lifecycle
yummybomb Jul 28, 2026
fa63399
Harden vGPU lifecycle compatibility
yummybomb Jul 29, 2026
44fce57
Validate unsupported vGPU frameworks on Darwin
yummybomb Jul 29, 2026
a378a7b
Reduce vGPU refactor diff noise
yummybomb Jul 29, 2026
9fd861f
Leave vGPU hypervisor selection to callers
yummybomb Jul 29, 2026
50495ee
Require qemu for vGPU instances
yummybomb Jul 29, 2026
709e270
Preserve vGPU hypervisor support
yummybomb Jul 29, 2026
f33f0bc
Track VF allocation with a single field
yummybomb Aug 4, 2026
66b64cf
Update vGPU lifecycle comments
yummybomb Aug 5, 2026
271cec8
Drop the dead mdev branch from QEMU PCI passthrough args
yummybomb Aug 5, 2026
e7c5adc
Fix Darwin vGPU build
yummybomb Aug 6, 2026
b32872f
Preserve QEMU vGPU device ordering
yummybomb Aug 6, 2026
5251369
Preserve mdev-era lifecycle semantics in the refactor
yummybomb Aug 6, 2026
a5349a9
Keep the mdev create error text in the refactor
yummybomb Aug 7, 2026
d461260
Restore mdev start error text
yummybomb Aug 9, 2026
1aa9d91
Restrict vGPU instances to QEMU
yummybomb Aug 10, 2026
bc6d415
Preserve mdev hypervisor behavior
yummybomb Aug 10, 2026
f9060a3
Restore vGPU lifecycle log statements and spacing from the pre-refact…
yummybomb Aug 10, 2026
40e61b2
Reject vGPU device path in microvm config validation
yummybomb Aug 13, 2026
113d9e6
Pass DestroyVGPU parameters as a VGPUAssignment struct
yummybomb Aug 14, 2026
9396bc0
Log vGPU device path instead of mdev UUID on stop and delete
yummybomb Aug 14, 2026
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
16 changes: 15 additions & 1 deletion lib/devices/mdev_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

package devices

import "context"
import (
"context"
"fmt"
)

// SetGPUProfileCacheTTL is a no-op on macOS.
func SetGPUProfileCacheTTL(ttl string) {
Expand Down Expand Up @@ -30,6 +33,10 @@ func ListMdevDevices() ([]MdevDevice, error) {
return []MdevDevice{}, nil
}

func CreateVGPU(ctx context.Context, profileName, instanceID string) (*VGPUDevice, error) {
return nil, ErrVGPUNotSupportedOnMacOS
}

// CreateMdev returns an error on macOS as mdev is not supported.
func CreateMdev(ctx context.Context, profileName, instanceID string) (*MdevDevice, error) {
return nil, ErrVGPUNotSupportedOnMacOS
Expand All @@ -45,6 +52,13 @@ func IsMdevInUse(mdevUUID string) bool {
return false
}

func DestroyVGPU(ctx context.Context, assignment VGPUAssignment) error {
if assignment.Framework != VGPUFrameworkNone && assignment.Framework != VGPUFrameworkMdev {
return fmt.Errorf("unknown vGPU framework %q", assignment.Framework)
}
return nil
}

// ReconcileMdevs is a no-op on macOS.
func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) error {
return nil
Expand Down
6 changes: 3 additions & 3 deletions lib/devices/mdev_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func DiscoverVFs() ([]VirtualFunction, error) {
vfs = append(vfs, VirtualFunction{
PCIAddress: vfAddr,
ParentGPU: parentGPU,
HasMdev: hasMdev,
Allocated: hasMdev,
})
}

Expand Down Expand Up @@ -253,7 +253,7 @@ func countAvailableVFsForProfilesParallel(vfs []VirtualFunction, profiles []prof
// Group free VFs by parent GPU (done once, shared by all goroutines)
freeVFsByParent := make(map[string][]VirtualFunction)
for _, vf := range vfs {
if vf.HasMdev {
if vf.Allocated {
continue
}
freeVFsByParent[vf.ParentGPU] = append(freeVFsByParent[vf.ParentGPU], vf)
Expand Down Expand Up @@ -453,7 +453,7 @@ func selectLeastLoadedVF(ctx context.Context, vfs []VirtualFunction, profileType
allGPUs := make(map[string]bool)
for _, vf := range vfs {
allGPUs[vf.ParentGPU] = true
if !vf.HasMdev {
if !vf.Allocated {
freeVFsByGPU[vf.ParentGPU] = append(freeVFsByGPU[vf.ParentGPU], vf)
}
}
Expand Down
23 changes: 22 additions & 1 deletion lib/devices/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ func ValidateDeviceName(name string) bool {
// GPUMode represents the host's GPU configuration mode
type GPUMode string

type VGPUFramework string

const (
VGPUFrameworkNone VGPUFramework = ""
VGPUFrameworkMdev VGPUFramework = "mdev"

// GPUModePassthrough indicates whole GPU VFIO passthrough
GPUModePassthrough GPUMode = "passthrough"
// GPUModeVGPU indicates SR-IOV + mdev based vGPU
Expand All @@ -73,7 +78,23 @@ const (
type VirtualFunction struct {
PCIAddress string `json:"pci_address"` // e.g., "0000:82:00.4"
ParentGPU string `json:"parent_gpu"` // e.g., "0000:82:00.0"
HasMdev bool `json:"has_mdev"` // true if an mdev is created on this VF
Allocated bool `json:"allocated"` // true if a vGPU is assigned to this VF
}

// VGPUAssignment identifies an existing vGPU assignment to release.
type VGPUAssignment struct {
Framework VGPUFramework
DevicePath string
MdevUUID string
}

type VGPUDevice struct {
Framework VGPUFramework
VFAddress string
ProfileType string
ProfileName string
SysfsPath string
MdevUUID string
}

// MdevDevice represents an active mediated device (vGPU instance)
Expand Down
38 changes: 38 additions & 0 deletions lib/devices/vgpu_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build linux

package devices

import (
"context"
"fmt"
"path/filepath"
)

func CreateVGPU(ctx context.Context, profileName, instanceID string) (*VGPUDevice, error) {
mdev, err := CreateMdev(ctx, profileName, instanceID)
if err != nil {
return nil, err
}
return &VGPUDevice{
Framework: VGPUFrameworkMdev,
VFAddress: mdev.VFAddress,
ProfileType: mdev.ProfileType,
ProfileName: mdev.ProfileName,
SysfsPath: mdev.SysfsPath,
MdevUUID: mdev.UUID,
}, nil
}

func DestroyVGPU(ctx context.Context, assignment VGPUAssignment) error {
if assignment.Framework != VGPUFrameworkNone && assignment.Framework != VGPUFrameworkMdev {
return fmt.Errorf("unknown vGPU framework %q", assignment.Framework)
}
mdevUUID := assignment.MdevUUID
if mdevUUID == "" {
if assignment.DevicePath == "" {
return nil
}
mdevUUID = filepath.Base(assignment.DevicePath)
}
return DestroyMdev(ctx, mdevUUID)
}
11 changes: 9 additions & 2 deletions lib/hypervisor/cloudhypervisor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,20 @@ func ToVMConfig(cfg hypervisor.VMConfig) vmm.VmConfig {

// Device passthrough configuration
var devices *[]vmm.DeviceConfig
if len(cfg.PCIDevices) > 0 {
deviceConfigs := make([]vmm.DeviceConfig, 0, len(cfg.PCIDevices))
deviceCount := len(cfg.PCIDevices)
if cfg.VGPUDevicePath != "" {
deviceCount++
}
if deviceCount > 0 {
deviceConfigs := make([]vmm.DeviceConfig, 0, deviceCount)
for _, path := range cfg.PCIDevices {
deviceConfigs = append(deviceConfigs, vmm.DeviceConfig{
Path: path,
})
}
if cfg.VGPUDevicePath != "" {
deviceConfigs = append(deviceConfigs, vmm.DeviceConfig{Path: cfg.VGPUDevicePath})
}
devices = &deviceConfigs
}

Expand Down
10 changes: 10 additions & 0 deletions lib/hypervisor/cloudhypervisor/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import (
"github.com/stretchr/testify/require"
)

func TestToVMConfigIncludesVGPUDevice(t *testing.T) {
path := "/sys/bus/mdev/devices/aa618089-8b16-4d01-a136-25a0f3c73123"

vmCfg := ToVMConfig(hypervisor.VMConfig{VGPUDevicePath: path})

require.NotNil(t, vmCfg.Devices)
require.Len(t, *vmCfg.Devices, 1)
assert.Equal(t, path, (*vmCfg.Devices)[0].Path)
}

func TestToVMConfig_GuestMemoryBalloon(t *testing.T) {
cfg := hypervisor.VMConfig{
VCPUs: 1,
Expand Down
3 changes: 2 additions & 1 deletion lib/hypervisor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ type VMConfig struct {
VsockSocket string

// PCI device passthrough (GPU, etc.)
PCIDevices []string
PCIDevices []string
VGPUDevicePath string

// Boot configuration
KernelPath string
Expand Down
11 changes: 6 additions & 5 deletions lib/hypervisor/qemu/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,10 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string {
args = append(args, "-device", fmt.Sprintf("%s,guest-cid=%d", virtioDevice(microvm, "vhost-vsock"), cfg.VsockCID))
}

// PCI device passthrough (GPU, mdev vGPU, etc.)
// Whole-device PCI passthrough (vGPU attaches via VGPUDevicePath below)
for _, devicePath := range cfg.PCIDevices {
var deviceArg string
if strings.HasPrefix(devicePath, "/sys/bus/mdev/devices/") {
// mdev device (vGPU) - use sysfsdev parameter
deviceArg = fmt.Sprintf("vfio-pci,sysfsdev=%s", devicePath)
} else if strings.HasPrefix(devicePath, "/sys/bus/pci/devices/") {
if strings.HasPrefix(devicePath, "/sys/bus/pci/devices/") {
// Full sysfs path for regular PCI device - extract the PCI address
// Using filepath.Base is more robust than manual string splitting
pciAddr := filepath.Base(strings.TrimSuffix(devicePath, "/"))
Expand All @@ -112,6 +109,10 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string {
args = append(args, "-device", deviceArg)
}

if cfg.VGPUDevicePath != "" {
args = append(args, "-device", fmt.Sprintf("vfio-pci,sysfsdev=%s", cfg.VGPUDevicePath))
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Serial console output to file. Use a chardev with append=on so QEMU
// opens the file with O_APPEND. Without it, QEMU writes at its internal
// fd offset; if the file is externally truncated (e.g. log rotation via
Expand Down
43 changes: 43 additions & 0 deletions lib/hypervisor/qemu/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,49 @@ func TestBuildArgs_Vsock(t *testing.T) {
assert.Contains(t, args, "vhost-vsock-pci,guest-cid=123")
}

func TestBuildArgs_VGPU(t *testing.T) {
t.Parallel()

for _, path := range []string{
"/sys/bus/mdev/devices/aa618089-8b16-4d01-a136-25a0f3c73123",
"/sys/bus/pci/devices/0000:82:00.4",
} {
path := path
t.Run(path, func(t *testing.T) {
t.Parallel()
args := BuildArgs(hypervisor.VMConfig{
VCPUs: 1,
MemoryBytes: 512 * 1024 * 1024,
VGPUDevicePath: path,
})
assert.Contains(t, args, "vfio-pci,sysfsdev="+path)
})
}
}

func TestBuildArgs_VGPUAfterPCIDevices(t *testing.T) {
args := BuildArgs(hypervisor.VMConfig{
VCPUs: 1,
MemoryBytes: 512 * 1024 * 1024,
PCIDevices: []string{"0000:01:00.0"},
VGPUDevicePath: "/sys/bus/mdev/devices/aa618089-8b16-4d01-a136-25a0f3c73123",
})

pciDeviceIndex := -1
vgpuDeviceIndex := -1
for i, arg := range args {
switch arg {
case "vfio-pci,host=0000:01:00.0":
pciDeviceIndex = i
case "vfio-pci,sysfsdev=/sys/bus/mdev/devices/aa618089-8b16-4d01-a136-25a0f3c73123":
vgpuDeviceIndex = i
}
}

assert.Greater(t, pciDeviceIndex, -1)
assert.Greater(t, vgpuDeviceIndex, pciDeviceIndex)
}

func TestBuildArgs_PCIPassthrough(t *testing.T) {
cfg := hypervisor.VMConfig{
VCPUs: 1,
Expand Down
13 changes: 13 additions & 0 deletions lib/hypervisor/qemu/machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ func TestMicroVMCapabilitiesExcludePCIPassthrough(t *testing.T) {
assert.True(t, (MicroVMProfile{}).capabilities().RequiresHostSnapshotVersion)
}

func TestMicroVMValidateConfigRejectsVFIODevices(t *testing.T) {
t.Parallel()
err := (MicroVMProfile{}).validateConfig(hypervisor.VMConfig{
PCIDevices: []string{"0000:82:00.4"},
})
require.ErrorContains(t, err, "microvm does not support PCI devices")

err = (MicroVMProfile{}).validateConfig(hypervisor.VMConfig{
VGPUDevicePath: "/sys/bus/mdev/devices/aa618089-8b16-4d01-a136-25a0f3c73123",
})
require.ErrorContains(t, err, "microvm does not support PCI devices")
}

func TestValidateConfigMicroVM(t *testing.T) {
t.Parallel()
if _, err := microVMMachineType(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion lib/hypervisor/qemu/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (MicroVMProfile) validateConfig(cfg hypervisor.VMConfig) error {
if cfg.HotplugBytes > 0 {
return fmt.Errorf("microvm does not support hotplug memory")
}
if len(cfg.PCIDevices) > 0 {
if len(cfg.PCIDevices) > 0 || cfg.VGPUDevicePath != "" {
return fmt.Errorf("microvm does not support PCI devices")
}

Expand Down
Loading
Loading