Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c25065e
Quarantine wedged vGPU VFs convicted by the guest sentinel
yummybomb Aug 20, 2026
4039379
Report GPU init failure from the guest agent and harden quarantine
yummybomb Aug 20, 2026
0ca6c9f
Close sentinel replay, fail-open, and persist-retry holes
yummybomb Aug 20, 2026
fe81ba3
Match only kernel-facility kmsg records in the GPU init watch
yummybomb Aug 21, 2026
e2dfbdf
Bound vGPU sentinel scans to the line cap
yummybomb Aug 21, 2026
c3f8da6
Signal an unavailable VF health store
yummybomb Aug 21, 2026
79a6fb7
Repeat the GPU init-failure marker so printk splits cannot lose a report
yummybomb Aug 21, 2026
8fb7b3c
Remove the conviction brake
yummybomb Aug 21, 2026
dddfba4
Read /dev/kmsg with a record-sized buffer
yummybomb Aug 21, 2026
c0ab31d
Fsync VF health state writes
yummybomb Aug 21, 2026
d7f11d8
Persist only the matched sentinel marker
yummybomb Aug 21, 2026
66ddbe0
Trim unused VF health and sentinel surface
yummybomb Aug 21, 2026
68fd1b5
Re-probe framework discovery while the sentinel gate is unknown
yummybomb Aug 21, 2026
964ed64
Revalidate the assignment before conviction and list targets strictly
yummybomb Aug 21, 2026
97601ce
Document draining the parent GPU before an SR-IOV cycle
yummybomb Aug 21, 2026
8881e6b
Deduplicate the wedge remediation docs
yummybomb Aug 21, 2026
c0aa19b
Convict on a released assignment instead of skipping
yummybomb Aug 21, 2026
c733ccc
Harden vGPU quarantine accounting
yummybomb Aug 24, 2026
76f189e
Validate persisted VF health state
yummybomb Aug 24, 2026
3072812
Require full vGPU sentinel log lines
yummybomb Aug 24, 2026
781a308
Reject incompatible vGPU sentinel managers
yummybomb Aug 24, 2026
e89113a
Initialize VF health during provider wiring
yummybomb Aug 24, 2026
e799cfd
Centralize allocatable VF quarantine policy
yummybomb Aug 24, 2026
8ddbd09
Distinguish partial metadata listing failures
yummybomb Aug 24, 2026
e162880
Share the vGPU placement lock across platforms
yummybomb Aug 24, 2026
98b7ddf
Test guest GPU reports against host matcher
yummybomb Aug 24, 2026
fce12e1
Log vGPU sentinel start after vendor detection
yummybomb Aug 24, 2026
0ca236d
Tighten vGPU sentinel comments
yummybomb Aug 24, 2026
f3bf0cc
Tidy VF health store boundaries and admission errors
yummybomb Aug 24, 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
6 changes: 6 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,12 @@ func run() error {
return app.HealthCheckController.Run(gctx)
})
}
if app.VGPUSentinelController != nil {
grp.Go(func() error {
logger.Info("starting vGPU sentinel controller")
return app.VGPUSentinelController.Run(gctx)
})
}
if restartController, ok := app.InstanceManager.(interface {
StartRestartPolicyController(context.Context) error
}); ok {
Expand Down
42 changes: 22 additions & 20 deletions cmd/api/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,27 @@ import (

// application struct to hold initialized components
type application struct {
Ctx context.Context
Logger *slog.Logger
Config *config.Config
ImageManager images.Manager
SystemManager system.Manager
NetworkManager network.Manager
DeviceManager devices.Manager
InstanceManager instances.Manager
VolumeManager volumes.Manager
BuilderManager builders.Manager
IngressManager ingress.Manager
BuildManager builds.Manager
PushManager imagepush.Manager
ResourceManager *resources.Manager
GuestMemoryController guestmemory.Controller
AutoStandbyController *autostandby.Controller
HealthCheckController *instances.HealthCheckController
VMMetricsManager *vm_metrics.Manager
Registry *registry.Registry
ApiService *api.ApiService
Ctx context.Context
Logger *slog.Logger
Config *config.Config
ImageManager images.Manager
SystemManager system.Manager
NetworkManager network.Manager
DeviceManager devices.Manager
InstanceManager instances.Manager
VolumeManager volumes.Manager
BuilderManager builders.Manager
IngressManager ingress.Manager
BuildManager builds.Manager
PushManager imagepush.Manager
ResourceManager *resources.Manager
GuestMemoryController guestmemory.Controller
AutoStandbyController *autostandby.Controller
HealthCheckController *instances.HealthCheckController
VGPUSentinelController *instances.VGPUSentinelController
VMMetricsManager *vm_metrics.Manager
Registry *registry.Registry
ApiService *api.ApiService
}

// initializeApp is the injector function
Expand All @@ -72,6 +73,7 @@ func initializeApp() (*application, func(), error) {
providers.ProvideGuestMemoryController,
providers.ProvideAutoStandbyController,
providers.ProvideHealthCheckController,
providers.ProvideVGPUSentinelController,
providers.ProvideVMMetricsManager,
providers.ProvideRegistry,
api.New,
Expand Down
86 changes: 46 additions & 40 deletions cmd/api/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 67 additions & 10 deletions lib/devices/GPU.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,41 @@ NVRM: GPU 0000:00:03.0: RmInitAdapter failed! (0x22:0x65:884)
```

(0x65 = timeout; the guest's init requests are never answered, and
`/proc/interrupts` shows the GPU's MSI-X vectors allocated but idle). Because
placement is deterministic least-loaded, an idle host re-picks the same VF for
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. The trigger is a SIGKILL delivered to QEMU while the vGPU plugin is
`/proc/interrupts` shows the GPU's MSI-X vectors allocated but idle).

Hypeman detects this automatically: the guest agent watches the guest kernel
log (`/dev/kmsg`) for that line and reports it as a `HYPEMAN-GPU-INIT-FAILED`
marker in the instance's `logs/app.log`, which the vGPU sentinel controller
scans for every vendor VFIO instance. A match quarantines the VF in
`<data-dir>/gpu/vf-health.json` (it survives restarts): the VF is excluded
from placement and from advertised profile availability, and its parent GPU
becomes overflow-only — deprioritized for new placements. The conviction is
logged at error level (`quarantined wedged vGPU VF`) and counted in
`hypeman_instances_vgpu_sentinel_convictions_total`;
`hypeman_instances_vgpu_quarantined_vfs` gauges the current quarantine count.
There is no rate limit on convictions: a systemic non-wedge init failure
(e.g. a guest/host driver mismatch) emits the same line on every VF and
would quarantine the whole host, so such changes must be validated on a
test host first, and the convictions counter is the signal to alert on if
one gets through.

Detection requires the hypeman guest agent: an image that skips the agent
never reports, so a wedge hit exclusively by such images stays undetected in
v1. Start archives the previous boot's app log before persisting a new
assignment, so a marker written within one scan interval of a stop/start is
archived unscanned; that VF returns to the pool unconvicted until the next
victim boot re-emits the marker. The scanner requires the complete standalone
guest-agent log envelope, so customer commands and ordinary output containing
the marker do not match. The
serial console remains guest-writable: a root guest can deliberately imitate
the entire line and quarantine the VF its own instance holds; the quarantine
only ever removes capacity, never touches the instance.

The wedge-creating kill itself leaves no host-side log: no kernel error, no
XID, no plugin crash. Detection therefore happens on the next boot that lands
on the VF, whose guest driver starts failing ~27s after spawn.

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 —
Expand All @@ -303,18 +331,47 @@ 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
SR-IOV on the parent GPU (this destroys and recreates all of its VFs, so it
requires no vGPU assignments on that GPU):
requires no vGPU assignments on that GPU). The DCGM quiesce is not optional:
with `nv-hostengine`/`dcgm-exporter` holding the GPUs open, `sriov-manage -d`
fails with `Cannot obtain unbindLock` on first contact.

Any manual edit to `vf-health.json` needs an immediate hypeman restart: the
store loads only at startup, and a conviction landing first re-persists the
in-memory set over your edit. The restart does not disturb running VMs —
startup reconciliation protects live VFs.

**Draining the parent GPU.** Overflow-only is a preference, not a cordon:
under capacity pressure new placements still land on the card's healthy VFs
and refill it. To drain the card, quarantine all of its VFs by hand — add
records to `vf-health.json` (the shape of a real conviction: `vf_address`
plus `quarantined_at`) and restart. Running instances are untouched and
drain through their normal lifecycle: standby is blocked for vGPU instances,
so only a running VM pins a VF, and each stop or delete frees one for good.
Monitor by listing instances whose `gpu.device_path` sits under the parent
GPU; once none remain, run the cycle below.

```bash
# 1. Quiesce the services holding the GPU (required for the unbind lock).
systemctl stop nvidia-dcgm-exporter nvidia-dcgm

# 2. Cycle SR-IOV on the parent GPU.
/usr/lib/nvidia/sriov-manage -d <parent-gpu-pci-addr>
/usr/lib/nvidia/sriov-manage -e <parent-gpu-pci-addr>

# 3. Restart the quiesced services.
systemctl start nvidia-dcgm nvidia-dcgm-exporter
```

After the cycle, clear the quarantine by removing the card's entries from
`vf-health.json` (restart rule above), then boot a GPU instance as
verification: placement excludes quarantined VFs, so the recovered VF cannot
be targeted while its entry exists, and there is no VF-pin API — clearing
first is safe because the sentinel automatically re-quarantines the VF if the
cycle did not cure it (every cycle in hardware validation did).

Do not unbind/rebind the VF from the nvidia driver — it breaks the
nvidia-vgpu-vfio core-device registration (`vfio_pci_core_device not found`)
and the VF stops accepting assignments entirely until the SR-IOV cycle.
Services holding the GPU (DCGM, persistenced) must be stopped for the cycle
to obtain the unbind lock.

### vGPU assignment fails

Expand Down
7 changes: 7 additions & 0 deletions lib/devices/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"runtime"
"strings"
Expand Down Expand Up @@ -85,6 +86,12 @@ type manager struct {
// NewManager creates a new device manager.
// Use SetLivenessChecker after construction to enable accurate orphan detection.
func NewManager(p *paths.Paths) Manager {
// The VF health store lives with the device manager: constructing one
// without loading the store would run placement against an empty,
// non-persisting quarantine set.
if err := InitVFHealth(p.VFHealthState()); err != nil {
slog.Default().Error("failed to load VF health state; vGPU placement is disabled until the state file is repaired or removed", "error", err)
}
return &manager{
paths: p,
vfioBinder: NewVFIOBinder(),
Expand Down
Loading
Loading