Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
e603429
Guard vGPU releases with live-instance claims
yummybomb Aug 6, 2026
5e7955a
Reconcile vendor VFIO vGPUs against a fail-closed instance inventory
yummybomb Aug 6, 2026
d7eadf8
Fail closed on vGPU claim checks
yummybomb Aug 6, 2026
506b421
Retain only vGPU assignment after failed create
yummybomb Aug 6, 2026
bd1fd38
Clear released vGPU assignment on start rollback
yummybomb Aug 6, 2026
5ca85a2
Test start rollback vGPU cleanup
yummybomb Aug 6, 2026
afc8dad
Normalize legacy mdev paths in live-claim check
yummybomb Aug 7, 2026
df1a288
Bind the live-claimant test socket under /tmp for macOS
yummybomb Aug 7, 2026
4453651
Surface retained vGPU cleanup through a typed create error and manage…
yummybomb Aug 7, 2026
38f14b1
Generalize the create vGPU error text
yummybomb Aug 7, 2026
1411cbd
Scope vGPU claim scan to vendor VFIO and close reconcile gaps
yummybomb Aug 8, 2026
520533a
Harden the vendor VFIO release path
yummybomb Aug 9, 2026
7e1737c
Fail closed on retained vGPU cleanup
yummybomb Aug 9, 2026
928f8d2
Report surviving vGPU retention metadata
yummybomb Aug 9, 2026
3f80948
Return accurate vGPU cleanup guidance
yummybomb Aug 9, 2026
5960195
Clarify vGPU retention fallback
yummybomb Aug 9, 2026
f677f8b
Pass hypervisor identity token to vGPU claim check
yummybomb Aug 10, 2026
51508f0
Fail safely on ambiguous vGPU claims
yummybomb Aug 10, 2026
3af8301
Expose retained vGPU instance IDs
yummybomb Aug 10, 2026
ad4c713
Harden vGPU startup rollback recovery
yummybomb Aug 10, 2026
df3dcf8
Preserve the create failure cause in vGPU cleanup errors
yummybomb Aug 10, 2026
d60193e
Fix vGPU reconciliation edge cases
yummybomb Aug 10, 2026
d0de0cc
Use boot-scoped hypervisor identities for vGPUs
yummybomb Aug 10, 2026
5b1b190
Run vGPU rollback tests with QEMU
yummybomb Aug 10, 2026
5420fb9
Protect new vGPU assignments from stale PIDs
yummybomb Aug 10, 2026
83c7f51
Persist vGPU assignments after create rollback failure
yummybomb Aug 10, 2026
abf009b
Preserve vGPU lifecycle compatibility
yummybomb Aug 10, 2026
e5e35e2
Reconcile vGPU protection from raw metadata and restore GPUAssignedAt
yummybomb Aug 10, 2026
081ead1
Surface pending vGPU cleanup from start as a typed error
yummybomb Aug 11, 2026
45fbedc
Cover retained-stub delete recovery and flag reconcile inventory fail…
yummybomb Aug 11, 2026
c162c87
Reject vendor VFIO vGPUs on Cloud Hypervisor and improve wedge forensics
yummybomb Aug 11, 2026
4fe3ad9
Surface retained rollback assignments from start as vgpu_cleanup_pending
yummybomb Aug 11, 2026
01f754b
Report retention as persisted when the mid-start save survives
yummybomb Aug 11, 2026
1838846
Leave vGPU hypervisor selection to callers
yummybomb Aug 11, 2026
b79b13e
Carry identity fields into the create-pending retention stub
yummybomb Aug 11, 2026
84f821a
Grace recent dead-PID claims in the release scan like reconcile does
yummybomb Aug 11, 2026
af8d527
Reject start on vGPU retention records
yummybomb Aug 13, 2026
ca6f98d
Make vGPU retention records fully delete-only
yummybomb Aug 13, 2026
ab20441
Adapt vGPU liveness guards to the identity struct resolver
yummybomb Aug 14, 2026
eb52f8c
Deep-copy the phase tracker into the start rollback snapshot
yummybomb Aug 17, 2026
3f4c157
Retry orphaned vGPU releases in the background after delete
yummybomb Aug 18, 2026
942a33d
Bound orphan retries in delete tests and tighten comments
yummybomb Aug 18, 2026
70de8d4
SIGTERM vGPU QEMU before SIGKILL during driver init
yummybomb Aug 19, 2026
c64689d
Apply vGPU SIGTERM grace on stop's direct kill paths
yummybomb Aug 19, 2026
53ff425
Lower vGPU SIGTERM grace to 5s
yummybomb Aug 19, 2026
c9e7ece
Apply vGPU SIGTERM grace in every instance state
yummybomb Aug 20, 2026
7948ce9
Harden the vGPU claim scan and retention-stub stop path
yummybomb Aug 20, 2026
bb8614a
Share one vGPU retention stub in create
yummybomb Aug 21, 2026
54c37b3
Name the strict metadata listing variant
yummybomb Aug 21, 2026
b86d1da
Skip instances deleted mid-listing in reconcile protection
yummybomb Aug 21, 2026
f255bb2
Count abandoned orphaned vGPU releases
yummybomb Aug 21, 2026
3644fbe
Define the vGPU retention-stub rejection once
yummybomb Aug 21, 2026
b444663
Render pending vGPU cleanup responses through one helper
yummybomb Aug 21, 2026
4d21b0b
Narrow the vGPU reconcile interface
yummybomb Aug 24, 2026
34deb09
Encapsulate vGPU rollback retention
yummybomb Aug 24, 2026
61c563e
Move vGPU reconciliation into instance manager
yummybomb Aug 24, 2026
e417c85
Derive QEMU VFIO grace from VM config
yummybomb Aug 24, 2026
648373d
Tighten vGPU lifecycle comments
yummybomb Aug 24, 2026
928d61c
Retry vGPU recovery paths that previously waited for restart
yummybomb Aug 24, 2026
e361540
Validate live vGPU claim test identity
yummybomb Aug 24, 2026
a9387fc
Clean guest data before retaining vGPU assignment
yummybomb Aug 24, 2026
9b3ece3
Retry failed vGPU reconciliation
yummybomb Aug 25, 2026
c273752
Trim vGPU lifecycle comments and tests
yummybomb Aug 25, 2026
78d8fa3
Track vendor VFIO assignment age and owner for periodic sweeps
yummybomb Aug 25, 2026
937caf1
Replace vGPU release retry machinery with a periodic reconciler
yummybomb Aug 25, 2026
d1ac2f2
Update vgpu_cleanup_pending assertions for the periodic reconcile mes…
yummybomb Aug 25, 2026
93cc755
Check socket ownership for vGPU liveness without a persisted PID
yummybomb Aug 25, 2026
a7886fe
Share one vGPU assignment grace period constant
yummybomb Aug 25, 2026
34f606a
Preserve delete-only vGPU retention records
yummybomb Aug 26, 2026
742281e
Trim redundant vGPU coverage
yummybomb Aug 26, 2026
33f58e1
Add counters for vGPU cleanup failure paths
yummybomb Aug 26, 2026
ea55f13
Simplify vGPU lifecycle cleanup
yummybomb Aug 26, 2026
be458d1
Simplify vGPU retention persistence to a single attempt
yummybomb Aug 27, 2026
a43dee1
Report vGPU retention as persisted when the assignment claim survives
yummybomb Aug 27, 2026
07da772
Protect claimed mdevs during reconciliation
yummybomb Aug 28, 2026
4f13a3c
Unify vGPU retention wrapping and dedupe test fixtures
yummybomb Aug 28, 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
34 changes: 34 additions & 0 deletions cmd/api/api/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,16 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst

inst, err := s.InstanceManager.CreateInstance(ctx, domainReq)
if err != nil {
var vgpuPending *instances.VGPUCleanupPendingError
switch {
case errors.As(err, &vgpuPending):
log.ErrorContext(ctx, "failed to create instance", "error", err, "image", request.Body.Image)
message, inner := vgpuCleanupPendingDetail(vgpuPending, "create", "delete it to retry")
return oapi.CreateInstance500JSONResponse{
Code: "vgpu_cleanup_pending",
Message: message,
InnerError: inner,
}, nil
Comment thread
cursor[bot] marked this conversation as resolved.
case errors.Is(err, instances.ErrImageNotReady):
return oapi.CreateInstance400JSONResponse{
Code: "image_not_ready",
Expand Down Expand Up @@ -424,6 +433,19 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
return oapi.CreateInstance201JSONResponse(instanceToOAPI(*inst)), nil
}

func vgpuCleanupPendingDetail(pending *instances.VGPUCleanupPendingError, action, retainedGuidance string) (string, *oapi.ErrorDetail) {
message := fmt.Sprintf("failed to %s instance: %v", action, pending)
innerCode := "vgpu_unretained_instance"
if pending.Retained {
message += "; " + retainedGuidance
innerCode = "vgpu_retained_instance"
}
return message, &oapi.ErrorDetail{
Code: lo.ToPtr(innerCode),
Message: lo.ToPtr(pending.InstanceID),
}
}

// GetInstance gets instance details
// The id parameter can be an instance ID, name, or ID prefix
// Note: Resolution is handled by ResolveResource middleware
Expand Down Expand Up @@ -834,7 +856,16 @@ func (s *ApiService) StartInstance(ctx context.Context, request oapi.StartInstan

result, err := s.InstanceManager.StartInstance(ctx, inst.Id, startReq)
if err != nil {
var vgpuPending *instances.VGPUCleanupPendingError
switch {
case errors.As(err, &vgpuPending):
log.ErrorContext(ctx, "failed to start instance", "error", err)
message, inner := vgpuCleanupPendingDetail(vgpuPending, "start", "delete it or retry start to release it")
return oapi.StartInstance500JSONResponse{
Code: "vgpu_cleanup_pending",
Message: message,
InnerError: inner,
}, nil
case errors.Is(err, instances.ErrInvalidState):
return oapi.StartInstance409JSONResponse{
Code: "invalid_state",
Expand Down Expand Up @@ -1269,6 +1300,9 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
if inst.GPUMdevUUID != "" {
gpu.MdevUuid = lo.ToPtr(inst.GPUMdevUUID)
}
if inst.GPUDevicePath != "" {
gpu.DevicePath = lo.ToPtr(inst.GPUDevicePath)
}
oapiInst.Gpu = gpu
}

Expand Down
65 changes: 65 additions & 0 deletions cmd/api/api/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/kernel/hypeman/lib/instances"
"github.com/kernel/hypeman/lib/instances/phasetracking"
mw "github.com/kernel/hypeman/lib/middleware"
"github.com/kernel/hypeman/lib/network"
"github.com/kernel/hypeman/lib/oapi"
"github.com/kernel/hypeman/lib/paths"
restartpolicy "github.com/kernel/hypeman/lib/restart-policy"
Expand Down Expand Up @@ -47,6 +48,39 @@ func TestGetInstance_NotFound(t *testing.T) {
require.Error(t, err)
}

func TestVGPUCleanupPendingDetail(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
name string
retained bool
code string
guidance string
exclude string
}{
{name: "retained", retained: true, code: "vgpu_retained_instance", guidance: "delete it to retry"},
{name: "unretained", code: "vgpu_unretained_instance", guidance: "periodic vGPU reconcile", exclude: "delete"},
} {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
message, inner := vgpuCleanupPendingDetail(&instances.VGPUCleanupPendingError{
InstanceID: "inst-1",
Retained: tt.retained,
Err: network.ErrNameExists,
}, "create", "delete it to retry")
assert.Contains(t, message, "inst-1")
assert.Contains(t, message, network.ErrNameExists.Error())
assert.Contains(t, message, tt.guidance)
if tt.exclude != "" {
assert.NotContains(t, message, tt.exclude)
}
require.NotNil(t, inner.Code)
assert.Equal(t, tt.code, *inner.Code)
require.NotNil(t, inner.Message)
assert.Equal(t, "inst-1", *inner.Message)
})
}
}

func TestCreateInstance_AutoPullImage(t *testing.T) {
t.Parallel()
if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) {
Expand Down Expand Up @@ -890,6 +924,16 @@ func TestCreateInstance_ErrorStatusMapping(t *testing.T) {
wantCode string
wantMessage string
}{
{
name: "vGPU cleanup pending beats wrapped name conflict -> 500",
err: &instances.VGPUCleanupPendingError{
InstanceID: "inst-1",
Retained: true,
Err: network.ErrNameExists,
},
wantType: oapi.CreateInstance500JSONResponse{},
wantCode: "vgpu_cleanup_pending",
},
{
name: "platform not available -> 404",
err: fmt.Errorf("resolve image: %w", images.ErrPlatformNotAvailable),
Expand Down Expand Up @@ -1014,6 +1058,27 @@ func TestRestoreInstance_ErrorMapping(t *testing.T) {
}
}

func TestStartInstance_VGPUCleanupPendingBeatsWrappedErrorMapping(t *testing.T) {
t.Parallel()
svc := newTestService(t)
resolved := &instances.Instance{
StoredMetadata: instances.StoredMetadata{Id: "inst-1", Name: "inst-1"},
State: instances.StateStopped,
}
svc.InstanceManager = &errActionInstanceManager{Manager: svc.InstanceManager, err: &instances.VGPUCleanupPendingError{
InstanceID: resolved.Id,
Retained: true,
Err: fmt.Errorf("create vGPU for profile p: %w", instances.ErrInsufficientResources),
}}

resp, err := svc.StartInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.StartInstanceRequestObject{Id: resolved.Id})
require.NoError(t, err)
pending, ok := resp.(oapi.StartInstance500JSONResponse)
require.True(t, ok, "expected 500 vgpu_cleanup_pending, got %T", resp)
assert.EqualValues(t, "vgpu_cleanup_pending", pending.Code)
assert.Contains(t, pending.Message, "delete it or retry start")
}

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

Expand Down
8 changes: 3 additions & 5 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,9 @@ func run() error {
return fmt.Errorf("reconcile device state: %w", err)
}

// Reconcile mdev devices (clears orphaned vGPUs from previous runs)
logger.Info("Reconciling mdev devices...")
if err := devices.ReconcileMdevs(app.Ctx, nil); err != nil {
// Log but don't fail - mdev cleanup is best-effort
logger.Warn("failed to reconcile mdev devices", "error", err)
logger.Info("Reconciling vGPU devices...")
if r, ok := app.InstanceManager.(interface{ StartVGPUReconciler(context.Context) }); ok {
r.StartVGPUReconciler(ctx)
}

// Wire up resource validator for aggregate limit checking
Expand Down
5 changes: 0 additions & 5 deletions integration/vgpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,6 @@ func checkVGPUTestPrerequisites() (string, string) {
if framework == devices.VGPUFrameworkNone {
return "vGPU test requires SR-IOV VFs with an mdev or vendor VFIO vGPU framework", ""
}
if framework == devices.VGPUFrameworkVendorVFIO {
// CreateVGPU rejects vendor VFIO until the instance lifecycle
// integration lands.
return "vGPU test requires the vendor VFIO instance lifecycle integration", ""
}

// Check for available profiles
profiles, err := devices.ListGPUProfiles()
Expand Down
17 changes: 12 additions & 5 deletions lib/devices/GPU.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Instance Create → Assign profile to VF → Attach VF to VM → Instance Runnin
Instance Stop/Delete → Release profile → VF available again
```

Hypeman reconciles orphaned assignments on server restart while preserving devices held open by a running VMM.
Hypeman reconciles orphaned assignments with a periodic fail-closed pass: once at startup and every minute afterward (skipped entirely on hosts without GPUs). Each pass releases assignments whose owning instance is no longer live and clears their metadata, then sweeps device-level leftovers with no live metadata claim. Devices held open by a running VMM and assignments younger than five minutes are preserved, so a release that fails during stop or delete (typically because a GPU-busy VMM's kernel-side VFIO teardown outlives the force-kill wait) is simply retried on later passes until the device is free.

### Hypervisor Support

Expand Down Expand Up @@ -288,10 +288,17 @@ every request, so one wedged VF presents as all vGPU instances failing while
`/resources` reports full capacity.

The wedge itself leaves no host-side log: no kernel error, no XID, no plugin
crash. In the observed case it followed a period of heavy attach/teardown
churn on the VF, including QEMU processes that exited within seconds of
opening the VFIO device (failed start attempts that were then retried), so
suspect any workload that repeatedly kills the VMM mid-device-init.
crash. The trigger is a SIGKILL delivered to QEMU while the vGPU plugin is
still initializing the VF (roughly the first seconds after process start):
a single hard kill in that window wedges the VF near-deterministically,
while QEMU processes that exit voluntarily — error exits, QMP quit, SIGTERM —
run their VFIO teardown and never wedge, and hard kills of fully-initialized
vGPU VMs are also safe. Hypeman therefore SIGTERMs a vGPU QEMU first and only
escalates to SIGKILL after a grace period, both in start-failure cleanup and
when force-killing any vGPU instance (the instance reports Running seconds
before driver init completes, so no state reliably marks the window); a hard
kill after an ignored SIGTERM logs `VF may wedge` with the device path.
External SIGKILLs (OOM killer, manual `kill -9`) can still trigger it.

Confirm by assigning the same profile on a different VF: if that guest
initializes, the VF is wedged, not the driver stack. Remediate by cycling
Expand Down
2 changes: 1 addition & 1 deletion lib/devices/mdev_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func DestroyVGPU(ctx context.Context, assignment VGPUAssignment) error {
return nil
}

func ReconcileVGPUs(ctx context.Context, protectedDevicePaths map[string]struct{}) error {
func ReconcileVGPUs(ctx context.Context, protectedDevicePaths map[string]struct{}, sweepDevices bool) error {
return nil
}

Expand Down
35 changes: 25 additions & 10 deletions lib/devices/mdev_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ import (
)

const (
mdevBusPath = "/sys/class/mdev_bus"
mdevDevices = "/sys/bus/mdev/devices"
orphanedMdevGracePeriod = 5 * time.Minute
procPath = "/proc"
mdevBusPath = "/sys/class/mdev_bus"
mdevDevices = "/sys/bus/mdev/devices"
procPath = "/proc"
)

// mdevMu protects mdev creation/destruction to prevent race conditions
Expand Down Expand Up @@ -708,17 +707,27 @@ func mdevPastGracePeriod(mdevUUID string, gracePeriod time.Duration) (bool, time
return age >= gracePeriod, age, nil
}

func protectedMdevUUIDs(instanceInfos []MdevReconcileInfo) map[string]struct{} {
protected := make(map[string]struct{}, len(instanceInfos))
for _, info := range instanceInfos {
if info.MdevUUID != "" && info.IsRunning {
protected[info.MdevUUID] = struct{}{}
}
}
return protected
}

// ReconcileMdevs destroys orphaned mdevs on managed VFs.
// This is called on server startup to clean up stale mdevs from previous runs.
//
// Policy:
// - Consider only mdevs whose parent VF is currently managed by hypeman (discoverable via /sys/class/mdev_bus)
// - Keep mdevs claimed by live instance metadata
// - Keep mdevs whose VFIO group has an open file handle (/dev/vfio/<group>)
// - Keep mdevs younger than a short grace period to avoid racing very recent state transitions
// - Delete all remaining mdevs
func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) error {
log := logger.FromContext(ctx)
_ = instanceInfos
protectedMdevs := protectedMdevUUIDs(instanceInfos)

vfs, err := discoverMdevVFs()
if err != nil {
Expand Down Expand Up @@ -747,17 +756,22 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
log.InfoContext(ctx, "reconciling mdev devices",
"total_mdevs", len(mdevs),
"managed_vfs", len(managedVFs),
"grace_period", orphanedMdevGracePeriod.String(),
"grace_period", VGPUAssignmentGracePeriod.String(),
)

groupInUseCache := make(map[int]bool)
var destroyed, failedDestroy, skippedUnmanagedVF, skippedInUse, skippedGrace, skippedProbeError int
var destroyed, failedDestroy, skippedUnmanagedVF, skippedClaimed, skippedInUse, skippedGrace, skippedProbeError int
for _, mdev := range mdevs {
if _, ok := managedVFs[mdev.VFAddress]; !ok {
log.DebugContext(ctx, "skipping mdev on unmanaged VF", "uuid", mdev.UUID, "vf", mdev.VFAddress)
skippedUnmanagedVF++
continue
}
if _, ok := protectedMdevs[mdev.UUID]; ok {
log.DebugContext(ctx, "skipping mdev claimed by live instance", "uuid", mdev.UUID)
skippedClaimed++
continue
}

group, err := mdevIOMMUGroup(mdev.UUID)
if err != nil {
Expand All @@ -782,7 +796,7 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
continue
}

pastGracePeriod, age, err := mdevPastGracePeriod(mdev.UUID, orphanedMdevGracePeriod)
pastGracePeriod, age, err := mdevPastGracePeriod(mdev.UUID, VGPUAssignmentGracePeriod)
if err != nil {
log.WarnContext(ctx, "failed to determine mdev age, skipping cleanup", "uuid", mdev.UUID, "error", err)
skippedProbeError++
Expand All @@ -792,7 +806,7 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
log.DebugContext(ctx, "skipping recently created mdev during grace period",
"uuid", mdev.UUID,
"age", age.String(),
"grace_period", orphanedMdevGracePeriod.String(),
"grace_period", VGPUAssignmentGracePeriod.String(),
)
skippedGrace++
continue
Expand All @@ -818,6 +832,7 @@ func ReconcileMdevs(ctx context.Context, instanceInfos []MdevReconcileInfo) erro
"destroyed", destroyed,
"failed_destroy", failedDestroy,
"skipped_unmanaged_vf", skippedUnmanagedVF,
"skipped_claimed", skippedClaimed,
"skipped_in_use", skippedInUse,
"skipped_grace", skippedGrace,
"skipped_probe_error", skippedProbeError,
Expand Down
5 changes: 5 additions & 0 deletions lib/devices/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ func ValidateDeviceName(name string) bool {
// GPUMode represents the host's GPU configuration mode
type GPUMode string

// VGPUAssignmentGracePeriod protects a fresh vGPU assignment from cleanup
// until its VM has had time to boot and become identifiable — by a persisted
// hypervisor PID, a control-socket owner, or an open VFIO handle.
const VGPUAssignmentGracePeriod = 5 * time.Minute

type VGPUFramework string

const (
Expand Down
Loading
Loading