Skip to content

Commit f661e63

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 915f0ef commit f661e63

13 files changed

Lines changed: 1159 additions & 148 deletions

integration/vgpu_test.go

Lines changed: 89 additions & 21 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,23 @@ import (
2123
"github.com/stretchr/testify/require"
2224
)
2325

24-
// TestVGPU is an integration test that verifies vGPU (SR-IOV mdev) support works.
26+
// TestVGPU is an integration test that verifies vGPU (SR-IOV) support works
27+
// on the host's framework: mdev or NVIDIA's vendor-specific VFIO.
2528
//
2629
// This test automatically detects vGPU availability and skips if:
27-
// - No SR-IOV VFs are found in /sys/class/mdev_bus/
30+
// - No vGPU framework (mdev or vendor VFIO) is discovered
2831
// - No vGPU profiles are available
29-
// - Not running as root (required for mdev creation)
32+
// - Not running as root (required for sysfs vGPU assignment)
3033
// - KVM is not available
3134
//
3235
// To run manually:
3336
//
3437
// sudo go test -v -run TestVGPU -timeout 5m ./integration/...
3538
//
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.
39+
// Note: This test verifies vGPU assignment, release on stop, reacquisition on
40+
// start, and PCI device visibility inside the VM. It does NOT test nvidia-smi
41+
// or CUDA functionality since that requires NVIDIA guest drivers pre-installed
42+
// in the image.
3943
func TestVGPU(t *testing.T) {
4044
t.Parallel()
4145
if testing.Short() {
@@ -159,9 +163,18 @@ func TestVGPU(t *testing.T) {
159163
instanceID = inst.Id
160164
t.Logf("Instance created: %s", inst.Id)
161165

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)
166+
// Verify the assignment matches the host's framework
167+
require.NotEmpty(t, inst.GPUDevicePath, "Instance should have a vGPU device path assigned")
168+
switch inst.GPUFramework {
169+
case devices.VGPUFrameworkMdev:
170+
require.NotEmpty(t, inst.GPUMdevUUID, "mdev instance should have a UUID assigned")
171+
t.Logf("mdev UUID: %s", inst.GPUMdevUUID)
172+
case devices.VGPUFrameworkVendorVFIO:
173+
require.Empty(t, inst.GPUMdevUUID, "vendor VFIO instance should not have an mdev UUID")
174+
t.Logf("vendor VFIO VF: %s", inst.GPUDevicePath)
175+
default:
176+
t.Fatalf("unexpected vGPU framework %q", inst.GPUFramework)
177+
}
165178

166179
// Step 5: Check GPU resources AFTER creating instance
167180
t.Run("ResourcesDecrementedAfterCreation", func(t *testing.T) {
@@ -180,12 +193,9 @@ func TestVGPU(t *testing.T) {
180193
assert.Less(t, availableAfter, availableBefore, "available instances should decrease after creating VM")
181194
})
182195

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)
196+
// Step 6: Verify the assignment exists in sysfs
197+
t.Run("VGPUAssignedInSysfs", func(t *testing.T) {
198+
assertVGPUAssigned(t, inst.GPUFramework, inst.GPUDevicePath)
189199
})
190200

191201
// Step 7: Wait for guest agent to be ready
@@ -225,13 +235,68 @@ func TestVGPU(t *testing.T) {
225235
require.NoError(t, err)
226236

227237
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)
238+
assert.Equal(t, inst.GPUFramework, actualInst.GPUFramework, "framework should match")
239+
assert.NotEmpty(t, actualInst.GPUDevicePath, "device path should be set")
240+
if inst.GPUFramework == devices.VGPUFrameworkMdev {
241+
assert.NotEmpty(t, actualInst.GPUMdevUUID, "mdev UUID should be set")
242+
}
243+
t.Logf("Instance GPU: profile=%s, framework=%s, device=%s", actualInst.GPUProfile, actualInst.GPUFramework, actualInst.GPUDevicePath)
244+
})
245+
246+
t.Log("Step 10: Stopping instance to release the vGPU...")
247+
_, err = instanceManager.StopInstance(ctx, inst.Id)
248+
require.NoError(t, err, "stop should succeed")
249+
250+
t.Run("VGPUReleasedOnStop", func(t *testing.T) {
251+
stopped, err := instanceManager.GetInstance(ctx, inst.Id)
252+
require.NoError(t, err)
253+
assert.Empty(t, stopped.GPUDevicePath, "assignment metadata should be cleared on stop")
254+
assertVGPUReleased(t, inst.GPUFramework, inst.GPUDevicePath)
255+
})
256+
257+
t.Log("Step 11: Starting instance to reacquire a vGPU...")
258+
started, err := instanceManager.StartInstance(ctx, inst.Id, instances.StartInstanceRequest{})
259+
require.NoError(t, err, "start should succeed")
260+
261+
t.Run("VGPUReacquiredOnStart", func(t *testing.T) {
262+
require.NotEmpty(t, started.GPUDevicePath, "start should assign a vGPU")
263+
assert.Equal(t, inst.GPUFramework, started.GPUFramework, "framework should match")
264+
assertVGPUAssigned(t, started.GPUFramework, started.GPUDevicePath)
230265
})
231266

232267
t.Log("✅ vGPU test PASSED!")
233268
}
234269

270+
func assertVGPUAssigned(t *testing.T, framework devices.VGPUFramework, devicePath string) {
271+
t.Helper()
272+
switch framework {
273+
case devices.VGPUFrameworkMdev:
274+
_, err := os.Stat(devicePath)
275+
assert.NoError(t, err, "mdev device should exist at %s", devicePath)
276+
case devices.VGPUFrameworkVendorVFIO:
277+
data, err := os.ReadFile(filepath.Join(devicePath, "nvidia", "current_vgpu_type"))
278+
require.NoError(t, err, "VF should expose current_vgpu_type")
279+
assert.NotEqual(t, "0", strings.TrimSpace(string(data)), "VF should have a vGPU type assigned")
280+
default:
281+
t.Fatalf("unexpected vGPU framework %q", framework)
282+
}
283+
}
284+
285+
func assertVGPUReleased(t *testing.T, framework devices.VGPUFramework, devicePath string) {
286+
t.Helper()
287+
switch framework {
288+
case devices.VGPUFrameworkMdev:
289+
_, err := os.Stat(devicePath)
290+
assert.True(t, os.IsNotExist(err), "mdev device should be gone from %s", devicePath)
291+
case devices.VGPUFrameworkVendorVFIO:
292+
data, err := os.ReadFile(filepath.Join(devicePath, "nvidia", "current_vgpu_type"))
293+
require.NoError(t, err, "VF should expose current_vgpu_type")
294+
assert.Equal(t, "0", strings.TrimSpace(string(data)), "VF assignment should be released")
295+
default:
296+
t.Fatalf("unexpected vGPU framework %q", framework)
297+
}
298+
}
299+
235300
// checkVGPUTestPrerequisites checks if vGPU test can run.
236301
// Returns (skipReason, profileName) - skipReason is empty if all prerequisites are met.
237302
func checkVGPUTestPrerequisites() (string, string) {
@@ -245,10 +310,13 @@ func checkVGPUTestPrerequisites() (string, string) {
245310
return "vGPU test requires root (sudo) for mdev creation", ""
246311
}
247312

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/", ""
313+
// Check for a vGPU framework (mdev or vendor VFIO)
314+
framework, _, err := devices.DiscoverVGPU()
315+
if err != nil {
316+
return "vGPU test failed to discover vGPU framework: " + err.Error(), ""
317+
}
318+
if framework == devices.VGPUFrameworkNone {
319+
return "vGPU test requires SR-IOV VFs with an mdev or vendor VFIO vGPU framework", ""
252320
}
253321

254322
// 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: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,13 @@ 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+
// discoverMdevVFs returns all SR-IOV Virtual Functions available for vGPU,
93+
// discovered by scanning /sys/class/mdev_bus/.
94+
func discoverMdevVFs() ([]VirtualFunction, error) {
9695
entries, err := os.ReadDir(mdevBusPath)
9796
if err != nil {
9897
if os.IsNotExist(err) {
99-
return nil, nil // No mdev_bus means no vGPU support
98+
return nil, nil // No mdev_bus means no mdev vGPU support
10099
}
101100
return nil, fmt.Errorf("read mdev_bus: %w", err)
102101
}
@@ -133,20 +132,9 @@ func DiscoverVFs() ([]VirtualFunction, error) {
133132
return vfs, nil
134133
}
135134

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) {
135+
// listMdevGPUProfilesWithVFs returns available vGPU profiles using
136+
// pre-discovered VFs. Uses parallel sysfs reads for fast availability counting.
137+
func listMdevGPUProfilesWithVFs(vfs []VirtualFunction) ([]GPUProfile, error) {
150138
if len(vfs) == 0 {
151139
return nil, nil
152140
}
@@ -305,7 +293,7 @@ func countAvailableForSingleProfile(freeVFsByParent map[string][]VirtualFunction
305293

306294
// findProfileType finds the internal type name (e.g., "nvidia-556") for a profile name (e.g., "L40S-1Q")
307295
func findProfileType(profileName string) (string, error) {
308-
vfs, err := DiscoverVFs()
296+
vfs, err := discoverMdevVFs()
309297
if err != nil || len(vfs) == 0 {
310298
return "", fmt.Errorf("no VFs available")
311299
}
@@ -531,7 +519,7 @@ func CreateMdev(ctx context.Context, profileName, instanceID string) (*MdevDevic
531519
}
532520

533521
// Discover all VFs
534-
vfs, err := DiscoverVFs()
522+
vfs, err := discoverMdevVFs()
535523
if err != nil {
536524
return nil, fmt.Errorf("discover VFs: %w", err)
537525
}
@@ -697,7 +685,7 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
697685
log := logger.FromContext(ctx)
698686
_ = instanceInfos
699687

700-
vfs, err := DiscoverVFs()
688+
vfs, err := discoverMdevVFs()
701689
if err != nil {
702690
return fmt.Errorf("discover managed VFs: %w", err)
703691
}

0 commit comments

Comments
 (0)