Skip to content

Add SNMP extend script for AMD GPU power and clock - #627

Open
Fever-Wits wants to merge 1 commit into
librenms:masterfrom
Fever-Wits:amdgpu
Open

Add SNMP extend script for AMD GPU power and clock#627
Fever-Wits wants to merge 1 commit into
librenms:masterfrom
Fever-Wits:amdgpu

Conversation

@Fever-Wits

@Fever-Wits Fever-Wits commented Aug 8, 2026

Copy link
Copy Markdown

Adds an SNMP extend script exposing AMD GPU power and clock.

These cannot reach SNMP on their own. The net-snmp lmSensors module maps only
the temperature, fan and voltage features of libsensors, so watts and hertz are
given no index at all and lmMiscSensorsTable stays empty — no configuration
changes that. GPU temperature and voltages are unaffected and keep arriving
through LM-SENSORS-MIB as before; this only adds what is missing.

The companion discovery side is librenms/librenms#20258.

Output

One block of exactly four lines per card:

0000:c7:00.0 AMD Radeon 780M Graphics
27244000
43186000
2700000000

The three metric lines carry a bare number, because LibreNMS reads a sensor OID
straight into a numeric value — a label on the same line would be parsed as part
of the number and silently yield 0.

Notes on the design

Fixed block length. An attribute that cannot be read is emitted as U, not
omitted. On APUs power1_average is not always available — the SMU may decline
it while idle — so a card publishing only some of the three is the ordinary
case. Omitting the line would shift every following value onto the wrong sensor;
U leaves a placeholder that discovery ignores. This follows ups-nut.sh, which
uses Unknown the same way.

Cards found through the PCI driver directory, not through hwmonN: hwmon
numbering is not stable across reboots, PCI slot addressing is. The glob expands
alphabetically, so block order is deterministic.

Product names come from the libdrm ids table, matched on device id and
revision — the revision matters, since e.g. 15BF revision C4 is a Radeon 780M
while C5 is a 740M. A card the table does not list falls back to its PCI
address alone.

Testing

Verified on a Ryzen 8845HS host (Radeon 780M, Proxmox), run both as root and as
the snmpd user through sudo. Also exercised against a synthetic sysfs tree with
three cards — complete, missing an attribute, and with no hwmon node — to confirm
the block length holds and a card without hwmon emits nothing rather than
shifting the others.

shellcheck clean at all severities; sh -n and dash -n clean.

@CLAassistant

CLAassistant commented Aug 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Fever-Wits

Copy link
Copy Markdown
Author

Reworked the output format after finding that the consumer side had to rebuild the card blocks by counting lines.

Each card now emits an identifier line naming the metrics it carries, followed by those metrics in that order:

0000:c7:00.0 power1_average,power1_input,freq1_input AMD Radeon 780M Graphics
30215000
40067000
2700000000

The names live on the identifier line because the value lines cannot hold them — LibreNMS reads a sensor OID straight into a numeric value, so a label beside the number is parsed as part of it.

The metric list is built from the same variable the loop reads, so a metric added to the script appears in both places or in neither.

The consumer side is librenms/librenms#20258; the two go together.

The net-snmp lmSensors module maps only the temperature, fan and voltage
features of libsensors, so GPU power and clock are given no index and never
reach SNMP. This script reads them from sysfs and serves them as a table
under the LibreNMS enterprise tree, for discovery as power and frequency
sensors.

The table is indexed by the PCI address of the card, so an OID carries the
card it belongs to. A card added, removed or unbound from the driver leaves
the readings of the others where they are, and two identical cards in one
host stay apart. A reading the card does not publish has no cell at all -
that OID answers noSuchInstance - rather than a placeholder that has to mean
something. On APUs power1_average is not always readable, since the SMU may
decline it while idle, so a card reporting only power1_input is ordinary.

Cards are walked through /sys/bus/pci/drivers/amdgpu rather than through
hwmonN, since hwmon numbering is not stable across reboots while PCI slot
addressing is. Product names are looked up in the libdrm ids table by device
id and revision, falling back to the PCI address when the table is absent or
does not list the card.

The rows are materialised once per collection, already in the order a walk
traverses them, so getnext is the first entry ordering after the requested
OID rather than arithmetic over columns and rows. A sparse table, a host
with no cards, and a request past the end of the tree are then the same case
rather than three.

Requires the companion MIB and discovery modules in librenms/librenms#20258;
the OID space is unusable without them.
Fever-Wits added a commit to Fever-Wits/librenms that referenced this pull request Aug 11, 2026
The net-snmp lmSensors module maps only the temperature, fan and voltage
features of libsensors, so GPU power and clock are given no index at all and
lmMiscSensorsTable stays empty. They cannot reach LM-SENSORS-MIB regardless
of configuration. GPU temperature and voltages are unaffected and keep
arriving as before.

This discovers them from the companion pass_persist script in
librenms-agent, which serves them as a table under LIBRENMS-AMDGPU-MIB.
Each card is one row and a sensor group of its own, identified by PCI
address and, where libdrm knows the card, by product name.

The table is indexed by the PCI address, so the OID of a sensor carries the
card it reads. That removes a failure the earlier extend-based approach had
to warn about: cards changing no longer shifts the readings of the others
onto their neighbours. The sensor index is the address together with the MIB
object, so it stays valid if the script ever reorders what it reports.

A reading the card does not publish has no cell in the table, and the OID
answers noSuchInstance - there is no placeholder value to filter here.

No thresholds are set. The frequency class guesses the reading +/-5%, which
is not meaningful for a GPU core clock that moves between idle and boost by
design, and a ceiling cannot be inferred from the sensor alone - the same
driver serves parts whose boost differs by a factor of ten. This is stated
in the docs so users adjust the limits themselves.

Placed in unix.inc.php rather than linux.inc.php: the group covers Proxmox
and the other unix os entries as well, and frequency/linux.inc.php already
serves the Raspberry Pi. The two files repeat the walk deliberately - the
query cache keys on oids plus options, so the identical chains resolve to a
single walk for both classes, while a single file serving both would be
included before the discovery_submodules.sensors filter and would produce
power sensors for users who disabled that class.

The MIB takes .1.3.6.1.4.1.60652.101, the next free arc under the LibreNMS
enterprise number after .100 (XCP-NG-VMINFO-MIB); say if another is
preferred.

Test data covers both paths: a card libdrm knows, and a second one
identified by address alone with a reading it does not publish. The
development host has a single GPU, so the multi-card path is covered there
rather than by hardware.

Requires librenms/librenms-agent#627; the two go together.
@Fever-Wits

Copy link
Copy Markdown
Author

Rewritten as pass_persist — it now serves a table under .1.3.6.1.4.1.60652.101 instead of printing lines, so the consumer side no longer has to rebuild the blocks. Indexed by PCI address; a reading the card does not publish has no cell at all rather than a placeholder.

Details and the reasoning are on the LibreNMS side: librenms/librenms#20258. The two go together — this OID space is not usable without the MIB that ships there.

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.

2 participants