Telemetry map and SMU control tools for AMD Granite Ridge (Zen 5) under Linux.
The ryzen_smu driver exposes a 1828-byte PM table at
/sys/kernel/ryzen_smu_drv/pm_table — 457 little-endian float32 values with no
published layout. This repo is the layout, worked out by measurement, plus the tools
that read and write it.
This is the one thing that would move the project forward, and it takes about ten seconds. Any Zen 5 desktop chip that is not a 9800X3D — 9600X, 9700X, 9900X, 9950X, 9950X3D, anything:
sudo python3 tools/dump_table_full.py > my_dump.txtOpen an issue with that file and your exact CPU model. The dump tool works on unvalidated hardware on purpose: it drops the labels and prints raw values, which is exactly what is needed to compare layouts.
Why it matters: every offset here comes from one machine, so nothing distinguishes "this is where AMD puts Tctl" from "this is where Tctl landed on my 9800X3D". A second part settles that. A 12- or 16-core part also settles where the per-core arrays end, which is currently inferred from a single 8-wide block.
The other open questions need a different lever rather than more data. Thirteen fields have a narrowed domain but no identification, because under load every axis rises at once and none of them separates cleanly; on Zen 5 the die's thermal constant is sub-second, so decay timing cannot separate the power and thermal domains either. That needs frequency varied at constant power, or fixed power at two ambient temperatures. And there is no live EDC value anywhere in this table version — that one is closed, and the search is written up as a negative result.
Every offset here was measured on exactly one machine: a Ryzen 7 9800X3D,
8 cores / 1 CCD, PM table version 0x620105. Nothing is validated anywhere else.
That matters more than it sounds, because the failure mode is silent. A different
table version moves every offset, but the bytes still parse as 457 floats — so the
GUI shows plausible watts and degrees that are simply the wrong fields. A different
core count changes the per-core array widths; d[317-324] is 8 wide here, and on a
16-core part everything after it shifts.
So the tools check first (tools/hwgate.py) and refuse rather than
guess:
| Validated hardware | Anything else | |
|---|---|---|
| Telemetry display | full | stops, with the reason |
| CSV/JSON export | full | exits, does not write a file |
| SMU writes (limits, Curve Optimizer) | allowed | blocked |
If you hit the gate, send a dump rather than loosening it — the offsets would be wrong, not missing.
All 457 indices have a row in PM_TABLE_MAP.md, but the rows carry very different weight, and the confidence column says which is which:
- Cross-validated (strongest). 14 automated checks compare PM fields against
independent sensors —
k10temp,amdgpu,cpufreq, DDR5 nominal — or against stock spec. Tctl, per-core temperatures, per-core frequency, boost limit, Vcore, VDDCR_SoC, VDDIO_MEM, iGPU clock, C6 residency, and the PPT/TDC/EDC limits (162 W / 120 A / 180 A, exact) are in this group. - Structural. Zone
0x000is the classic Zen(LIMIT, VALUE)pair layout: PPT0x008/0x00Cin watts, TDC0x020/0x024in amps, thermal0x028/0x02Cin °C, EDC limit at0x0FC. Every real temperature in the table is direct °C — there is no encoding to undo. - Inferred. Correlation and load response only. Treat as a hypothesis.
- Known wrong, and left in the map as such. Several fields once marked CONFIRMED were disproved; the rows now say what they are not. See the honesty audit.
Open questions are tracked in docs/TOFIX.md; the EDC search is written up as a negative result.
The map is not trusted on its word. research/audit_map.py
parses PM_TABLE_MAP.md itself and asserts every mechanically checkable claim against
live hardware — static fields must not move under load, fields documented as zero must
read zero, documented mirrors must be bit-identical, and cross-validated fields must
match their system sensor within tolerance. It exits non-zero on any failure.
sudo python3 research/audit_map.pyIt runs a stress load and takes a few minutes. Its first run found 11 genuine documentation errors, including three "perfect mirrors" that differ on every read and nine fields labelled energy accumulators that do not accumulate.
Two measurement lessons from building it are worth stealing if you write your own:
- Read the external sensor before
pm_table, not after. The PM table read costs an SMU transfer that warms the die enough to show in the next sensor read — a +2.4 °C bias, larger than most things you would be validating. - Use medians over a window, and compare sensors sampled in the same window. Occasional garbage sysfs reads wreck a mean, and a reading taken before or after the window is a different point on a thermal transient.
- Wait for equilibrium, and prove it with two stable windows, not one. A sensor
disagreement of a few degrees is almost always cooldown, not calibration: after a
stress load the
k10temp−d[11]delta decays +7.3 → +1.0 °C over a couple of minutes and only then settles at +0.15 °C. Single instantaneous reads cannot tell that apart from noise — at idlek10tempalone swings 46.6 → 64.6 °C on background activity.
All of them need the ryzen_smu driver loaded, and root.
sudo python3 tools/gui/gnr_master.py # PyQt6 dashboard: per-core clocks, power, CO
sudo python3 tools/gnr_master.py # menu-driven CLI for limits and Curve Optimizer
sudo python3 tools/export_telemetry.py # 5 JSON snapshots
sudo python3 tools/export_telemetry.py --csv
sudo python3 tools/export_telemetry.py --live 2 # append a CSV row every 2 s
sudo python3 tools/dump_table_full.py # all 457 floats, labelled from the mapSMU control uses MP1 mailbox message IDs (not table offsets): 0x3E PPT,
0x3D TDC, 0x3C EDC, 0x50-0x57 per-core Curve Optimizer as a signed 32-bit
value. On Zen 5 the TDC and EDC IDs are the reverse of what Zen 4 documentation
implies, which is why they are commented at every call site. Curve Optimizer is
write-only — the SMU will not read the offsets back, so the tools cache them locally
in $XDG_CONFIG_HOME/gnr_master.json to keep the display honest.
research/ holds the measurement scripts, one per question asked: audit_map.py
(the map's regression gate), recheck_zone0.py / recheck_sweep.py / recheck_edc.py
(the zone 0x000 correction), hunt_edc.py (the exhaustive EDC search),
classify_unknown.py, profile_load.py, profile_demoted.py and
transient_demoted.py. smu_send.py and smu_advanced.py are standalone MP1/RSMU
mailbox tools.
dump_table_full.py prints the whole table with each field's documented meaning and
confidence, read from PM_TABLE_MAP.md itself.
- Linux 6.10+
- The
ryzen_smukernel module, loaded python3-pyqt6andpyqtgraph, for the GUI only
Writing to the SMU mailbox can destabilise or damage hardware. Specifics that matter:
- A wrong offset is worse than a missing one. Reading the wrong field shows a wrong number; writing a limit derived from a wrong field pushes it into the SMU. That has already happened here once — the thermal limit (88 °C) was read as TDC and pre-filled the write dialog as 88 A. Hence the hardware gate.
- Both front-ends block message IDs
0x03-0x0Dand0x10outright, and that should stay.0x58-0x5Dfreeze MP1 on this part; do not probe them. - Stock limits for the 9800X3D are 162 W PPT / 120 A TDC / 180 A EDC. The reset paths send exactly those.
- 3D V-Cache runs under a tighter thermal ceiling than the rest of the die. The thermal limit in the table reads 88 °C.
- SMU settings are volatile — a reboot reverts everything to BIOS constraints. That is also your recovery path.
MIT — see LICENSE.
