Skip to content

Commit 7fc3b49

Browse files
committed
Support vendor VFIO vGPU devices
Linux 6.8 hosts with NVIDIA R580 drop the mdev interface: vGPUs are assigned by writing a type ID to a VF's nvidia/current_vgpu_type and passed to QEMU as a plain VFIO PCI device. Add a vendor VFIO backend behind the existing framework dispatch: profile discovery from the capacity-dependent creatable catalogs, least-loaded VF placement, create/verify/rollback, and release. Because the same VF path is reused across assignments (unlike mdev UUIDs), release is guarded: an in-process owner map covers the window before QEMU opens the device, and an open-VFIO-handle scan refuses to clear a VF a running VM still holds. Reconciliation clears orphaned assignments on startup, skipping VFs protected by the caller and failing closed when the protected set is unavailable. Branch the vGPU integration test by discovered framework and extend it to cover release on stop and reacquisition on start.
1 parent 3c8b92f commit 7fc3b49

13 files changed

Lines changed: 1142 additions & 158 deletions

integration/vgpu_test.go

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"bytes"
55
"context"
66
"os"
7+
"path/filepath"
8+
"strings"
79
"testing"
810
"time"
911

@@ -21,21 +23,7 @@ import (
2123
"github.com/stretchr/testify/require"
2224
)
2325

24-
// TestVGPU is an integration test that verifies vGPU (SR-IOV mdev) support works.
25-
//
26-
// This test automatically detects vGPU availability and skips if:
27-
// - No SR-IOV VFs are found in /sys/class/mdev_bus/
28-
// - No vGPU profiles are available
29-
// - Not running as root (required for mdev creation)
30-
// - KVM is not available
31-
//
32-
// To run manually:
33-
//
34-
// sudo go test -v -run TestVGPU -timeout 5m ./integration/...
35-
//
36-
// Note: This test verifies mdev creation and PCI device visibility inside the VM.
37-
// It does NOT test nvidia-smi or CUDA functionality since that requires NVIDIA
38-
// guest drivers pre-installed in the image.
26+
// TestVGPU verifies vGPU support through mdev or vendor VFIO.
3927
func TestVGPU(t *testing.T) {
4028
t.Parallel()
4129
if testing.Short() {
@@ -159,9 +147,17 @@ func TestVGPU(t *testing.T) {
159147
instanceID = inst.Id
160148
t.Logf("Instance created: %s", inst.Id)
161149

162-
// Verify mdev UUID was assigned
163-
require.NotEmpty(t, inst.GPUMdevUUID, "Instance should have mdev UUID assigned")
164-
t.Logf("mdev UUID: %s", inst.GPUMdevUUID)
150+
require.NotEmpty(t, inst.GPUDevicePath, "Instance should have a vGPU device path assigned")
151+
switch inst.GPUFramework {
152+
case devices.VGPUFrameworkMdev:
153+
require.NotEmpty(t, inst.GPUMdevUUID, "mdev instance should have a UUID assigned")
154+
t.Logf("mdev UUID: %s", inst.GPUMdevUUID)
155+
case devices.VGPUFrameworkVendorVFIO:
156+
require.Empty(t, inst.GPUMdevUUID, "vendor VFIO instance should not have an mdev UUID")
157+
t.Logf("vendor VFIO VF: %s", inst.GPUDevicePath)
158+
default:
159+
t.Fatalf("unexpected vGPU framework %q", inst.GPUFramework)
160+
}
165161

166162
// Step 5: Check GPU resources AFTER creating instance
167163
t.Run("ResourcesDecrementedAfterCreation", func(t *testing.T) {
@@ -180,12 +176,8 @@ func TestVGPU(t *testing.T) {
180176
assert.Less(t, availableAfter, availableBefore, "available instances should decrease after creating VM")
181177
})
182178

183-
// Step 6: Verify mdev was created in sysfs
184-
t.Run("MdevCreated", func(t *testing.T) {
185-
mdevPath := "/sys/bus/mdev/devices/" + inst.GPUMdevUUID
186-
_, err := os.Stat(mdevPath)
187-
assert.NoError(t, err, "mdev device should exist at %s", mdevPath)
188-
t.Logf("mdev exists at: %s", mdevPath)
179+
t.Run("VGPUAssignedInSysfs", func(t *testing.T) {
180+
assertVGPUAssigned(t, inst.GPUFramework, inst.GPUDevicePath)
189181
})
190182

191183
// Step 7: Wait for guest agent to be ready
@@ -225,13 +217,68 @@ func TestVGPU(t *testing.T) {
225217
require.NoError(t, err)
226218

227219
assert.Equal(t, profile, actualInst.GPUProfile, "GPU profile should match")
228-
assert.NotEmpty(t, actualInst.GPUMdevUUID, "mdev UUID should be set")
229-
t.Logf("Instance GPU: profile=%s, mdev=%s", actualInst.GPUProfile, actualInst.GPUMdevUUID)
220+
assert.Equal(t, inst.GPUFramework, actualInst.GPUFramework, "framework should match")
221+
assert.NotEmpty(t, actualInst.GPUDevicePath, "device path should be set")
222+
if inst.GPUFramework == devices.VGPUFrameworkMdev {
223+
assert.NotEmpty(t, actualInst.GPUMdevUUID, "mdev UUID should be set")
224+
}
225+
t.Logf("Instance GPU: profile=%s, framework=%s, device=%s", actualInst.GPUProfile, actualInst.GPUFramework, actualInst.GPUDevicePath)
226+
})
227+
228+
t.Log("Step 10: Stopping instance to release the vGPU...")
229+
_, err = instanceManager.StopInstance(ctx, inst.Id)
230+
require.NoError(t, err, "stop should succeed")
231+
232+
t.Run("VGPUReleasedOnStop", func(t *testing.T) {
233+
stopped, err := instanceManager.GetInstance(ctx, inst.Id)
234+
require.NoError(t, err)
235+
assert.Empty(t, stopped.GPUDevicePath, "assignment metadata should be cleared on stop")
236+
assertVGPUReleased(t, inst.GPUFramework, inst.GPUDevicePath)
237+
})
238+
239+
t.Log("Step 11: Starting instance to reacquire a vGPU...")
240+
started, err := instanceManager.StartInstance(ctx, inst.Id, instances.StartInstanceRequest{})
241+
require.NoError(t, err, "start should succeed")
242+
243+
t.Run("VGPUReacquiredOnStart", func(t *testing.T) {
244+
require.NotEmpty(t, started.GPUDevicePath, "start should assign a vGPU")
245+
assert.Equal(t, inst.GPUFramework, started.GPUFramework, "framework should match")
246+
assertVGPUAssigned(t, started.GPUFramework, started.GPUDevicePath)
230247
})
231248

232249
t.Log("✅ vGPU test PASSED!")
233250
}
234251

252+
func assertVGPUAssigned(t *testing.T, framework devices.VGPUFramework, devicePath string) {
253+
t.Helper()
254+
switch framework {
255+
case devices.VGPUFrameworkMdev:
256+
_, err := os.Stat(devicePath)
257+
assert.NoError(t, err, "mdev device should exist at %s", devicePath)
258+
case devices.VGPUFrameworkVendorVFIO:
259+
data, err := os.ReadFile(filepath.Join(devicePath, "nvidia", "current_vgpu_type"))
260+
require.NoError(t, err, "VF should expose current_vgpu_type")
261+
assert.NotEqual(t, "0", strings.TrimSpace(string(data)), "VF should have a vGPU type assigned")
262+
default:
263+
t.Fatalf("unexpected vGPU framework %q", framework)
264+
}
265+
}
266+
267+
func assertVGPUReleased(t *testing.T, framework devices.VGPUFramework, devicePath string) {
268+
t.Helper()
269+
switch framework {
270+
case devices.VGPUFrameworkMdev:
271+
_, err := os.Stat(devicePath)
272+
assert.True(t, os.IsNotExist(err), "mdev device should be gone from %s", devicePath)
273+
case devices.VGPUFrameworkVendorVFIO:
274+
data, err := os.ReadFile(filepath.Join(devicePath, "nvidia", "current_vgpu_type"))
275+
require.NoError(t, err, "VF should expose current_vgpu_type")
276+
assert.Equal(t, "0", strings.TrimSpace(string(data)), "VF assignment should be released")
277+
default:
278+
t.Fatalf("unexpected vGPU framework %q", framework)
279+
}
280+
}
281+
235282
// checkVGPUTestPrerequisites checks if vGPU test can run.
236283
// Returns (skipReason, profileName) - skipReason is empty if all prerequisites are met.
237284
func checkVGPUTestPrerequisites() (string, string) {
@@ -245,10 +292,12 @@ func checkVGPUTestPrerequisites() (string, string) {
245292
return "vGPU test requires root (sudo) for mdev creation", ""
246293
}
247294

248-
// Check for vGPU mode (SR-IOV VFs present)
249-
mode := devices.DetectHostGPUMode()
250-
if mode != devices.GPUModeVGPU {
251-
return "vGPU test requires SR-IOV VFs in /sys/class/mdev_bus/", ""
295+
framework, _, err := devices.DiscoverVGPU()
296+
if err != nil {
297+
return "vGPU test failed to discover vGPU framework: " + err.Error(), ""
298+
}
299+
if framework == devices.VGPUFrameworkNone {
300+
return "vGPU test requires SR-IOV VFs with an mdev or vendor VFIO vGPU framework", ""
252301
}
253302

254303
// Check for available profiles

lib/devices/GPU.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ hypeman supports two GPU modes, automatically detected based on host configurati
88

99
| Mode | Description | Use Case |
1010
|------|-------------|----------|
11-
| **vGPU (SR-IOV)** | Virtual GPUs via mdev on SR-IOV VFs | Multi-tenant, shared GPU resources |
11+
| **vGPU (SR-IOV)** | Virtual GPUs on SR-IOV VFs via mdev or vendor VFIO | Multi-tenant, shared GPU resources |
1212
| **Passthrough** | Whole GPU VFIO passthrough | Dedicated GPU per instance |
1313

1414
The host's GPU mode is determined by the host driver configuration:
15-
- If `/sys/class/mdev_bus/` contains VFs → vGPU mode
16-
- If NVIDIA GPUs are available for VFIO → passthrough mode
15+
- If `/sys/class/mdev_bus/` contains VFs → mdev vGPU mode
16+
- If VFs expose `/sys/bus/pci/devices/<VF>/nvidia/current_vgpu_type` → vendor VFIO vGPU mode
17+
- If NVIDIA GPUs are available for whole-device VFIO → passthrough mode
1718

1819
## vGPU Mode (Recommended)
1920

20-
vGPU mode uses NVIDIA's SR-IOV technology to create Virtual Functions (VFs), each capable of hosting an mdev (mediated device) representing a vGPU.
21+
vGPU mode uses NVIDIA's SR-IOV technology to create Virtual Functions (VFs). Hosts on older kernels represent each vGPU as an mdev. Hosts using NVIDIA's vendor VFIO framework assign the profile directly to the VF through `current_vgpu_type`.
2122

2223
### How It Works
2324

@@ -74,7 +75,7 @@ curl -X POST http://localhost:4973/instances \
7475
}'
7576
```
7677

77-
The response includes the assigned mdev UUID:
78+
On an mdev host, the response also includes the assigned mdev UUID:
7879

7980
```json
8081
{
@@ -87,19 +88,16 @@ The response includes the assigned mdev UUID:
8788
}
8889
```
8990

90-
### Ephemeral mdev Lifecycle
91+
### Ephemeral vGPU Lifecycle
9192

92-
mdev devices are **ephemeral**: created on instance start, destroyed on instance delete.
93+
vGPU assignments are created on instance start and released on stop or delete. Hypeman creates/removes an mdev on mdev hosts and writes the profile ID/`0` to `current_vgpu_type` on vendor VFIO hosts.
9394

9495
```
95-
Instance Create → Create mdev → Attach to VM → Instance Running
96-
Instance Delete → Stop VM → Destroy mdev → VF available again
96+
Instance Create → Assign profile to VF → Attach VF to VM → Instance Running
97+
Instance Stop/Delete → Release profile → VF available again
9798
```
9899

99-
This ensures:
100-
- **Security**: No VRAM data leakage between instances
101-
- **Clean state**: Fresh vGPU for each instance
102-
- **Automatic cleanup**: Orphaned mdevs cleaned up on server restart
100+
Hypeman reconciles orphaned assignments on server restart while preserving devices held open by a running VMM.
103101

104102
## Passthrough Mode
105103

@@ -256,7 +254,8 @@ If assignment cleanup fails, Hypeman retains the instance metadata so a compatib
256254

257255
1. Check host GPU mode detection:
258256
```bash
259-
ls /sys/class/mdev_bus/ # Should show VFs for vGPU mode
257+
ls /sys/class/mdev_bus/
258+
find /sys/bus/pci/devices -path '*/nvidia/current_vgpu_type'
260259
```
261260

262261
2. Verify NVIDIA drivers are loaded on host:
@@ -280,17 +279,18 @@ curl -s http://localhost:4973/resources | jq '.gpu.profiles'
280279
curl http://localhost:4973/instances/<id>/logs?source=app
281280
```
282281

283-
### mdev creation fails
282+
### vGPU assignment fails
284283

285-
1. Check if VFs are available:
286-
```bash
287-
ls /sys/class/mdev_bus/
288-
```
284+
Check the files for the framework detected on the host:
289285

290-
2. Verify mdev types:
291-
```bash
292-
cat /sys/class/mdev_bus/*/mdev_supported_types/*/available_instances
293-
```
286+
```bash
287+
# mdev
288+
cat /sys/class/mdev_bus/*/mdev_supported_types/*/available_instances
289+
290+
# vendor VFIO
291+
cat /sys/bus/pci/devices/*/nvidia/creatable_vgpu_types
292+
cat /sys/bus/pci/devices/*/nvidia/current_vgpu_type
293+
```
294294

295295
## Performance Tuning
296296

lib/devices/gpu_mode.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

lib/devices/mdev_darwin.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ func SetGPUProfileCacheTTL(ttl string) {
99
// No-op on macOS
1010
}
1111

12-
// DiscoverVFs returns an empty list on macOS.
13-
// SR-IOV Virtual Functions are not available on macOS.
14-
func DiscoverVFs() ([]VirtualFunction, error) {
15-
return []VirtualFunction{}, nil
12+
// DiscoverVGPU reports no vGPU framework on macOS.
13+
func DiscoverVGPU() (VGPUFramework, []VirtualFunction, error) {
14+
return VGPUFrameworkNone, nil, nil
1615
}
1716

1817
// ListGPUProfiles returns an empty list on macOS.
@@ -21,7 +20,7 @@ func ListGPUProfiles() ([]GPUProfile, error) {
2120
}
2221

2322
// ListGPUProfilesWithVFs returns an empty list on macOS.
24-
func ListGPUProfilesWithVFs(vfs []VirtualFunction) ([]GPUProfile, error) {
23+
func ListGPUProfilesWithVFs(framework VGPUFramework, vfs []VirtualFunction) ([]GPUProfile, error) {
2524
return []GPUProfile{}, nil
2625
}
2726

@@ -56,6 +55,10 @@ func DestroyVGPU(ctx context.Context, assignment VGPUAssignment) error {
5655
return nil
5756
}
5857

58+
func ReconcileVGPUs(ctx context.Context, protectedDevicePaths map[string]struct{}) error {
59+
return nil
60+
}
61+
5962
// ReconcileMdevs is a no-op on macOS.
6063
func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) error {
6164
return nil

lib/devices/mdev_linux.go

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,11 @@ func getCachedProfiles(firstVF string) []profileMetadata {
8989
return cachedProfiles
9090
}
9191

92-
// DiscoverVFs returns all SR-IOV Virtual Functions available for vGPU.
93-
// These are discovered by scanning /sys/class/mdev_bus/ which contains
94-
// VFs that can host mdev devices.
95-
func DiscoverVFs() ([]VirtualFunction, error) {
92+
func discoverMdevVFs() ([]VirtualFunction, error) {
9693
entries, err := os.ReadDir(mdevBusPath)
9794
if err != nil {
9895
if os.IsNotExist(err) {
99-
return nil, nil // No mdev_bus means no vGPU support
96+
return nil, nil // No mdev_bus means no mdev vGPU support
10097
}
10198
return nil, fmt.Errorf("read mdev_bus: %w", err)
10299
}
@@ -133,20 +130,7 @@ func DiscoverVFs() ([]VirtualFunction, error) {
133130
return vfs, nil
134131
}
135132

136-
// ListGPUProfiles returns available vGPU profiles with availability counts.
137-
// Profiles are discovered from the first VF's mdev_supported_types directory.
138-
func ListGPUProfiles() ([]GPUProfile, error) {
139-
vfs, err := DiscoverVFs()
140-
if err != nil {
141-
return nil, err
142-
}
143-
return ListGPUProfilesWithVFs(vfs)
144-
}
145-
146-
// ListGPUProfilesWithVFs returns available vGPU profiles using pre-discovered VFs.
147-
// This avoids redundant VF discovery when the caller already has the list.
148-
// Uses parallel sysfs reads for fast availability counting.
149-
func ListGPUProfilesWithVFs(vfs []VirtualFunction) ([]GPUProfile, error) {
133+
func listMdevGPUProfilesWithVFs(vfs []VirtualFunction) ([]GPUProfile, error) {
150134
if len(vfs) == 0 {
151135
return nil, nil
152136
}
@@ -305,7 +289,7 @@ func countAvailableForSingleProfile(freeVFsByParent map[string][]VirtualFunction
305289

306290
// findProfileType finds the internal type name (e.g., "nvidia-556") for a profile name (e.g., "L40S-1Q")
307291
func findProfileType(profileName string) (string, error) {
308-
vfs, err := DiscoverVFs()
292+
vfs, err := discoverMdevVFs()
309293
if err != nil || len(vfs) == 0 {
310294
return "", fmt.Errorf("no VFs available")
311295
}
@@ -531,7 +515,7 @@ func CreateMdev(ctx context.Context, profileName, instanceID string) (*MdevDevic
531515
}
532516

533517
// Discover all VFs
534-
vfs, err := DiscoverVFs()
518+
vfs, err := discoverMdevVFs()
535519
if err != nil {
536520
return nil, fmt.Errorf("discover VFs: %w", err)
537521
}
@@ -697,7 +681,7 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
697681
log := logger.FromContext(ctx)
698682
_ = instanceInfos
699683

700-
vfs, err := DiscoverVFs()
684+
vfs, err := discoverMdevVFs()
701685
if err != nil {
702686
return fmt.Errorf("discover managed VFs: %w", err)
703687
}

0 commit comments

Comments
 (0)