feat(observability): per-GPU metrics for the mixed AMD/NVIDIA fleet
Add a host-process GPU exporter, scrape it on both servers, and graph it on the host-system dashboard. The fleet has four GPUs across three vendors of interest (2x R9700 on Dankserver, the 8060S iGPU + RTX 3090 on Dankclaw) and nothing exposed them until now. scripts/gpu-exporter.py reads each AMD card's amdgpu sysfs (gpu_busy_percent / mem_info_vram_* / hwmon temp1_input / power1_average) plus one nvidia-smi --query-gpu CSV, and emits unified gpu_* Prometheus families (utilization_percent, memory_used/total_bytes, temperature_celsius, power_watts) labelled gpu/vendor/model. Absent hardware yields ABSENT series, never a fabricated 0: the R9700s expose no sysfs power, so they simply carry no gpu_power_watts line. Both sources are injectable, so the seam runs on a GPU-less dev box; live-verified on Dankserver's real sysfs. It runs as a plain user unit (not a quadlet): it must read the host's own DRM sysfs and shell to nvidia-smi, which a container cannot do without GPU passthrough. Same posture as signal-relay. The base unit binds loopback; the deploy drop-in appends the host LAN address, never a wildcard (matching node-exporter/cadvisor). deploy_gpu_exporter installs/enables/health-gates it on both hosts and is wired into the planner (both targets), the test-changed router, and two Prometheus scrape jobs using the node/cadvisor reach rules (gateway on Dankclaw, LAN on Dankserver).
This commit is contained in:
1 parent
b35269bfa5
commit
b02647b992
9 files changed
+592
-9
No files matched your search
@@ -18,6 +18,16 @@
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"current": { "text": "All", "value": "$__all" }
|
||||
},
|
||||
{
|
||||
"name": "gpuhost",
|
||||
"label": "gpu host",
|
||||
"type": "query",
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"query": { "query": "label_values(gpu_utilization_percent, host)", "refId": "A" },
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"current": { "text": "All", "value": "$__all" }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -139,6 +149,56 @@
|
||||
{ "expr": "sum by (job,device) (rate(node_network_receive_errs_total{job=~\"$node\"}[5m]))", "legendFormat": "{{job}} {{device}} RX err", "refId": "A" },
|
||||
{ "expr": "sum by (job,device) (rate(node_network_transmit_errs_total{job=~\"$node\"}[5m]))", "legendFormat": "{{job}} {{device}} TX err", "refId": "B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13, "type": "timeseries", "title": "GPU busy %",
|
||||
"description": "Per-GPU utilization from gpu-exporter (amdgpu gpu_busy_percent / nvidia-smi). The picker above selects the host; each series is one card.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 36 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100 }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "gpu_utilization_percent{host=~\"$gpuhost\"}", "legendFormat": "{{host}} {{gpu}} ({{model}})", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14, "type": "timeseries", "title": "GPU VRAM used",
|
||||
"description": "Device memory in use. On the R9700s (frank-archon) and the 3090 (frank-zealot) this is the loaded model's footprint.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 36 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "bytes", "min": 0 }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "gpu_memory_used_bytes{host=~\"$gpuhost\"}", "legendFormat": "{{host}} {{gpu}} ({{model}})", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15, "type": "timeseries", "title": "GPU VRAM used %",
|
||||
"description": "Used / total device memory. Near 100% is the pre-OOM wall.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 44 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "percent", "min": 0, "max": 100 }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "100 * gpu_memory_used_bytes{host=~\"$gpuhost\"} / gpu_memory_total_bytes{host=~\"$gpuhost\"}", "legendFormat": "{{host}} {{gpu}} ({{model}})", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16, "type": "timeseries", "title": "GPU temperature",
|
||||
"description": "Package temperature. AMD edge temp (hwmon temp1_input); NVIDIA gpu temp.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 44 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "celsius", "min": 0 }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "gpu_temperature_celsius{host=~\"$gpuhost\"}", "legendFormat": "{{host}} {{gpu}} ({{model}})", "refId": "A" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17, "type": "timeseries", "title": "GPU power draw",
|
||||
"description": "Board power draw. Emitted only where readable: NVIDIA and the Dankclaw iGPU. The R9700s have no sysfs power sensor, so they are simply absent here — never a fabricated 0.",
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 52 },
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"fieldConfig": { "defaults": { "unit": "watt", "min": 0 }, "overrides": [] },
|
||||
"targets": [
|
||||
{ "expr": "gpu_power_watts{host=~\"$gpuhost\"}", "legendFormat": "{{host}} {{gpu}} ({{model}})", "refId": "A" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -191,3 +191,25 @@ scrape_configs:
|
||||
action: keep
|
||||
- regex: 'id|name|image|container_label.*|machine_id|boot_id'
|
||||
action: labeldrop
|
||||
|
||||
# Per-GPU metrics (gpu_utilization_percent / gpu_memory_used_bytes /
|
||||
# gpu_memory_total_bytes / gpu_temperature_celsius / gpu_power_watts) from the
|
||||
# host-process gpu-exporter (scripts/gpu-exporter.py). One job per host, same
|
||||
# reach rules as node/cadvisor: Dankclaw is scraped at the podman gateway
|
||||
# because the netavark hairpin quirk makes Prometheus fail to dial its own
|
||||
# host LAN IP; Dankserver is plain LAN. The gpu label (PCI BDF on AMD, index on
|
||||
# NVIDIA) + vendor/model come from the exporter, so the GPU row on the
|
||||
# host-system dashboard filters by host. Power is emitted only where readable
|
||||
# (NVIDIA + the Dankclaw iGPU); the R9700s have no sysfs power, so they simply
|
||||
# have no gpu_power_watts series — no fabricated zero.
|
||||
- job_name: "gpu-dankclaw"
|
||||
static_configs:
|
||||
- targets: ["host.containers.internal:9401"]
|
||||
labels:
|
||||
host: dankclaw
|
||||
|
||||
- job_name: "gpu-dankserver"
|
||||
static_configs:
|
||||
- targets: ["192.168.0.235:9401"]
|
||||
labels:
|
||||
host: dankserver
|
||||
Reference in new issue
Block a user