Repository navigation
Add gpu-bug-report executor for on-demand NVIDIA bug reports - #638
ankitjoju-google merged 8 commits into
Conversation
d334b19 to
e3c8f14
Compare
Adds a self-contained, cgo-free binary shipped in the device plugin image that watches its own Node and, when the cloud.google.com/trigger-gpu-diagnostics label is set, runs the host nvidia-bug-report.sh (via chroot), stores the report under /home/kubernetes/bin/nvidia-bug-reports, publishes the result in the cloud.google.com/gpu-diagnostics-output annotation, and emits a Node Event. The existing nvidia-gpu-device-plugin binary is unchanged.
e3c8f14 to
87b7d65
Compare
- Stage nvidia-bug-report.sh execution and TMPDIR inside a dedicated .work subdirectory under ReportsDir and move the completed report into ReportsDir on completion, removing cleanTempFiles filename pattern matching. - Always update StatusAnnotation alongside OutputAnnotation on both manual and automatic runs to prevent status/output drift when an automatic run is followed by a manual run. - Decouple TriggerLabel removal, StatusAnnotation CAS update, and OutputAnnotation publication on CAS conflict so a mid-run StatusAnnotation change during a coalesced run does not leave TriggerLabel unremoved or block OutputAnnotation. - Replace the custom kick channel with k8s.io/client-go/util/workqueue (RateLimitingInterface) and return transient Get/Patch errors from reconcile/recoverStartup for rate-limited retries.
…lict Track wantStatus separately from hasExpectedStatus in pendingResult so that if StatusAnnotation changes mid-run (causing a CAS conflict) and the fallback outputOnlyOps patch fails with a transient API error, the subsequent workqueue retry only patches OutputAnnotation rather than unconditionally overwriting StatusAnnotation.
|
|
Sounds good! Renamed all four labels/annotations and the Node event reasons to variations of |
| noAnnotationsMap: len(n.Annotations) == 0 && !wantAuto, | ||
| } | ||
| if err := c.finishCollection(ctx, p); err != nil { | ||
| // Use a bounded cleanup context so that if ctx was canceled by SIGTERM during collection, |
There was a problem hiding this comment.
Switching finishCollection to context.WithoutCancel(ctx) ensures StatusAnnotation transitions out of "in-progress" (-> "failed: interrupted") on SIGTERM so the maintenance handler does not block. However, p.wantManual is still unconditionally set to wantManual:
- If a DaemonSet rolling update or pod eviction sends
SIGTERMwhile a manual (or coalesced) collection is running,Collector.Runaborts withReasonInterrupted, andfinishCollectionremovesTriggerLabelfrom the Node. When the replacement pod starts moments later, the manual trigger is gone and never fulfilled. - Additionally, unconditionally wrapping
finishCtxwith a5stimeout even whenctx.Err() == nilcaps total API requests (Patch+Getconflict retries) during normal operation to 5 seconds total.
There was a problem hiding this comment.
Good catch! Updated in c10849b:
pending.wantManualis nowwantManual && res.Reason != ReasonInterrupted, so when a run is interrupted bySIGTERM,finishCollectionstill transitionscloud.google.com/gpu-report-statusto"failed: interrupted"while leavingcloud.google.com/trigger-gpu-reporton the Node for the replacement pod to fulfill.- We now only wrap
finishCtxin the 5scontext.WithoutCancel(ctx)deadline whenfinishCtx.Err() != nil, so normal runs aren't capped to 5s.
There was a problem hiding this comment.
Follow-up: I tested c10849b end-to-end on A4X Max (GB300, arm64) nodes:
- Pod deleted (SIGTERM) 4s into a manual run: the node went to
gpu-report-status: failed: interrupted, with output{"status":"failed","reason":"interrupted","trigger":"manual","complete":false}.cloud.google.com/trigger-gpu-report=truewas still on the Node after the old pod exited. The replacement pod picked up the label, finished withcompletedin 20.9s, and removed the label. - That run also showed a separate bug: the old pod's
GPUReportFailed"interrupted" event never reached the API server. The broadcaster writes events on a background goroutine, andShutdown()only waits until they are queued, so the process exited first. Fixed in d4ebdf5:mainnow tracks in-flight event sink writes and waits for them (bounded to 5s) before exiting. Unit tests cover the flush case and the hung-API-server timeout.
leiyiz
left a comment
There was a problem hiding this comment.
I think we can submit probably. it's your call i don't think they are too big of issues.
The event broadcaster writes events on a background goroutine and Shutdown only waits for them to be queued, so the GPUReportFailed "interrupted" event recorded on SIGTERM was dropped when the process exited. Track in-flight sink writes and wait (bounded to 5s) for them before exiting.
Adds a self-contained, cgo-free binary,
gpu-bug-report, that ships in the existing device plugin image and runs as a second container in thenvidia-gpu-device-pluginDaemonSet pod. It watches its own Node and, on request (either manually via a Node label or automatically via the Maintenance Handler status annotation handshake), executes the host'snvidia-bug-report.shand publishes the result metadata on the Node. The existingnvidia-gpu-device-pluginbinary is untouched.Supported Features
On-Demand Manual Trigger via Node Label (
pkg/gpu/nvidia/diagnostics/controller.go)cloud.google.com/trigger-gpu-report(any value except"false") on a GPU node triggers a bug-report collection on that node.testop on the observed label value. If a patch precondition fails, the controller fetches the live Node and compares the currentTriggerLabelandStatusAnnotationvalues against the expectedtestvalues so only the non-conflicting fields are updated on retry.Automatic Maintenance Handler Handshake (
pkg/gpu/nvidia/diagnostics/controller.go)cloud.google.com/gpu-report-status):collect→in-progress: Acknowledges an automatic request from the Maintenance Handler via an RFC 6902test+replaceCAS patch.in-progress→completedorfailed: <reason>: Publishes the terminal status andcloud.google.com/gpu-report-outputatomically when collection finishes (on both manual and automatic runs so status and output annotations never drift).cloud.google.com/disable-gpu-report):"false"). When active, automaticcollectrequests are immediately transitioned tofailed: disabled(with a Warning Event) without running the script, while manual triggers remain allowed.recoverStartup):cloud.google.com/gpu-report-statusis left atin-progressfrom a prior container crash or OOM kill, the controller CAS-patches it tofailed: interruptedand emits a Warning Event so the Maintenance Handler never waits for a dead run.k8s.io/client-go/util/workqueue(RateLimitingInterface) worker that always reconciles against a freshGETof the Node. Concurrent manual and automatic requests share a single collection run, and transientGet/Patcherrors retry with exponential backoff viapendingResultwithout re-runningnvidia-bug-report.sh.Host-Isolated Execution from a Distroless Container (
pkg/gpu/nvidia/diagnostics/collector.go)nvidia-bug-report.shviasyscall.SysProcAttr{Chroot: <host-root>}, probing/home/kubernetes/bin/nvidia/bin,/opt/nvidia/bin, and/usr/binand automatically derivingPATHandLD_LIBRARY_PATH.<reports-dir>/.work($TMPDIR) and atomically renames only the finished report into/home/kubernetes/bin/nvidia-bug-reports/nvidia-bug-report-<UTC-timestamp>-<manual|automatic>.log.gz(0700permissions), removing.workafterwards. This directory is sibling to—never inside—/home/kubernetes/bin/nvidia, ensuring diagnostic reports are never mounted into user GPU workloads.Result Publishing & Observability (
pkg/gpu/nvidia/diagnostics/types.go)cloud.google.com/gpu-report-output:{ "status": "completed", "trigger": "manual", "filePath": "/home/kubernetes/bin/nvidia-bug-reports/nvidia-bug-report-20260930T221010Z-manual.log.gz", "fileSizeBytes": 558947, "complete": true, "durationSeconds": 6.739, "collectedAt": "2026-09-30T22:10:10Z" }Normal/GPUReportCompletedorWarning/GPUReportFailedevents on the Node (visible inkubectl describe node).Process, Log-Age, Disk & Memory Safety (
pkg/gpu/nvidia/diagnostics/collector.go)--max-log-age, default168h/ 7 days): Places a lightweightjournalctlwrapper inside<reports-dir>/.work/journalctlat the front ofPATHthat injects--since="<start - max-log-age>". Becausenvidia-bug-report.shscansjournalctl -btwice per boot without a time bound, cappingjournalctlto the last 7 days uses systemd's timestamp index to keep log scanning fast on long-lived nodes and prevents falsefailed: timeoutruns.60s) withSetpgid: trueand sendsSIGKILLto the entire process group (-pgid) so hungnvidia-smichildren cannot leak or hold/dev/nvidia*open.Dstate) afterSIGKILL, the collector marks itselfbusyand refuses further runs rather than accumulating stuck processes..log.gzoutput to verify both a clean gzipEOFand the"End of NVIDIA bug report log file"footer (sincenvidia-bug-report.shcan exit0even when subcommands fail or truncate). Partial reports on timeout are retained on disk with"complete": false.--max-reports=5) and removes the.workstaging directory after each run.TMPDIRto<reports-dir>/.workon disk sonvidia-debugdumptemporary archives do not allocate unreclaimabletmpfs(shmem) inside the container's cgroup.fsync+unix.Fadvise(..., unix.FADV_DONTNEED)on the finished report to immediately evict its page cache from the container's memory cgroup.stderrcapture to a 4 KiB ring buffer (tailBuffer) and callsdebug.FreeOSMemory()after each run.cmd/gpu_bug_report/deps_test.go): Built statically withCGO_ENABLED=0; unit test enforces that the binary never importsgoogle.golang.org/grpc,github.com/NVIDIA/*(NVML), orruntime/cgo.Performance & Resource Benchmarks
Validated end-to-end on GKE (COS) across four GPU machine families (x86_64 and ARM64 baremetal):
.log.gz/ Raw)current/anon)memory.peak/anon)g4-standard-48g4-standard-384a4-highgpu-8ga4x-maxgpu-4g-metal24.1 MiBkernel)*Note:
g4-standard-384steady-state memory was measured prior to switching from the stdlib HTTP client toclient-go(+3.5–4 MiB idleanonoverhead).Key Performance Observations
6.7svs6.4–8.0s), 8× B200 with NVLink 5 completes in13.3–13.6s, and 4× GB300 (a4x-maxgpu-4g-metalARM64 baremetal) completes in17.6–18.8s—all well within the default60stimeout.a4x-maxgpu-4g-metalnode (325.6 MBjournal),--max-log-age=168hcleanly excluded boot logs older than 7 days via systemd's timestamp index.nvidia-persistencedon Multi-GPU NVLink Nodes (8× B200):nvidia-persistencedactive (standard on GKE GPU nodes with the device plugin), 8× B200 collection takes 13.3s.Persistence Mode: Disabled), collection took 97.0s (~7.3× slower) because each sequentialnvidia-smi/nvidia-debugdumpinvocation (~9 separate calls innvidia-bug-report.sh) had to cold-initialize all 8 GPUs, load the GSP firmware, and retrain NVLink on every command:nvidia-smi -q:10.39s(cold) →0.73s(with persistence mode, 14× faster)nvidia-smi nvlink --status:9.67s→0.20s(48× faster)nvidia-smi nvlink --errors:9.68s→0.09s(100× faster)nvidia-smi topo -m:11.14s→1.60s(7× faster)64 KBPage Size) Kernel Overhead:TMPDIRoff/tmp(tmpfs) to<reports-dir>/.work: Eliminated 8.6–9.6 MiB of unreclaimableshmemcreated bynvidia-debugdumptemporary zip archives on 8-GPU nodes, with no measurable runtime penalty.fadvise64(POSIX_FADV_DONTNEED)after report completion: Evicts finished report page cache immediately, keeping post-run file cache at 64–140 KiB.a4x-maxgpu-4g-metal) Memory Limit Sizing (128 MiB): On 144-core, 34-NUMA-node Grace ARM64 baremetal nodes running a 64 KB page size kernel (KernelPageSize: 64 kB), unreclaimable cgroupkernelmemory (percpu: 7.3 MiB+ 64 KBpagetables: 3.7 MiB+kernel_stack: 0.8 MiB+slab: 0.9 MiB) reaches 24.1 MiB during script execution. Settinglimits.memory: 128Mi(while keepingrequests.memory: 10Mi) provides ample headroom above the ~37.5 MiBanon + kernelworking set.nvidia-smi -q, ECC/row-remapper states, VBIOS/GSP versions,/proc/driver/nvidia/gpus/*,lspci, kernel/NVRM logs, and a valid multi-GPUnvidia-debugdumpzip archive.Deployment Requirements (for GKE Manifest Integration)
/usr/bin/gpu-bug-report --host-root=/root,privileged: true,readOnlyRootFilesystem: true,GOMAXPROCS=1, with5mCPU /10Mimemory request and128Mimemory limit./mounted read-only at/root, and host/home/kubernetes/binmounted read-write at/root/home/kubernetes/bin(reusing the pod's existingroot-mountandkubernetes-binvolumes).hostPID: trueandhostNetwork: true(already enabled on the device plugin pod) sosystemctl status nvidia-persistencedand host hostname resolve from the host namespace;NODE_NAMEpassed via the downward API.gpu-device-plugin-saServiceAccount (get/watch/patchonnodes,create/patchonevents, and own-nodeValidatingAdmissionPolicy).