Prerequisites
Bug Description
Split out of #1791 at @lalitadithya's request. #1793 fixes the suppression path; this is the underlying data loss that remains once suppression is out of the picture.
When DCGM reports two or more incidents for the same (watch, GPU), only the first error code survives. Later incidents contribute their message but their code is discarded, so the reported code depends on DCGM's iteration order.
health-monitors/gpu-health-monitor/gpu_health_monitor/dcgm_watcher/dcgm.py:643
if accumulator_key not in gpu_failures_accumulator:
gpu_failures_accumulator[accumulator_key] = {"code": error_code, "messages": []}
gpu_failures_accumulator[accumulator_key]["messages"].append(error_msg) # :647
Consolidation at :653 then writes a single ErrorDetails, and types.ErrorDetails holds exactly one code plus one message — so the structure cannot represent two codes even if the loop wanted to.
Why it matters
The code is what drives remediation. The messages are merged, so an operator reading the event text may see both problems, but the recommendedAction mapping keys off the single surviving code. A GPU reporting, say, DCGM_FR_FABRIC_PROBE_STATE and DCGM_FR_NVLINK_DOWN under the NVLINK watch is actioned as whichever DCGM happened to return first.
It also makes the reported code non-deterministic across polls if DCGM's ordering is not stable, which would show up as an event flapping between codes on a GPU whose hardware state has not changed.
We have not been bitten by this in production — our GPU 0 case was the suppression interaction, which #1793 fixes. This is the latent half.
Suggested fix
Two options, in increasing order of correctness and cost:
- Key the accumulator on
(watch, gpu, code). Each distinct code becomes its own incident. Cheap, but entity_failures is dict[gpu_id, ErrorDetails], so it cannot hold two entries for one GPU — this needs the container to change too.
- Make
entity_failures hold a list per GPU, i.e. dict[int, list[ErrorDetails]]. Correct, and it is the shape the data actually has, but it touches every consumer of HealthDetails.
A cheaper interim that removes the non-determinism without restructuring: pick the surviving code deterministically rather than by arrival — for example the highest-severity code, or the first in a documented precedence order — and say so in a comment. That does not fix the loss, but it makes behaviour predictable and reviewable.
Test worth having
One GPU, one watch, two unsuppressed incidents with different codes, asserted in both arrival orders — the reported code should be the same either way. That test fails today for one of the two orders whichever precedence you pick, which is the point.
Prerequisites
Bug Description
Split out of #1791 at @lalitadithya's request. #1793 fixes the suppression path; this is the underlying data loss that remains once suppression is out of the picture.
When DCGM reports two or more incidents for the same (watch, GPU), only the first error code survives. Later incidents contribute their message but their code is discarded, so the reported code depends on DCGM's iteration order.
health-monitors/gpu-health-monitor/gpu_health_monitor/dcgm_watcher/dcgm.py:643Consolidation at
:653then writes a singleErrorDetails, andtypes.ErrorDetailsholds exactly onecodeplus onemessage— so the structure cannot represent two codes even if the loop wanted to.Why it matters
The code is what drives remediation. The messages are merged, so an operator reading the event text may see both problems, but the
recommendedActionmapping keys off the single survivingcode. A GPU reporting, say,DCGM_FR_FABRIC_PROBE_STATEandDCGM_FR_NVLINK_DOWNunder the NVLINK watch is actioned as whichever DCGM happened to return first.It also makes the reported code non-deterministic across polls if DCGM's ordering is not stable, which would show up as an event flapping between codes on a GPU whose hardware state has not changed.
We have not been bitten by this in production — our GPU 0 case was the suppression interaction, which #1793 fixes. This is the latent half.
Suggested fix
Two options, in increasing order of correctness and cost:
(watch, gpu, code). Each distinct code becomes its own incident. Cheap, butentity_failuresisdict[gpu_id, ErrorDetails], so it cannot hold two entries for one GPU — this needs the container to change too.entity_failureshold a list per GPU, i.e.dict[int, list[ErrorDetails]]. Correct, and it is the shape the data actually has, but it touches every consumer ofHealthDetails.A cheaper interim that removes the non-determinism without restructuring: pick the surviving code deterministically rather than by arrival — for example the highest-severity code, or the first in a documented precedence order — and say so in a comment. That does not fix the loss, but it makes behaviour predictable and reviewable.
Test worth having
One GPU, one watch, two unsuppressed incidents with different codes, asserted in both arrival orders — the reported code should be the same either way. That test fails today for one of the two orders whichever precedence you pick, which is the point.