feat(nvidia_bug_report): extract Xid/SXid GPU error events
Parse Xid and SXid entries from the kernel log into GPU error events with severity and the decoded failure reason. See ADL-055. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e7b8a8badc
commit
a430a8c46f
@@ -0,0 +1,77 @@
|
||||
# Analyzing nvidia-bug-report.log.gz — agent instructions
|
||||
|
||||
Short checklist for an AI agent (or a human) given a `nvidia-bug-report-*.log.gz` and
|
||||
asked "what's wrong with this box."
|
||||
|
||||
## 1. Structured inventory pass (LOGPile)
|
||||
|
||||
Run it through LOGPile's `nvidia_bug_report` parser first — it gives board/CPU/memory/
|
||||
GPU/NIC/PSU inventory as structured JSON (`internal/parser/vendors/nvidia_bug_report/`).
|
||||
As of this session the decompressor no longer hard-caps at 50MB (streams with a
|
||||
gzip-bomb ratio guard instead — see `internal/parser/archive.go`), so large dumps
|
||||
come through whole. Known gap: PSU `present` flag was buggy (fixed), sensors/FRU are
|
||||
legitimately empty for this format (no `ipmitool sdr`/`fru` in host-side bug reports).
|
||||
|
||||
## 2. Grep passes on the raw decompressed log
|
||||
|
||||
In priority order:
|
||||
|
||||
1. `Xid (PCI:` — GPU error reports. Note **all** `PCI:xxxx` addresses and Xid codes,
|
||||
count occurrences, get first/last timestamp per GPU.
|
||||
2. `SXid` — NVSwitch (fabric manager) errors on HGX/NVSwitch systems.
|
||||
3. `GPU recovery action changed` — tracks a GPU through `None → Drain and Reset →
|
||||
GPU Reset Required` — a GPU stuck at "Reset Required" for a long span is a live
|
||||
incident, not a transient blip.
|
||||
4. `fell off the bus`, `Uncorrectable`, `Double Bit ECC`, `Row Remap` — hardware
|
||||
memory/PCIe failures.
|
||||
5. Thermal/power: `thermal slowdown`, `power slowdown`, `Reason: SW\|HW`.
|
||||
6. `nvidia-smi -q` section per GPU: `ECC Errors`, `Retired Pages`, `Remapped Rows`,
|
||||
`GPU Recovery Action` — cross-check current (not historical) state at capture time.
|
||||
7. `segfault`, kernel panics, `Call Trace`, `MCE`/machine-check in dmesg/journalctl
|
||||
excerpts.
|
||||
|
||||
Cross-reference: an Xid burst that stops before the `nvidia-smi -q` snapshot and shows
|
||||
`GPU Recovery Action: None` + clean ECC counters at the end means the box already
|
||||
self-recovered (e.g. via reboot) — still worth flagging, but not an active fire.
|
||||
|
||||
## 3. Classify each Xid, don't just list it
|
||||
|
||||
Look it up in NVIDIA's Xid catalog (link below) before writing a verdict. Same code
|
||||
can mean different things depending on class:
|
||||
- App-level GPU exception (bad kernel launch, illegal memory access) — usually not
|
||||
hardware-actionable, correlate with the offending `pid`/process name in the log.
|
||||
- NVLink/fabric fault (`MSE Degraded`, link training failures) — check
|
||||
`knvlink*` messages, Fabric Manager logs — often requires GPU/node reset, can
|
||||
recur → RMA candidate if repeated.
|
||||
- ECC/memory retirement (uncorrectable DRAM, row-remap failures) — RMA candidate,
|
||||
check `Retired Pages` / `Remapped Rows` counts.
|
||||
- `XID 79` (GPU fell off the bus) — drain the node, treat as hardware failure.
|
||||
|
||||
## 4. Escalation bar
|
||||
|
||||
Recommend RMA/hardware escalation when: repeated fatal Xid/SXid on the same GPU across
|
||||
multiple days, non-zero uncorrectable ECC or remap failures, or a GPU that required
|
||||
manual reset more than once. Otherwise: flag as informational / correlate with workload.
|
||||
|
||||
## What already exists online (recipes found 2026-08-26)
|
||||
|
||||
- **Lambda Labs — `check-nvidia-bug-report.sh`**: an actual automated scanner for this
|
||||
exact file, open source. Clones and runs against the extracted report; flags Xid
|
||||
errors, thermal/power slowdown, segfaults, CPU throttling, "fell off the bus",
|
||||
RmInit failures. This is the closest thing to a ready-made "recipe" — worth using
|
||||
directly or mining its grep patterns.
|
||||
https://github.com/lambdal-support/lambda-public-tools (see `check-nvidia-bug-report/`)
|
||||
Guide: [Lambda Docs — Using the nvidia-bug-report.log file to troubleshoot your system](https://docs.lambda.ai/education/linux-usage/using-the-nvidia-bug-report.log-file-to-troubleshoot-your-system/)
|
||||
- **NVIDIA — GPU Debug Guidelines**: official step-by-step (collect → `dcgmi diag` →
|
||||
classify Xid → escalate/RMA criteria), the authoritative human-oriented recipe.
|
||||
https://docs.nvidia.com/deploy/gpu-debug-guidelines/index.html
|
||||
- **NVIDIA — Xid Errors reference**: the Xid code catalog itself (meanings, no fixed
|
||||
hardware/software verdict per code — context-dependent).
|
||||
https://docs.nvidia.com/deploy/xid-errors/introduction.html
|
||||
- **AWS re:Post — Troubleshoot Xid errors in NVIDIA GPU-accelerated instances**:
|
||||
cloud-instance-flavored version of the same workflow.
|
||||
https://repost.aws/knowledge-center/ec2-linux-troubleshoot-xid-errors
|
||||
|
||||
No public "AI agent prompt/recipe" specifically for feeding this file to an LLM was
|
||||
found — the closest things are the shell-script scanners above; an agent should treat
|
||||
their grep pattern lists as a starting checklist (§2 above already folds them in).
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
// syslogLineRegex matches the kernel/journal line prefix nvidia-bug-report.sh
|
||||
// captures from dmesg/journalctl: "Aug 24 13:52:04 hostname kernel: <message>".
|
||||
var syslogLineRegex = regexp.MustCompile(`^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+(.+)$`)
|
||||
|
||||
// xidRegex matches NVRM Xid (GPU) and SXid (NVSwitch/fabric manager) error
|
||||
// reports, e.g. "NVRM: Xid (PCI:0000:dc:00): 31, pid=..." or
|
||||
// "NVRM: Xid (PCI:0000:1a:00): 150, MSE Degraded Fatal ...".
|
||||
var xidRegex = regexp.MustCompile(`NVRM:\s*(S?Xid)\s*\(PCI:([0-9a-fA-F:.]+)\):\s*(\d+),\s*(.*)$`)
|
||||
|
||||
// nvlinkRxDetectFailRegex matches a specific NVLink topology-discovery retry
|
||||
// message that was observed to flood the log 100k+ times in a single burst
|
||||
// around a real NVLink fault; individual lines carry no extra information
|
||||
// beyond "it happened again", so they are aggregated into one summary event
|
||||
// instead of one event per line.
|
||||
var nvlinkRxDetectFailRegex = regexp.MustCompile(`knvlink\w*RxDetect\w*: Failed to update Rx Detect Link mask`)
|
||||
|
||||
// fellOffBusRegex matches the classic "GPU fell off/has fallen off the bus"
|
||||
// hardware fault for the (rare) case it's logged outside an Xid line — the
|
||||
// same fault is usually also reported as Xid 79, already covered above.
|
||||
var fellOffBusRegex = regexp.MustCompile(`(?i)fell off the bus|fallen off the bus`)
|
||||
|
||||
// hardwareLeaningXidCodes are Xid codes NVIDIA's GPU Debug Guidelines
|
||||
// (https://docs.nvidia.com/deploy/gpu-debug-guidelines/index.html) and Xid
|
||||
// catalog associate with likely hardware failures (ECC/memory retirement,
|
||||
// NVLink/fabric faults, GPU falling off the bus) as opposed to an
|
||||
// application-triggered exception. This is a heuristic severity hint for
|
||||
// triage, not a definitive verdict — the actual cause is always
|
||||
// context-dependent; see bible-local/docs/nvidia-bug-report-analysis.md.
|
||||
var hardwareLeaningXidCodes = map[string]bool{
|
||||
"48": true, "63": true, "64": true, "74": true, "79": true,
|
||||
"92": true, "94": true, "95": true, "119": true, "120": true,
|
||||
"122": true, "123": true, "134": true, "137": true, "150": true, "154": true, "155": true,
|
||||
}
|
||||
|
||||
// gpuFault accumulates the worst status seen for one GPU's PCI address, so
|
||||
// the fault can be written back onto that GPU's own inventory record — an
|
||||
// event in the Event Logs table is easy to miss; a GPU card showing
|
||||
// "Critical" is not.
|
||||
type gpuFault struct {
|
||||
severity models.Severity
|
||||
lastTS time.Time
|
||||
description string
|
||||
}
|
||||
|
||||
func (f *gpuFault) absorb(severity models.Severity, ts time.Time, description string) {
|
||||
if severity == models.SeverityCritical || f.severity != models.SeverityCritical {
|
||||
f.severity = severity
|
||||
}
|
||||
if ts.After(f.lastTS) {
|
||||
f.lastTS = ts
|
||||
f.description = description
|
||||
}
|
||||
}
|
||||
|
||||
// parseXidAndFaultEvents scans the raw bug report for Xid/SXid GPU error
|
||||
// reports and a small set of other well-known hardware fault patterns,
|
||||
// records them as events, and writes the worst fault seen for each GPU back
|
||||
// onto that GPU's own Status/ErrorDescription/StatusChangedAt fields so the
|
||||
// inventory record itself reflects the fault, not just the event log. This
|
||||
// does not attempt to classify every message — see
|
||||
// bible-local/docs/nvidia-bug-report-analysis.md for the full manual triage
|
||||
// checklist.
|
||||
func parseXidAndFaultEvents(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
|
||||
collectedYear := time.Now().Year()
|
||||
if !result.CollectedAt.IsZero() {
|
||||
collectedYear = result.CollectedAt.Year()
|
||||
}
|
||||
|
||||
faultsByPCI := make(map[string]*gpuFault)
|
||||
recordFault := func(pci string, severity models.Severity, ts time.Time, description string) {
|
||||
if pci == "" {
|
||||
return
|
||||
}
|
||||
f, ok := faultsByPCI[pci]
|
||||
if !ok {
|
||||
f = &gpuFault{}
|
||||
faultsByPCI[pci] = f
|
||||
}
|
||||
f.absorb(severity, ts, description)
|
||||
}
|
||||
|
||||
var nvlinkFloodCount int
|
||||
var nvlinkFloodFirst, nvlinkFloodLast time.Time
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
m := syslogLineRegex.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
timeStr, message := m[1], m[2]
|
||||
ts := parseSyslogTimestamp(timeStr, collectedYear, result.CollectedAt)
|
||||
|
||||
if xm := xidRegex.FindStringSubmatch(message); xm != nil {
|
||||
kind, pci, code, rest := xm[1], xm[2], xm[3], strings.TrimSpace(xm[4])
|
||||
|
||||
severity := models.SeverityWarning
|
||||
if hardwareLeaningXidCodes[code] || strings.Contains(rest, "GPU Reset Required") {
|
||||
severity = models.SeverityCritical
|
||||
}
|
||||
|
||||
eventType := "GPU Xid Error"
|
||||
if kind == "SXid" {
|
||||
eventType = "NVSwitch SXid Error"
|
||||
}
|
||||
description := fmt.Sprintf("%s %s reported on %s", kind, code, pci)
|
||||
|
||||
result.Events = append(result.Events, models.Event{
|
||||
Timestamp: ts,
|
||||
Source: "NVIDIA Driver",
|
||||
SensorType: "GPU",
|
||||
SensorName: pci,
|
||||
EventType: eventType,
|
||||
Severity: severity,
|
||||
Description: description,
|
||||
RawData: rest,
|
||||
})
|
||||
if kind == "Xid" {
|
||||
recordFault(pci, severity, ts, description+": "+rest)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if fellOffBusRegex.MatchString(message) {
|
||||
result.Events = append(result.Events, models.Event{
|
||||
Timestamp: ts,
|
||||
Source: "NVIDIA Driver",
|
||||
SensorType: "GPU",
|
||||
EventType: "GPU Fell Off The Bus",
|
||||
Severity: models.SeverityCritical,
|
||||
Description: "GPU fell off the PCIe bus",
|
||||
RawData: strings.TrimSpace(message),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if nvlinkRxDetectFailRegex.MatchString(message) {
|
||||
nvlinkFloodCount++
|
||||
// Track true chronological min/max, not scan order: a
|
||||
// nvidia-bug-report.sh dump concatenates multiple overlapping
|
||||
// log sources (e.g. dmesg -T and journalctl excerpts covering
|
||||
// different but overlapping windows), so later lines in the
|
||||
// file are not guaranteed to be later in time.
|
||||
if !ts.IsZero() {
|
||||
if nvlinkFloodFirst.IsZero() || ts.Before(nvlinkFloodFirst) {
|
||||
nvlinkFloodFirst = ts
|
||||
}
|
||||
if ts.After(nvlinkFloodLast) {
|
||||
nvlinkFloodLast = ts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nvlinkFloodCount > 0 {
|
||||
severity := models.SeverityWarning
|
||||
if nvlinkFloodCount > 100 {
|
||||
severity = models.SeverityCritical
|
||||
}
|
||||
result.Events = append(result.Events, models.Event{
|
||||
Timestamp: nvlinkFloodLast,
|
||||
Source: "NVIDIA Driver",
|
||||
SensorType: "GPU",
|
||||
EventType: "NVLink Rx Detect Failure Burst",
|
||||
Severity: severity,
|
||||
Description: fmt.Sprintf("NVLink Rx Detect Link mask update failed %d times between %s and %s", nvlinkFloodCount, nvlinkFloodFirst.Format(time.RFC3339), nvlinkFloodLast.Format(time.RFC3339)),
|
||||
})
|
||||
}
|
||||
|
||||
applyGPUFaultStatus(result, faultsByPCI)
|
||||
}
|
||||
|
||||
// applyGPUFaultStatus writes each GPU's worst known fault onto its own
|
||||
// Status/ErrorDescription/StatusChangedAt fields, matched by PCI address
|
||||
// (Xid lines report "0000:1a:00", GPU.BDF is "0000:1a:00.0" — compare on the
|
||||
// bus:device:slot prefix, ignoring the trailing PCI function digit).
|
||||
func applyGPUFaultStatus(result *models.AnalysisResult, faultsByPCI map[string]*gpuFault) {
|
||||
if len(faultsByPCI) == 0 || result.Hardware == nil {
|
||||
return
|
||||
}
|
||||
for i := range result.Hardware.GPUs {
|
||||
gpu := &result.Hardware.GPUs[i]
|
||||
base := gpu.BDF
|
||||
if idx := strings.LastIndex(base, "."); idx != -1 {
|
||||
base = base[:idx]
|
||||
}
|
||||
f, ok := faultsByPCI[base]
|
||||
if !ok || gpu.Status == "Excluded" {
|
||||
continue
|
||||
}
|
||||
gpu.Status = string(f.severity)
|
||||
gpu.ErrorDescription = f.description
|
||||
ts := f.lastTS
|
||||
gpu.StatusChangedAt = &ts
|
||||
}
|
||||
}
|
||||
|
||||
// parseSyslogTimestamp parses a syslog-style "Mon _2 15:04:05" timestamp
|
||||
// (no year) using collectedYear as the assumed year, then corrects for a
|
||||
// year rollover if that guess would put the event implausibly after the
|
||||
// report was collected (e.g. a December log line in a January report).
|
||||
func parseSyslogTimestamp(raw string, collectedYear int, collectedAt time.Time) time.Time {
|
||||
ts, err := parser.ParseInDefaultArchiveLocation("Jan _2 15:04:05 2006", fmt.Sprintf("%s %d", raw, collectedYear))
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if !collectedAt.IsZero() && ts.After(collectedAt.Add(48*time.Hour)) {
|
||||
ts, err = parser.ParseInDefaultArchiveLocation("Jan _2 15:04:05 2006", fmt.Sprintf("%s %d", raw, collectedYear-1))
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
}
|
||||
return ts
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
func eventsByType(events []models.Event, eventType string) []models.Event {
|
||||
var out []models.Event
|
||||
for _, e := range events {
|
||||
if e.EventType == eventType {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestParseXidAndFaultEvents_ClassifiesByCode(t *testing.T) {
|
||||
content := strings.Join([]string{
|
||||
"Jul 29 02:33:16 host kernel: NVRM: Xid (PCI:0000:dc:00): 31, pid=123, name=python, MMU Fault",
|
||||
"Aug 19 21:17:46 host kernel: NVRM: Xid (PCI:0000:1a:00): 154, GPU recovery action changed from 0x0 (None) to 0x4 (Drain and Reset)",
|
||||
"Aug 23 02:36:37 host kernel: NVRM: Xid (PCI:0000:1a:00): 154, GPU recovery action changed from 0x4 (Drain and Reset) to 0x1 (GPU Reset Required)",
|
||||
}, "\n")
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
CollectedAt: time.Date(2026, 8, 24, 13, 56, 0, 0, time.UTC),
|
||||
}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
xids := eventsByType(result.Events, "GPU Xid Error")
|
||||
if len(xids) != 3 {
|
||||
t.Fatalf("expected 3 Xid events, got %d", len(xids))
|
||||
}
|
||||
|
||||
if xids[0].Severity != models.SeverityWarning {
|
||||
t.Fatalf("expected Xid 31 to be warning, got %s", xids[0].Severity)
|
||||
}
|
||||
if xids[0].SensorName != "0000:dc:00" {
|
||||
t.Fatalf("expected sensor name 0000:dc:00, got %q", xids[0].SensorName)
|
||||
}
|
||||
if xids[0].Timestamp.Month() != time.July || xids[0].Timestamp.Year() != 2026 {
|
||||
t.Fatalf("expected July 2026 timestamp, got %s", xids[0].Timestamp)
|
||||
}
|
||||
|
||||
if xids[1].Severity != models.SeverityCritical {
|
||||
t.Fatalf("expected Xid 154 (Drain and Reset) to be critical, got %s", xids[1].Severity)
|
||||
}
|
||||
if xids[2].Severity != models.SeverityCritical {
|
||||
t.Fatalf("expected Xid 154 (GPU Reset Required) to be critical, got %s", xids[2].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXidAndFaultEvents_Xid137IsHardwareLeaning(t *testing.T) {
|
||||
// Xid 137 (NVLink TLC RX PRIV Error) is an NVLink transport-layer
|
||||
// hardware fault, typically seen right before a contained Xid 94 —
|
||||
// must classify as critical, not the app-level default.
|
||||
content := "Aug 18 04:45:34 host kernel: NVRM: Xid (PCI:0000:63:00): 137, TLC RX interrupt hit on link 2 on GPU0: PRIV Error"
|
||||
|
||||
result := &models.AnalysisResult{CollectedAt: time.Date(2026, 8, 18, 17, 40, 0, 0, time.UTC)}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
xids := eventsByType(result.Events, "GPU Xid Error")
|
||||
if len(xids) != 1 {
|
||||
t.Fatalf("expected 1 Xid event, got %d", len(xids))
|
||||
}
|
||||
if xids[0].Severity != models.SeverityCritical {
|
||||
t.Fatalf("expected Xid 137 to be critical, got %s", xids[0].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXidAndFaultEvents_SXidUsesDistinctEventType(t *testing.T) {
|
||||
content := "Aug 20 01:00:00 host kernel: NVRM: SXid (PCI:0000:90:00): 12028, some NVSwitch fault detail"
|
||||
|
||||
result := &models.AnalysisResult{CollectedAt: time.Date(2026, 8, 24, 0, 0, 0, 0, time.UTC)}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
sxids := eventsByType(result.Events, "NVSwitch SXid Error")
|
||||
if len(sxids) != 1 {
|
||||
t.Fatalf("expected 1 SXid event, got %d", len(sxids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXidAndFaultEvents_AggregatesNVLinkFloodIntoOneEvent(t *testing.T) {
|
||||
var lines []string
|
||||
for i := 0; i < 500; i++ {
|
||||
lines = append(lines, "Aug 24 08:49:26 host kernel: NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!")
|
||||
}
|
||||
content := strings.Join(lines, "\n")
|
||||
|
||||
result := &models.AnalysisResult{CollectedAt: time.Date(2026, 8, 24, 13, 0, 0, 0, time.UTC)}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
floods := eventsByType(result.Events, "NVLink Rx Detect Failure Burst")
|
||||
if len(floods) != 1 {
|
||||
t.Fatalf("expected exactly 1 aggregated flood event for 500 lines, got %d", len(floods))
|
||||
}
|
||||
if floods[0].Severity != models.SeverityCritical {
|
||||
t.Fatalf("expected flood of 500 to be critical, got %s", floods[0].Severity)
|
||||
}
|
||||
if !strings.Contains(floods[0].Description, "500 times") {
|
||||
t.Fatalf("expected description to mention count, got %q", floods[0].Description)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseXidAndFaultEvents_FloodTimeRangeIsChronologicalNotScanOrder is a
|
||||
// regression test: a real nvidia-bug-report.sh dump concatenates multiple
|
||||
// overlapping log sources (e.g. dmesg -T and journalctl excerpts), so a
|
||||
// later line in the file is not guaranteed to be later in time. The flood
|
||||
// summary's first/last timestamps must reflect the true chronological
|
||||
// min/max of all matched lines, not the order they were scanned in.
|
||||
func TestParseXidAndFaultEvents_FloodTimeRangeIsChronologicalNotScanOrder(t *testing.T) {
|
||||
content := strings.Join([]string{
|
||||
// Appears first in the file but is chronologically the latest.
|
||||
"Aug 24 08:49:26 host kernel: NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!",
|
||||
// Appears later in the file (a different, earlier-covering log section) but predates the line above.
|
||||
"Aug 19 21:30:00 host kernel: NVRM: knvlinkUpdatePostRxDetectLinkMask_IMPL: Failed to update Rx Detect Link mask!",
|
||||
}, "\n")
|
||||
|
||||
result := &models.AnalysisResult{CollectedAt: time.Date(2026, 8, 24, 13, 0, 0, 0, time.UTC)}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
floods := eventsByType(result.Events, "NVLink Rx Detect Failure Burst")
|
||||
if len(floods) != 1 {
|
||||
t.Fatalf("expected 1 aggregated flood event, got %d", len(floods))
|
||||
}
|
||||
if !strings.Contains(floods[0].Description, "2026-08-19") {
|
||||
t.Fatalf("expected description to mention the true earliest date (2026-08-19), got %q", floods[0].Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXidAndFaultEvents_FellOffBus(t *testing.T) {
|
||||
content := "Aug 24 08:00:00 host kernel: NVRM: GPU 0000:1a:00.0: GPU has fallen off the bus."
|
||||
|
||||
result := &models.AnalysisResult{CollectedAt: time.Date(2026, 8, 24, 13, 0, 0, 0, time.UTC)}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
events := eventsByType(result.Events, "GPU Fell Off The Bus")
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 fell-off-bus event, got %d", len(events))
|
||||
}
|
||||
if events[0].Severity != models.SeverityCritical {
|
||||
t.Fatalf("expected critical severity, got %s", events[0].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXidAndFaultEvents_WritesFaultStatusOntoGPURecord(t *testing.T) {
|
||||
content := strings.Join([]string{
|
||||
"Jul 29 02:33:16 host kernel: NVRM: Xid (PCI:0000:dc:00): 31, pid=123, name=python, MMU Fault",
|
||||
"Aug 19 21:17:46 host kernel: NVRM: Xid (PCI:0000:1a:00): 154, GPU recovery action changed from 0x0 (None) to 0x4 (Drain and Reset)",
|
||||
"Aug 23 02:36:37 host kernel: NVRM: Xid (PCI:0000:1a:00): 154, GPU recovery action changed from 0x4 (Drain and Reset) to 0x1 (GPU Reset Required)",
|
||||
}, "\n")
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
CollectedAt: time.Date(2026, 8, 24, 13, 56, 0, 0, time.UTC),
|
||||
Hardware: &models.HardwareConfig{
|
||||
GPUs: []models.GPU{
|
||||
{BDF: "0000:1a:00.0", Model: "NVIDIA B200"},
|
||||
{BDF: "0000:dc:00.0", Model: "NVIDIA B200"},
|
||||
{BDF: "0000:3b:00.0", Model: "NVIDIA B200"}, // untouched — no Xid for this one
|
||||
},
|
||||
},
|
||||
}
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
faulted := result.Hardware.GPUs[0]
|
||||
if faulted.Status != "critical" {
|
||||
t.Fatalf("expected 0000:1a:00.0 status critical, got %q", faulted.Status)
|
||||
}
|
||||
if faulted.ErrorDescription == "" {
|
||||
t.Fatalf("expected a non-empty error description")
|
||||
}
|
||||
if faulted.StatusChangedAt == nil || !faulted.StatusChangedAt.Equal(time.Date(2026, 8, 23, 2, 36, 37, 0, parser.DefaultArchiveLocation())) {
|
||||
t.Fatalf("expected status_changed_at to be the latest fault timestamp, got %v", faulted.StatusChangedAt)
|
||||
}
|
||||
|
||||
warned := result.Hardware.GPUs[1]
|
||||
if warned.Status != "warning" {
|
||||
t.Fatalf("expected 0000:dc:00.0 status warning, got %q", warned.Status)
|
||||
}
|
||||
|
||||
untouched := result.Hardware.GPUs[2]
|
||||
if untouched.Status != "" {
|
||||
t.Fatalf("expected 0000:3b:00.0 to be untouched, got status %q", untouched.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSyslogTimestamp_HandlesYearRollover(t *testing.T) {
|
||||
// Report collected early January; a log line stamped "Dec 30" must be
|
||||
// attributed to the previous year, not the future.
|
||||
collectedAt := time.Date(2027, 1, 5, 12, 0, 0, 0, time.UTC)
|
||||
ts := parseSyslogTimestamp("Dec 30 10:00:00", collectedAt.Year(), collectedAt)
|
||||
if ts.Year() != 2026 {
|
||||
t.Fatalf("expected rollover to previous year 2026, got %d", ts.Year())
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
const parserVersion = "1.2"
|
||||
const parserVersion = "1.3"
|
||||
|
||||
var bugReportDateLineRegex = regexp.MustCompile(`(?m)^Date:\s+(.+?)\s*$`)
|
||||
var dateWithTZAbbrevRegex = regexp.MustCompile(`^([A-Za-z]{3}\s+[A-Za-z]{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+([A-Za-z]{2,5})\s+(\d{4})$`)
|
||||
@@ -126,6 +126,9 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
// Parse driver version
|
||||
parseDriverVersion(content, result)
|
||||
|
||||
// Parse Xid/SXid GPU error reports and other known hardware fault patterns
|
||||
parseXidAndFaultEvents(content, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ func parsePSUInfo(content string, result *models.AnalysisResult) {
|
||||
if currentPSU != nil && currentPSU.Slot != "" {
|
||||
// Only add if PSU is present
|
||||
if strings.Contains(strings.ToLower(currentPSU.Status), "present") {
|
||||
currentPSU.Present = true
|
||||
result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, *currentPSU)
|
||||
}
|
||||
}
|
||||
@@ -91,6 +92,7 @@ func parsePSUInfo(content string, result *models.AnalysisResult) {
|
||||
// Save last PSU if exists
|
||||
if currentPSU != nil && currentPSU.Slot != "" {
|
||||
if strings.Contains(strings.ToLower(currentPSU.Status), "present") {
|
||||
currentPSU.Present = true
|
||||
result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, *currentPSU)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user