Detect and quarantine wedged vGPU VFs - #435
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
Detection moves from host-side matching of the raw NVRM kernel line to an explicit guest-to-host report: the guest agent watches /dev/kmsg for the driver's RmInitAdapter failure and emits a HYPEMAN-GPU-INIT-FAILED marker over the established sentinel-marker channel, which the controller convicts on. The raw kernel line is no longer matched, so detection now requires the guest agent; images without it do not report. Quarantine hardening in the same pass: - The conviction brake pauses instead of drops: a suppressed conviction leaves the tail open and the agent's re-emission retries it once the window clears. - A match on an already-quarantined VF is not a new conviction: no brake accounting, no metric, no wedge-count inflation on controller restarts. - Tails reset when the instance acquires a new assignment, so a finished tail from a previous boot or VF cannot suppress scanning the next one. - A vf-health state file that fails to load refuses mutations (and retries the load) instead of letting the next conviction clobber every previously persisted quarantine. - Oversized unterminated log lines are skipped instead of re-buffered on every scan. - The controller idles on hosts without the vendor VFIO framework. GPU.md: DCGM quiesce is now an ordered step of the recovery sequence, and clearing vf-health.json documents the immediate-restart requirement.
Review fixes on the quarantine layer: - start now archives the previous boot's serial log before persisting the new vGPU assignment. The sentinel keys its tail on the assignment epoch, so the old ordering let a scan replay the previous boot's wedge report against the freshly assigned VF. A failed archive is fatal for GPU instances instead of a warning. - placement and profile availability refuse to run when the VF health state file exists but cannot be loaded, instead of treating the empty in-memory set as healthy and returning every quarantined VF to rotation. The load is retried on each attempt, so a repaired file self-heals. - a conviction whose persist fails is rolled back from memory: keeping it made the next report look like a repeat conviction, ending retries with nothing on disk. - the marker pattern requires the full shape through the quoted NVRM payload; a truncated or payload-less marker echoed by an exec command no longer matches. - unreadable instance metadata in the sentinel target listing logs a warning instead of silently shrinking detection coverage. - GPU.md recovery runbook unwound a circularity: placement excludes quarantined VFs and there is no VF-pin API, so the entry is cleared before the verification boot; the sentinel re-quarantines automatically if the cycle did not cure the VF.
The /dev/kmsg record priority encodes facility*8+level. Kernel printk is always facility 0 and the kernel assigns userspace writers LOG_USER or higher (a facility-0 prefix is coerced to LOG_USER, verified on a live 6.12 kernel), so requiring facility 0 makes in-guest forgery of the report impossible, matching the intended kernel-records-only semantics. Also retry a failed /dev/kmsg open instead of permanently disabling the watcher for the guest's lifetime.
The scan buffered each complete line whole, so guest console output could make the controller allocate line-sized buffers every pass; the 64KB cap only kept an unterminated tail from being re-read, and once such a line was skipped its late-arriving tail was parsed as a fresh line. Read through a fixed-size buffer instead: a line that overflows it cannot be a marker, so it is discarded — across scans if its newline has not arrived — without ever being held in memory, and its tail can no longer replay a marker. Rotation resets the skip state with the offset.
When the persisted state file fails to load, quarantine mutations are refused and vGPU placement fails closed, but the quarantined-VFs gauge reads zero from the empty in-memory set — exactly when quarantines exist and are unreadable. Export the load-failure state as its own gauge so the condition is alertable.
The serial console is a shared byte stream: kernel printk bypasses the tty buffer and can land mid-marker, and on a wedged VF the kernel is emitting NVRM errors exactly when the agent reports. A corrupted copy does not match the host's full-shape scan (deliberately, so echoed commands cannot convict), which delayed the report to the next 30s re-emission. Emit each report as three identical lines sharing one ts; the host convicts on the first intact copy and ignores the rest.
Auto-conviction no longer pauses on a burst. Systemic non-wedge init failures (e.g. a driver-mismatch image rollout) are expected to be caught on a test host before reaching production, and the convictions counter remains the alerting signal if one gets through; the brake was extra state and logic guarding against a case the rollout process already covers. Quarantine still only removes capacity and never touches instances, and the store's fail-closed load handling is unchanged.
Each /dev/kmsg read returns exactly one record and fails with EINVAL — without consuming the record — when the buffer is smaller. Records run up to CONSOLE_EXT_LOG_MAX (8 KiB), so bufio's default 4 KiB buffer wedged the watcher on the first oversized record: every reopen replayed the ring into the same record, silently losing all detection behind it. Size the buffer to the kernel's record bound and log non-EPIPE scan errors so a wedge is visible instead of silent.
A quarantine is only real once it is on disk, but the persist renamed without syncing the file or directory, so a host crash right after a conviction could silently drop it. Sync the temp file before the rename and the directory after, and deduplicate the sorted record listing.
The matched line is guest-controlled console bytes up to the 64 KiB line cap; logging and persisting it verbatim put up to that much guest output in error logs and vf-health.json. Keep just the marker match.
ClearVFQuarantine had no callers — the runbook documents editing vf-health.json directly — and WedgeCount was always 1. QuarantineVF's record return and IsVFQuarantined only served tests, and the sentinel's isQuarantined pre-check duplicated what QuarantineVF already reports via existed. The hostFramework indirection wrapped a single DiscoverVGPU call nothing injected.
A transient DiscoverVGPU error at startup failed open permanently, leaving a host that is not vendor VFIO scanning instance metadata every tick for the process lifetime. Keep failing open while discovery errors, but retry the probe each tick so the controller exits once the host is known not to be vendor VFIO.
A conviction used the metadata snapshot taken before the log read, so a stop/start racing the scan could quarantine the VF the instance no longer holds. Reload the assignment and convict only when the VF and assignment epoch are unchanged; start's archive-before-persist ordering guarantees a marker read under an unchanged epoch belongs to that epoch's VF. On any other outcome the tail stays open and the next scan picks up the current assignment. Target listing used the lenient metadata walk, which drops entries whose stat fails without ever reaching the unreadable-metadata warning. Walk strictly first so a stat failure names the instance losing detection coverage, then fall back to the lenient walk so the readable rest keeps being scanned.
The remediation section required an empty card but never said how to get one: the quarantine only deprioritizes the card, so under capacity pressure new placements keep refilling its healthy VFs. Document the manual cordon (pre-quarantining the card's VFs in vf-health.json plus a hypeman restart), why running instances are untouched and drain through their normal lifecycle, and how to monitor and uncordon.
The drain section restated the quarantine's placement-exclusion semantics and the vf-health.json edit-plus-restart rule, and gave the uncordon instruction twice. Hoist the edit rule into one paragraph both the cordon and clear steps reference, and let the detection paragraph own the exclusion semantics.
The pre-conviction recheck treated a missing assignment like a changed one and skipped the quarantine. A stop or delete landing between the metadata snapshot and the recheck releases the assignment but cannot un-wedge the VF, and the archive-before-persist ordering already ties a marker read under this epoch's snapshot to this epoch's VF, so the conviction is valid. Skip only when a different assignment exists.
55a0d6f to
ee1b160
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 ee1b160. Configure here.
| for _, profile := range creatable { | ||
| profilesByType[profile.TypeName] = profile | ||
| if !vf.Allocated { | ||
| if !vf.Allocated && !bad { |
There was a problem hiding this comment.
Admission ignores quarantined VF capacity
Medium Severity
Quarantine correctly lowers profile Available and blocks placement, but GPU admission still uses TotalSlots - UsedSlots from GetGPUStatus, which only counts allocated VFs. Free quarantined VFs stay in the available pool, and a health-store load failure only clears profiles while slot math still allows creates. Admission can therefore accept GPU work that placement must reject—especially after victims stop and quarantines remain.
Triggered by learned rule: Admission capacity accounting must not silently undercount on errors
Reviewed by Cursor Bugbot for commit ee1b160. Configure here.


Summary
Killing the process holding a vendor VFIO VF during guest driver init (~8s window) silently corrupts the VF: every later boot on it fails while sysfs, NVML, and the vGPU plugin all look healthy. #321 removed hypeman's own kill triggers; this layer detects the wedges external kills (OOM, operator, deploy restarts) still cause, and takes the VF out of placement.
Detection — guest agent report, no lifecycle change. On a wedged VF the guest driver is present, trying, and failing: its kernel loops
NVRM: ... RmInitAdapter failed!in the guest kernel log. The guest agent watches/dev/kmsgfor that line and reports it as aHYPEMAN-GPU-INIT-FAILEDmarker — the same guest→host channel as the existingHYPEMAN-*markers, landing in the instance'slogs/app.log. A newVGPUSentinelControllertails that file for every instance holding a vendor VFIO assignment and convicts on the marker; the full marker shape is required since a bare token could appear in echoed exec command lines. The agent only watches when an NVIDIA PCI function is present, throttles re-emission to one per 30s, and matches the full kernel-line shape, never the driver-build-specific(0x22:0x65:884)tuple. A no-driver image never produces the kernel line and can never be convicted. An image that skips the guest agent does not report — v1 accepts that coverage bound; the raw kernel line still reachesapp.logfor manual diagnosis.Quarantine. Convictions persist to
<data-dir>/gpu/vf-health.json(keyed by VF address, survives restarts) under the placement lock. One wedge produces one record no matter how many victim boots or controller restarts report it. The store fails closed on an unreadable state file: mutations are refused, and vGPU placement and advertised availability are disabled (with load retried on each attempt) rather than treating the empty in-memory set as healthy and returning quarantined VFs to rotation. A conviction whose persist fails is rolled back from memory so the next report retries it instead of reading as a repeat. Start archives the previous boot's serial log before persisting a new assignment (fatal on failure for GPU instances), so a scan can never replay the prior boot's report against a freshly assigned VF. There is no rate limit on convictions: a systemic non-wedge init failure (e.g. a driver-mismatch rollout) emits the same line on every VF and would quarantine the whole host, so such changes are validated on a test host first and the convictions counter is the alerting signal if one gets through.Placement.
Observability. Conviction logs at error level;
hypeman_instances_vgpu_sentinel_convictions_total, thehypeman_instances_vgpu_quarantined_vfsgauge, andhypeman_instances_vgpu_vf_health_store_unavailable(1 while the persisted state file cannot be loaded and placement is failing closed). The controller idles on hosts without the vendor VFIO framework.GPU.md documents the new behavior and the manual recovery flow (DCGM quiesce → SR-IOV cycle → restart services → verification boot → clear the VF's entry in
vf-health.json→ restart hypeman immediately).Out of scope (follow-up layer)
An operator force-cycle endpoint (DCGM quiesce →
sriov-manage -d/-e→ verification boot → clear quarantine). Until then recovery stays the documented manual runbook.Testing
go test -race ./lib/devices ./lib/paths ./lib/providers ./lib/system/guest_agentand the vGPU-relatedlib/instancestests pass locally;go vetclean. The fulllib/instancessuite fails only on environment-dependent VM/network tests, identically on the unmodified base.app.log) and the recovery sequence were validated on real L40S hardware by deliberately wedging VFs; the guest-agent watcher and the controller have not yet run against a live wedge end-to-end — that run should happen on the dev GPU host before this merges.Note
High Risk
Changes vGPU placement, advertised GPU capacity, and can fail-closed disable all vendor-VFIO placement if health state is unreadable. False or burst convictions can drain host GPU capacity until operators cycle SR-IOV.
Overview
Automatically detects wedged vendor-VFIO vGPU VFs (guest
RmInitAdaptertimeout with a healthy sysfs/plugin) and takes them out of placement so one bad VF no longer looks like a host-wide GPU outage.The guest agent tails
/dev/kmsgon NVIDIA guests and emits aHYPEMAN-GPU-INIT-FAILEDmarker. A newVGPUSentinelControllerscans instance serial logs, quarantines the assigned VF ingpu/vf-health.json(durable, fail-closed if the file is unreadable), and excludes those VFs from selection and advertised availability. Parent GPUs with quarantined VFs become overflow-only; free-VF pick on the chosen card is randomized instead of lowest-address.Start archives the previous boot’s app log before a new GPU assignment (fatal for GPU instances) so a prior marker cannot convict a fresh VF. Metrics:
hypeman_instances_vgpu_sentinel_convictions_total,hypeman_instances_vgpu_quarantined_vfs,hypeman_instances_vgpu_vf_health_store_unavailable. Recovery remains the documented SR-IOV cycle plus clearing the health file.Reviewed by Cursor Bugbot for commit ee1b160. Bugbot is set up for automated code reviews on this repo. Configure here.