Detect wedged vGPU VFs from the guest and report to the health store - #435
Detect wedged vGPU VFs from the guest and report to the health store#435yummybomb wants to merge 8 commits into
Conversation
d312338 to
7d54fb9
Compare
7d54fb9 to
9a90223
Compare
9a90223 to
0ff16d1
Compare
0ff16d1 to
3db5546
Compare
3db5546 to
b35501a
Compare
b3bf925 to
55a0d6f
Compare
55a0d6f to
ee1b160
Compare
a5dc229 to
fed19c4
Compare
7bd4f52 to
67b2b24
Compare
67b2b24 to
0662ad9
Compare
f3bf0cc to
ff54c20
Compare
ff54c20 to
37bc190
Compare
6582c0e to
4d174d2
Compare
yummybomb
left a comment
There was a problem hiding this comment.
one blocking test-quality issue. the sentinel/store boundaries, fail-closed placement behavior, assignment revalidation, and persistence rollback paths otherwise look coherent in context. targeted race tests and go vet pass locally; the current linux CI also exposes the changed-test failure below.
| require.NoError(t, os.WriteFile(nvidiaSMI, []byte(script), 0o755)) | ||
| t.Setenv("COUNT_PATH", countPath) | ||
|
|
||
| probeGPUInitUntil(&gpuInitReporter{}, nvidiaSMI, time.Now().Add(time.Second), 10*time.Millisecond, 0) |
There was a problem hiding this comment.
blocker: this 10ms deadline is shorter than normal shell startup under race/loaded CI, so the nominally successful second attempt can time out too. The current Linux job failed here with actual: 3, and go test -race ... -count=100 reproduces it locally. Please make the attempt runner/clock injectable and use a deterministic fake, or otherwise give the success attempt independent scheduling margin; asserting exactly two real process launches with this deadline is flaky.
There was a problem hiding this comment.
fixed in 88f3c3d by injecting the probe attempt into the retry loop and testing the timeout/success sequence directly. go test -race ./lib/system/guest_agent -run TestProbeGPUInitRetriesAfterAttemptTimeout -count=100, the package race suite, and go vet pass.
yummybomb
left a comment
There was a problem hiding this comment.
two code-quality findings. the store/placement boundaries, persistence rollback, assignment revalidation, and tests otherwise look coherent in repo context. the new functions peak at cyclomatic complexity 16 (loadLocked / selectLeastLoadedVF); those paths remain cohesive and well-covered, so I don't see mechanical decomposition as a merge requirement.
| func (c *VGPUSentinelController) scanTarget(ctx context.Context, target vgpuSentinelTarget) { | ||
| tail := c.tails[target.instanceID] | ||
| if tail == nil || tail.vfAddress != target.vfAddress || tail.assignedAt != target.assignedAt { | ||
| tail = &vgpuSentinelTail{vfAddress: target.vfAddress, assignedAt: target.assignedAt} |
There was a problem hiding this comment.
The tail offset is process-local, but a successful report removes the only persistent dedup record. After FAILED -> OK, restarting hypeman starts at offset 0, records the historical failure again, increments init_failures_total, then clears it again on the historical OK. With vf_quarantine_threshold: 1, replay also emits a fresh quarantine/rescind and increments both counters on every restart; if that replayed clear fails, a healthy VF stays quarantined. Please retain a bounded last-resolved assignment/watermark per VF (or coalesce a replay before mutating state) and add a restart regression test.
| } | ||
|
|
||
| reader := bufio.NewReaderSize(f, vgpuSentinelMaxLineBytes) | ||
| for { |
There was a problem hiding this comment.
non-blocking: please thread ctx into scanSentinelLog and check cancellation between reads (and target scans). On the first pass after controller/process start every assignment begins at byte 0, so a host with many logs near the 50 MB rotation limit can keep Run inside this loop and hold the process-level errgroup.Wait well after shutdown was requested.
yummybomb
left a comment
There was a problem hiding this comment.
reviewed the change against hypeship/vendor-vfio-vgpu and traced the sentinel through guest output, host log scanning, durable health state, placement, and admission. the persistence rollback and assignment-identity handling are coherent, but the serial marker is not actually source-authenticated, and timed-out probes can overlap in the exact uninterruptible-ioctl case this code targets.
leaving the first finding as a blocker; github does not allow this account to request changes on its own PR.
|
|
||
| // Require a standalone guest-agent line so echoed marker text cannot count against a VF. | ||
| var ( | ||
| vgpuSentinelFailedPattern = regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} \[guest-agent\] (HYPEMAN-GPU-INIT-FAILED ts=\S+ nvrm="NVRM: [^"\r\n]*RmInitAdapter failed![^"\r\n]*")\r?\n?$`) |
There was a problem hiding this comment.
blocker: a standalone line does not establish that the guest agent wrote it. In exec mode the customer entrypoint's stdout/stderr is wired directly to the same serial console (lib/system/init/mode_exec.go:132-133), so an ordinary workload can print a complete line matching this regex without root or /dev/kmsg access. Repeating that across assignments can quarantine healthy VFs, and the runbook's statement that only a root guest can forge this is therefore incorrect. Please carry the report over a channel whose source the host can distinguish (for example a dedicated guest-agent RPC/transport), or otherwise authenticate the source; matching the log shape is not sufficient here.
| case err := <-done: | ||
| return err | ||
| case <-timer.C: | ||
| _ = cmd.Process.Kill() |
There was a problem hiding this comment.
this bounds how long the caller waits, but not the attempt's lifetime. If nvidia-smi is stuck in the uninterruptible ioctl described above, Kill does not make cmd.Wait return; the process and waiter goroutine remain live while probeGPUInitUntil launches another attempt after 15s. The 10-minute loop can therefore accumulate many concurrent stuck probes on the VF. Can we retain the in-flight attempt and avoid launching another until it is reaped (or stop retrying once an attempt cannot be killed), so there is at most one outstanding nvidia-smi?
88f3c3d to
b901282
Compare
b901282 to
1fd824a
Compare
a805d59 to
aa66cd3
Compare
The guest agent watches /dev/kmsg for kernel-facility NVRM RmInitAdapter-failed records and emits a standalone HYPEMAN-GPU-INIT-FAILED marker (repeated so printk splits cannot lose it), and probes driver init at boot with nvidia-smi -L when present, emitting a terminal HYPEMAN-GPU-INIT-OK on success. A hung probe attempt is bounded to 30 seconds. A host sentinel controller tails each vendor VFIO instance's app.log, matches only complete standalone markers, revalidates the assignment before reporting, and feeds failures and successes into the VF health store. Start archives the previous boot's app log before persisting a new assignment so stale markers cannot count against the new VF, and create allocates the vGPU immediately before metadata persistence to minimize the unpersisted window. vGPU reconciliation preserves assignments when hypervisor liveness is uncertain and records the condition in logs and a metric. GPU.md gains the detection and sentinel documentation.
… scan Markers share the serial console with workload stdout, so a bare log line does not establish that the guest agent wrote it: a workload could print an exact FAILED line to quarantine its healthy VF, or an OK line to clear a real failure record. The guest agent now serves its observed GPU init state over the existing vsock gRPC channel (GetGPUInitStatus), and the sentinel only acts on a marker the guest agent corroborates. Markers from instances whose agent is unreachable (e.g. already stopped) are skipped and logged; a wedged VF still convicts through its next assignment. Also fixes in the same scan path: - A clear whose persist fails is remembered on the tail and retried on later scans (and once more before a reassignment replaces the tail), since the once-per-boot OK marker is already consumed by then. - Only the latest marker in a pass is dispatched, so a FAILED with a later OK - a resolved pair replayed after a hypeman restart - is not recorded again. - When copy-truncate rotation moves unread bytes to the .1 backup, the scanner reads the backup remainder before restarting at offset zero, instead of dropping unseen markers. - The guest probe reaps a timed-out nvidia-smi before returning, so a probe stuck in an uninterruptible ioctl cannot accumulate processes.
…rkers The sentinel now polls GetGPUInitStatus over vsock for every vendor VFIO instance on its 5s tick and acts on the guest agent's reported state directly. The serial-console markers, the log scanner, and its offset, rotation, replay, and pending-clear bookkeeping are removed; the health store's per-assignment dedup makes polling idempotent and a failed persist retries on the next tick. The response carries the NVRM failure line so quarantine logs keep their diagnostic detail.
1084aef to
b23a654
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b23a654. Configure here.
A stopped or standby instance whose vGPU release failed retains its assignment metadata, so the sentinel polled it forever: a permanent rpc_error every tick, and QEMU vsock dials by guest CID alone, so a reused CID could reach an unrelated live guest. Gate targets on the control socket, the same no-VMM signal deriveState uses. Also hold polling until vendor-VFIO discovery is confirmed instead of scanning while discovery errors are still being retried.

Summary
Top half of the wedged-VF work, stacked on #462 (the VF health store, placement exclusion, admission, and
/resourcesfields). This PR adds the detection path that feeds that store./dev/kmsgfor kernel-facilityNVRM: ... RmInitAdapter failed!records and exposes the current GPU initialization state overGetGPUInitStatusvia vsock.Safety and failure handling
nvidia-smiattempt is killed after 30 seconds. The guest agent waits for that process to be reaped before retrying, so an attempt stuck in uninterruptible I/O cannot accumulate concurrent probes; the independent kmsg watcher still reports the underlying init failure.Observability
hypeman_instances_vgpu_sentinel_init_failures_totalhypeman_instances_vgpu_sentinel_quarantines_totalhypeman_instances_vgpu_sentinel_checks_totalbyresult(ok,failed,unknown,rpc_error,unsupported_agent, orlist_error)hypeman_instances_vgpu_quarantined_vfshypeman_instances_vgpu_vf_health_store_unavailablehypeman_instances_vgpu_reconcile_liveness_uncertain_totallib/devices/GPU.mdgains the detection and sentinel documentation.Out of scope
An operator force-cycle endpoint. Recovery remains the documented manual DCGM quiesce, SR-IOV cycle, state edit, restart, and verification flow.
Testing
Passed:
The full
lib/instancesrace suite did not complete cleanly locally because its integration tests require additional host networking and image tooling and surfaced unrelated dependency races.The underlying wedge signal and manual recovery sequence were previously validated on L40S hardware. A live end-to-end run of the guest watcher and host controller is still required before merge, including two failed assignments and the success path.
Note
Medium Risk
Changes vGPU placement safety, automatic VF quarantine, and create/reconcile timing on GPU hosts; incorrect guest signals or assignment confirmation could remove capacity, though running instances are not detached.
Overview
Adds an automatic detection path for wedged vendor-VFIO virtual functions: the guest agent watches
/dev/kmsgfor kernelNVRMRmInitAdapter failed!lines (and optionally probes withnvidia-smi -L), exposesGetGPUInitStatusover vsock, and the newVGPUSentinelControllerpolls running vendor-VFIO instances every 5s (up to 64 concurrent) to record failures and successes into the VF health store from #462.Host wiring: Wire injects
VGPUSentinelControllerandmainstarts it alongside other background controllers. Create path now allocates the vGPU immediately before metadata persistence so assignments are less likely to exist only in memory.VF health store: Adds
RepairVFHealthStore(used once per sentinel tick); failure/success reports no longer auto-retry a failed persist on every call—they fail closed until repair succeeds. vGPU reconcile treats ambiguous hypervisor liveness as “preserve assignment” with a warning andhypeman_instances_vgpu_reconcile_liveness_uncertain_total.Docs/metrics:
GPU.mddocuments sentinel behavior, metrics (sentinel_checks_total, quarantine gauges, store-unavailable), and tightened error text for failed-create retention stubs.Reviewed by Cursor Bugbot for commit b23a654. Bugbot is set up for automated code reviews on this repo. Configure here.