Skip to content

fix(chart): make GPU alerts exporter-agnostic instead of DCGM-only - #1372

Open
Tanguille wants to merge 6 commits into
defilantech:mainfrom
Tanguille:fix/gpu-alerts-vendor-aware
Open

fix(chart): make GPU alerts exporter-agnostic instead of DCGM-only#1372
Tanguille wants to merge 6 commits into
defilantech:mainfrom
Tanguille:fix/gpu-alerts-vendor-aware

Conversation

@Tanguille

@Tanguille Tanguille commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #1356. Off main, independent of #1368, touches no dashboard.

All four GPU rules key on DCGM_FI_DEV_* while rules.gpu.enabled defaults to true, so a cluster without DCGM gets four rules that never fire and never error.

Change

Each rule ORs the normalized expression for every exporter the chart knows, so the one a cluster runs is the one that evaluates. rules.gpu.vendor is gone: nothing needs configuring.

Role Expression
util DCGM_FI_DEV_GPU_UTIL or amdgpu_gpu_busy_percent or drm_engine_utilization_ratio{engine="gpu"} * 100
temp DCGM_FI_DEV_GPU_TEMP or node_hwmon_temp_celsius{sensor="temp1"} * on(chip, instance) group_left() node_hwmon_chip_names{chip_name="amdgpu"} or drm_temperature_celsius{sensor="edge"}
power DCGM_FI_DEV_POWER_USAGE or node_hwmon_power_watt * on(chip, instance) group_left() node_hwmon_chip_names{chip_name="amdgpu"} or drm_power_watts
mem (DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL) * 100 or (amdgpu_vram_used_bytes / amdgpu_vram_total_bytes) * 100 or (drm_memory_used_bytes{pool="vram"} / drm_memory_total_bytes{pool="vram"}) * 100

Each union is parenthesised before the comparison: (a or b or c) > 90, not a or b or (c > 90). Rule bodies are written once from a role-keyed helper. GPUMetricsMissing is now absent() over all three families, ANDed.

GPUMemoryPressure ships off by default (rules.gpu.memoryPressure.enabled): on an inference host VRAM utilization reports the fraction the runtime was configured to reserve (mem-fraction-static, gpu-memory-utilization), not pressure. It is held for the process lifetime and does not fall under load, so no static threshold on it distinguishes healthy from saturated.

Normalization, measured on a live AMD cluster

Finding Measurement Fix
Sensor split edge 69 C, junction 82 C, mem 79 C on one GPU; DCGM reports one edge-equivalent series pin sensor="edge" / temp1
node_hwmon scope unscoped, fired GPUHighTemperature at 89.25 C off a chipset sensor while the GPU sat at 69 C join node_hwmon_chip_names{chip_name="amdgpu"}, as amd-gpu-observability.json does
Memory pool drm carries pool={vram,smem}; unfiltered denominator worked only by implicit label matching pin pool="vram" both sides
VRAM threshold 99% on this host, but the number is the configured reservation, not demand GPUMemoryPressure opt-in

Evidence

Rendered from this branch, evaluated against live VictoriaMetrics:

Rule Result
GPUHighUtilization fires, 99% busy under load
GPUHighTemperature does not fire; union resolves to GPU sensors only (39 / 69 / 44 C)
GPUPowerLimit does not fire, 246 W vs 250
GPUMetricsMissing does not fire
GPUMemoryPressure absent from rendered output by default

Both exporters run on that cluster, so the temp union returns the node_hwmon and drm branches for the same GPU: they agree at 69 C.

helm unittest 67/67, go test ./internal/metrics/... ok, helm lint clean. The nvidia path renders byte-identical to the current release apart from GPUMetricsMissing.

If you would rather keep an explicit vendor knob, or disagree with any of the normalization choices, say so and I will revert it.

DCGM_FI_DEV_* series only exist on NVIDIA clusters, so the GPU alert
group installed clean on AMD (drm-exporter) clusters and never fired
or errored — indistinguishable from healthy GPUs (defilantech#1356).

rules.gpu.vendor (nvidia|amd, default nvidia) selects between the
DCGM expressions and an AMD set over drm-exporter's
drm_engine_utilization_ratio/drm_temperature_celsius/drm_power_watts/
drm_memory_*_bytes. An unrecognized vendor fails the schema and the
template render instead of installing rules that match nothing.

Both vendors also get a new GPUMetricsMissing alert so a dead
exporter (or a vendor/cluster mismatch) is visible rather than
silent, same as ControllerMetricsMissing already does for the
controller scrape target.

Signed-off-by: Tanguille <91473554+Tanguille@users.noreply.github.com>
Move per-vendor metric names into llmkube.gpuMetricNames in _helpers.tpl
instead of duplicating all five rule bodies per vendor; a third vendor is
now one map entry, not five more rules. Drop the unreachable fail branch
(values.schema.json enum is the only guard Helm ever exercises) and rename
its test to state it asserts schema rejection, not template code. Note in
values.yaml that the alert vendor is narrower than the Model CRD's vendor
field on purpose.

Signed-off-by: Tanguille <91473554+Tanguille@users.noreply.github.com>
…-fallback

Live AMD testing found drm_temperature_celsius carries per-sensor series
(edge/junction/mem) so one threshold meant different things per exporter,
and GPUMemoryPressure fired permanently on healthy inference hosts since
SGLang/vLLM preallocate the KV pool to ~99% VRAM by design. Review also
found the drm_* metric family disagreed with the chart's own AMD dashboard
and dashboard_sync_test.go allowlist, which both expect amdgpu_*.

Rules are now keyed by role (util/temp/mem/power) and OR every known
exporter (DCGM, amdgpu-sysfs, drm) in one normalized expression, pinning
sensor="edge" and pool="vram" where needed. rules.gpu.vendor is gone.
GPUMemoryPressure is opt-in (rules.gpu.memoryPressure.enabled, default
false).

Signed-off-by: Tanguille <91473554+Tanguille@users.noreply.github.com>
A bare node_hwmon_temp_celsius is every hwmon sensor on the node, not the
GPU. Measured on a live cluster it fired GPUHighTemperature at 89C off a
chipset sensor with no GPU involved, while the GPU itself sat at 69C.

Scoped through node_hwmon_chip_names, the same join amd-gpu-observability.json
already uses, and pinned to temp1 so the amdgpu hwmon split (temp1/temp2/temp3
= edge/junction/mem) matches the edge sensor the other branches report.

Signed-off-by: Tanguille <91473554+Tanguille@users.noreply.github.com>
@Tanguille Tanguille changed the title fix(chart): make the GPU alert group vendor-aware fix(chart): make GPU alerts exporter-agnostic instead of DCGM-only Jul 30, 2026
Drops the stale block describing the removed vendor parameter.

Signed-off-by: Tanguille <91473554+Tanguille@users.noreply.github.com>
VRAM utilization on an inference host reports the fraction the runtime was
configured to reserve, not memory pressure. The old wording implied 99% is
inherent; it is whatever mem-fraction-static or gpu-memory-utilization was
set to, and a lower setting is equally healthy.

Signed-off-by: Tanguille <91473554+Tanguille@users.noreply.github.com>

@Defilan Defilan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tanguille, excellent, well-researched change, thank you. I verified every AMD/DCGM/drm metric name against amd-gpu-observability.json and the dashboard_sync_test.go allowlist and they all check out, including the correct call to source temp/power from node_hwmon_* rather than a nonexistent amdgpu_temperature_celsius.

One blocker: GPUMetricsMissing (prometheusrule.yaml:75) never fires. PromQL and matches only on exactly-equal label sets, and absent(drm_engine_utilization_ratio{engine="gpu"}) produces {engine="gpu"} while the other two absent() terms produce {}, so the intersection is empty even when all three exporter families are absent, the exact silent no-op #1356 exists to kill. promtool test rules against an empty series set confirms it. Drop the {engine="gpu"} matcher inside that absent() call (keep it in the util alert, where it's a value comparison and unaffected), and update the expected string in prometheusrule_test.yaml:196 to match.

Two smaller things: please add the Assisted-by: disclosure line to the PR body per CONTRIBUTING, and consider keeping {{ $labels.instance }} in the descriptions so alerts still name a GPU (instance is present on all three families). I'll authorize the CI runs so we get a green signal.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Defilan

Defilan commented Jul 31, 2026

Copy link
Copy Markdown
Member

CI heads-up, and it's a real consequence of the change rather than a flake. The Test Prometheus Integration job fails at .github/workflows/helm-chart.yml:276:

grep -q "DCGM_FI_DEV_GPU_UTIL > 95"   →   ❌ Custom GPU threshold not applied

That assertion pins the old DCGM-only alert shape. Your generalized GPUHighUtilization now renders as:

(DCGM_FI_DEV_GPU_UTIL or amdgpu_gpu_busy_percent or drm_engine_utilization_ratio{engine="gpu"} * 100) > 95

so the custom threshold is applied correctly, but the contiguous string DCGM_FI_DEV_GPU_UTIL > 95 no longer exists (the or terms split it). promtool itself passes ("SUCCESS: 12 rules found"), so the rules are valid, it's purely the test string that's stale.

Since this PR intentionally changes the alert shape, please update that assertion in the same pass, e.g.:

if ! grep -qE 'DCGM_FI_DEV_GPU_UTIL.*> 95' /tmp/custom-thresholds.yaml; then

which confirms the DCGM term is still present and the custom threshold applied, without requiring the old contiguous form. That, plus the GPUMetricsMissing absent() fix, should get you green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] gpu alert group is DCGM-only and default-on, so AMD clusters get silent no-op alerting

2 participants