Skip to content

Commit 155d99c

Browse files
committed
Support vendor VFIO vGPU devices
1 parent 186cbf3 commit 155d99c

19 files changed

Lines changed: 1114 additions & 144 deletions

cmd/api/main.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,32 @@ func configureUFFDGraduationController(cfg *config.Config, instanceManager insta
159159
}, logger), nil
160160
}
161161

162+
// liveInstanceVGPUDevicePaths returns the stored vGPU device paths of
163+
// instances whose hypervisor process is still alive. Vendor VFIO
164+
// reconciliation skips these VFs so it cannot clear an assignment out from
165+
// under a VM that survived a hypeman restart but has not opened the VFIO
166+
// device yet.
167+
func liveInstanceVGPUDevicePaths(ctx context.Context, instanceManager instances.Manager) (map[string]struct{}, error) {
168+
allInstances, err := instanceManager.ListInstances(ctx, nil)
169+
if err != nil {
170+
return nil, err
171+
}
172+
var protected map[string]struct{}
173+
for _, inst := range allInstances {
174+
if inst.GPUDevicePath == "" || inst.HypervisorPID == nil {
175+
continue
176+
}
177+
if err := syscall.Kill(*inst.HypervisorPID, 0); err != nil {
178+
continue
179+
}
180+
if protected == nil {
181+
protected = make(map[string]struct{})
182+
}
183+
protected[inst.GPUDevicePath] = struct{}{}
184+
}
185+
return protected, nil
186+
}
187+
162188
func run() error {
163189
// Load config early for OTel initialization
164190
// Config path can be specified via CONFIG_PATH env var or defaults to platform-specific locations
@@ -349,11 +375,15 @@ func run() error {
349375
return fmt.Errorf("reconcile device state: %w", err)
350376
}
351377

352-
// Reconcile mdev devices (clears orphaned vGPUs from previous runs)
353-
logger.Info("Reconciling mdev devices...")
354-
if err := devices.ReconcileMdevs(app.Ctx, nil); err != nil {
355-
// Log but don't fail - mdev cleanup is best-effort
356-
logger.Warn("failed to reconcile mdev devices", "error", err)
378+
// Reconcile vGPU devices (clears orphaned vGPUs from previous runs)
379+
logger.Info("Reconciling vGPU devices...")
380+
if protected, err := liveInstanceVGPUDevicePaths(app.Ctx, app.InstanceManager); err != nil {
381+
// Without the instance inventory we cannot tell live assignments from
382+
// orphans, so skip rather than clear a live VM's vGPU.
383+
logger.Warn("failed to list instances for vGPU reconcile protection; skipping vGPU reconciliation", "error", err)
384+
} else if err := devices.ReconcileVGPUs(app.Ctx, protected); err != nil {
385+
// Log but don't fail - vGPU cleanup is best-effort
386+
logger.Warn("failed to reconcile vGPU devices", "error", err)
357387
}
358388

359389
// Wire up resource validator for aggregate limit checking

integration/vgpu_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,13 @@ func checkVGPUTestPrerequisites() (string, string) {
245245
return "vGPU test requires root (sudo) for mdev creation", ""
246246
}
247247

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/", ""
248+
// Check for a vGPU framework (SR-IOV VFs present)
249+
framework, _, err := devices.DiscoverVGPU()
250+
if err != nil {
251+
return "vGPU test failed to discover vGPU framework: " + err.Error(), ""
252+
}
253+
if framework == devices.VGPUFrameworkNone {
254+
return "vGPU test requires SR-IOV VFs with an mdev or vendor VFIO vGPU framework", ""
252255
}
253256

254257
// 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
@@ -15,10 +15,9 @@ func SetGPUProfileCacheTTL(ttl string) {
1515
// No-op on macOS
1616
}
1717

18-
// DiscoverVFs returns an empty list on macOS.
19-
// SR-IOV Virtual Functions are not available on macOS.
20-
func DiscoverVFs() ([]VirtualFunction, error) {
21-
return []VirtualFunction{}, nil
18+
// DiscoverVGPU reports no vGPU framework on macOS.
19+
func DiscoverVGPU() (VGPUFramework, []VirtualFunction, error) {
20+
return VGPUFrameworkNone, nil, nil
2221
}
2322

2423
// ListGPUProfiles returns an empty list on macOS.
@@ -27,7 +26,7 @@ func ListGPUProfiles() ([]GPUProfile, error) {
2726
}
2827

2928
// ListGPUProfilesWithVFs returns an empty list on macOS.
30-
func ListGPUProfilesWithVFs(vfs []VirtualFunction) ([]GPUProfile, error) {
29+
func ListGPUProfilesWithVFs(framework VGPUFramework, vfs []VirtualFunction) ([]GPUProfile, error) {
3130
return []GPUProfile{}, nil
3231
}
3332

@@ -62,6 +61,10 @@ func DestroyVGPU(ctx context.Context, assignment VGPUAssignment) error {
6261
return nil
6362
}
6463

64+
func ReconcileVGPUs(ctx context.Context, protectedDevicePaths map[string]struct{}) error {
65+
return nil
66+
}
67+
6568
// ReconcileMdevs is a no-op on macOS.
6669
func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) error {
6770
return nil

lib/devices/mdev_linux.go

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

92-
// DiscoverVFs returns all SR-IOV Virtual Functions available for vGPU.
92+
// discoverMdevVFs returns all SR-IOV Virtual Functions available for mdev vGPU.
9393
// These are discovered by scanning /sys/class/mdev_bus/ which contains
9494
// VFs that can host mdev devices.
95-
func DiscoverVFs() ([]VirtualFunction, error) {
95+
func discoverMdevVFs() ([]VirtualFunction, error) {
9696
entries, err := os.ReadDir(mdevBusPath)
9797
if err != nil {
9898
if os.IsNotExist(err) {
99-
return nil, nil // No mdev_bus means no vGPU support
99+
return nil, nil // No mdev_bus means no mdev vGPU support
100100
}
101101
return nil, fmt.Errorf("read mdev_bus: %w", err)
102102
}
@@ -133,20 +133,9 @@ func DiscoverVFs() ([]VirtualFunction, error) {
133133
return vfs, nil
134134
}
135135

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) {
136+
// listMdevGPUProfilesWithVFs returns available vGPU profiles with availability
137+
// counts, discovered from the first VF's mdev_supported_types directory.
138+
func listMdevGPUProfilesWithVFs(vfs []VirtualFunction) ([]GPUProfile, error) {
150139
if len(vfs) == 0 {
151140
return nil, nil
152141
}
@@ -305,7 +294,7 @@ func countAvailableForSingleProfile(freeVFsByParent map[string][]VirtualFunction
305294

306295
// findProfileType finds the internal type name (e.g., "nvidia-556") for a profile name (e.g., "L40S-1Q")
307296
func findProfileType(profileName string) (string, error) {
308-
vfs, err := DiscoverVFs()
297+
vfs, err := discoverMdevVFs()
309298
if err != nil || len(vfs) == 0 {
310299
return "", fmt.Errorf("no VFs available")
311300
}
@@ -531,7 +520,7 @@ func CreateMdev(ctx context.Context, profileName, instanceID string) (*MdevDevic
531520
}
532521

533522
// Discover all VFs
534-
vfs, err := DiscoverVFs()
523+
vfs, err := discoverMdevVFs()
535524
if err != nil {
536525
return nil, fmt.Errorf("discover VFs: %w", err)
537526
}
@@ -697,7 +686,7 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
697686
log := logger.FromContext(ctx)
698687
_ = instanceInfos
699688

700-
vfs, err := DiscoverVFs()
689+
vfs, err := discoverMdevVFs()
701690
if err != nil {
702691
return fmt.Errorf("discover managed VFs: %w", err)
703692
}

lib/devices/types.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,29 +63,34 @@ type GPUMode string
6363
type VGPUFramework string
6464

6565
const (
66-
VGPUFrameworkNone VGPUFramework = ""
67-
VGPUFrameworkMdev VGPUFramework = "mdev"
66+
VGPUFrameworkNone VGPUFramework = ""
67+
VGPUFrameworkMdev VGPUFramework = "mdev"
68+
VGPUFrameworkVendorVFIO VGPUFramework = "vendor-vfio"
6869

6970
// GPUModePassthrough indicates whole GPU VFIO passthrough
7071
GPUModePassthrough GPUMode = "passthrough"
71-
// GPUModeVGPU indicates SR-IOV + mdev based vGPU
72+
// GPUModeVGPU indicates an mdev or vendor VFIO vGPU host
7273
GPUModeVGPU GPUMode = "vgpu"
7374
// GPUModeNone indicates no GPU available
7475
GPUModeNone GPUMode = "none"
7576
)
7677

7778
// VirtualFunction represents an SR-IOV Virtual Function for vGPU
7879
type VirtualFunction struct {
79-
PCIAddress string `json:"pci_address"` // e.g., "0000:82:00.4"
80-
ParentGPU string `json:"parent_gpu"` // e.g., "0000:82:00.0"
81-
Allocated bool `json:"allocated"` // true if a vGPU is assigned to this VF
80+
PCIAddress string `json:"pci_address"` // e.g., "0000:82:00.4"
81+
ParentGPU string `json:"parent_gpu"` // e.g., "0000:82:00.0"
82+
Allocated bool `json:"allocated"` // true if a vGPU is assigned to this VF
83+
ProfileType string `json:"profile_type,omitempty"`
8284
}
8385

8486
// VGPUAssignment identifies an existing vGPU assignment to release.
8587
type VGPUAssignment struct {
8688
Framework VGPUFramework
8789
DevicePath string
8890
MdevUUID string
91+
// InstanceID guards vendor VFIO releases: a release is skipped when the
92+
// VF's in-process owner is a different instance.
93+
InstanceID string
8994
}
9095

9196
type VGPUDevice struct {

0 commit comments

Comments
 (0)