No BIOS image, so I diffed the NVRAM

flashrom got two attempts at the GEEKOM A9 Mega's SPI flash. Both were read-only: -r, run twice, so the two dumps could be hashed against each other before anyone trusted them. The package was Fedora 45's flashrom-1.6.0-6.fc45.

Attempt one, Secure Boot on. The kernel is in lockdown, and lockdown takes raw port I/O away from userspace: ioperm and iopl are on its refusal list. flashrom never reaches the chip.

Attempt two, Secure Boot off. flashrom runs, and names the chipset "AMD FP4". The CPU is a Ryzen AI Max+ 395, family 0x1A, model 0x70: Strix Halo. Whatever flashrom thinks it is talking to, it is not this. It fails to lift the ROM protection, changes nothing, and segfaults.

Score: zero bytes. The plan to hash two dumps against each other never got a first dump to hash.

As a tool review, that's a bug report waiting to be filed, and possibly a stale one: flashrom's own docs are already past v1.8.0, and I haven't checked whether anything newer than Fedora's 1.6.0 knows Strix Halo. But flashrom was only ever plan B. Plan A was downloading the BIOS, and there's nothing to download.

No BIOS image on the vendor page, only the EC#

GEEKOM's support page for the A9 Mega offers one firmware package: EC 0.17, for the ITE IT5571 embedded controller on board NUCSH01. It's a 128 KiB image for the chip that runs the fans, among other things. This unit already ran 0.17 out of the box. No BIOS 0.16 file, and no older one either.

On the AE8, GEEKOM published the ROM, and a ROM is a complete map: unpack it, extract the IFR, and you get all 876 knobs with their options, defaults and the byte offset each one writes to. With two ROMs you can diff BIOS versions knob by knob. IFR is the schema. Without the ROM there's no schema, and the setup screen is the only documentation left.

Filming the hidden menus#

The hidden menus open close to the way GEEKOM documents for other models. Hold Ctrl from power-on, tap H, spam Del. It works only at power-on: Ctrl+H pressed inside setup, after systemctl reboot --firmware-setup, unhides nothing. You get a second Advanced tab with AMD CBS, PBS and PMF. AMD Overclocking (PBO, Curve Optimizer, Curve Shaper) sits behind AMD's warning screen in the regular Advanced tab.

GEEKOM's own instructions for this cover the A8, AX8 and AE8 Max. The A9 Mega isn't on its model list.

So the menus got filmed with a phone: three videos, frames pulled at 2 fps. The first video alone gave 387 distinct frames, OCR'd in four chunks into a 51-page text tree; the other two added the AMD Overclocking page, the CBS top level and the manual PBO and SMU fields it had missed. That tree tells you what each setting is called and what it offers:

  • System Configuration: Auto / 45W / 55W / 85W / 120W
  • PPT Control: Auto / Manual, and under Manual: Fast PPT Limit [mW], Slow PPT Limit [mW], Slow PPT Time Constant [s]
  • Dedicated Graphics Memory: 0.5G up to 96G

It doesn't tell you where any of them are stored, or whether they do anything.

AmdSetup in efivarfs: the settings were readable all along#

Setup values live in NVRAM as UEFI variables, and Linux exposes those as files under /sys/firmware/efi/efivars. On this box the AMD CBS settings sit in one variable, AmdSetup, a 1,526-byte file. The first four bytes are the attribute word (07 00 00 00), and the setting data follows.

That gives a method that doesn't need a ROM. Snapshot the variables, change one setting in the BIOS, boot, snapshot again, diff. The byte that moved is the setting.

#!/usr/bin/env bash
# snap.sh <dir>: copy the AMD setup variables out of efivarfs
set -euo pipefail
mkdir -p "$1"
for v in /sys/firmware/efi/efivars/{AmdSetup,AodSetupStxh,AMD_PBS_SETUP}-*; do
  [ -e "$v" ] || continue
  cat "$v" > "$1/${v##*/}"    # cat, not cp
done

Boiled down to the three variables that matter here, that's the method; the committed snapshots hold every variable. The comment is there for a reason. On the A9, cp from efivarfs produced empty files, while cat from the same paths gave full copies. I didn't chase why. Every snapshot in this post was taken with cat.

Reading cmp -l: decimal offsets, octal bytes#

Here is the diff that decoded the power limit, run against two committed snapshots: one after Optimized Defaults, one after setting PPT by hand.

$ cmp -l 20261010-0041-f3-85w/AmdSetup-* 20261010-0048-ppt85-uma05/AmdSetup-*
 249 377   3
 262   2   0
 322   0   1
 323   0  10
 324   0 114
 325   0   1
 327   0  10
 328   0 114
 329   0   1
 331   0   5

The AodSetupStxh and AMD_PBS_SETUP copies compared equal, so everything is in this one variable.

cmp -l prints the byte number in decimal, counting from 1, then the old and new values in octal. So 114 is not 114. It's 0o114 = 76 = 0x4C. And to get the data offset, subtract 1 for cmp's counting and 4 for the attribute word. File byte 322 is data+317.

Translated:

cmp bytedata offsetold → newmeaning
322+3170 → 1PPT Control: Auto → Manual
323–325+318..321→ 08 4C 01Fast PPT Limit, u32 little-endian
327–329+322..325→ 08 4C 01Slow PPT Limit, same encoding
331+3260 → 5Slow PPT Time Constant, seconds

Each limit field shows only three changed bytes because the fourth was zero before and is still zero. cmp lists changes, not fields. xxd shows the whole run, from data+317 to data+326:

$ xxd -s 321 -l 10 20261010-0048-ppt85-uma05/AmdSetup-*
00000141: 0108 4c01 0008 4c01 0005                 ..L...L...

08 4C 01 00, read little-endian, is 0x00014C08 = 85,000. Milliwatts. That's the number I typed into the menu.

The snapshot that changed two settings#

The method says one setting at a time. This snapshot changed two. Optimized Defaults had put Dedicated Graphics Memory back to 64 G, so the same BIOS visit set it to 0.5 G as well as the PPT fields. That's the top two lines of the diff:

  • data+244, 0xFF → 3: likely the Dedicated Graphics Memory selector. It was 3 in the first snapshot, when the box ran at 0.5 G, went to 0xFF when Optimized Defaults restored the 64 G default, and came back to 3 with 0.5 G selected again.
  • data+257, 2 → 0: it moved at the same time, and I don't know what it is.

The PPT fields were safe because 85,000 identifies itself: type 85000 into a menu, and a four-byte field that becomes 85,000 isn't hard to name. A one-byte enum doesn't have that luxury. So "likely" stays on +244 until it gets a diff of its own.

The diff before this one was worse. Optimized Defaults, with the 85W preset re-selected afterwards, moved eight bytes in AmdSetup at once, most of them to 0xFF. Apart from +244, none of them is decoded. Changing one setting per diff is the whole method. A BIOS reset changes everything that wasn't at its default, all in one diff, and tells you almost nothing.

The 85W preset that changes nothing#

System Configuration is the obvious power knob. It sits in AMD CBS → SMU Common Options and comes with a warning in its help text, as transcribed from the filmed setup screen:

Warning: Select System Configuration may cause the system to hang, as some System Configuration may not be supported by your OPN.

I selected 85W. The system did not hang. Nothing else happened either: the quick benchmark's sustained CPU stress after that boot averaged 120.7 W package power, and a later 60 s stress with 85W still selected gave 120.1 W, Tctl peaking at 80.4 °C. That's the uncapped number.

Where the preset stores itself, I can't tell you. The first snapshot was taken after it was already set, so there's no before to diff against. All I know is what it does, and it does nothing.

PPT Control is the knob that works. Manual, Fast and Slow PPT at 85000 mW, time constant 5 s, then the same 60 s stress:

  • package power: 85.0 W mean (was ~120)
  • Tctl max: 65.5 °C (was ~80)
  • CPU clock: 4084 MHz average (was ~4540, about −10 %)

Linux doesn't offer a softer route here: there's no ACPI platform_profile on this box, likely because PMF Device Support is Disabled in the hidden menus.

What 85 W costs llama.cpp: mostly prefill#

The same quick benchmark ran with PPT at 85 W, against two baselines with no power limit. One ran at 20:44. The other ran at 23:37 with the 85W preset selected, and since the preset is a no-op, that's a second control for free. Vulkan rows, tok/s:

Row20:4423:37PPT 85 WΔ vs 20:44Δ vs 23:37
Qwen3-30B-A3B pp5121656.571660.641472.61−11.1 %−11.3 %
Qwen3-30B-A3B tg12889.4992.8488.84−0.7 %−4.3 %
gpt-oss-120b pp5121065.341070.92923.35−13.3 %−13.8 %
gpt-oss-120b tg12854.3055.5553.59−1.3 %−3.5 %
gpt-oss-20b pp5122261.462258.721920.79−15.1 %−15.0 %
gpt-oss-20b tg12875.6178.4074.97−0.8 %−4.4 %
llama-2-7b pp5121722.041700.011488.91−13.5 %−12.4 %
llama-2-7b tg12853.1054.0052.50−1.1 %−2.8 %

Prefill loses 11–15 % on Vulkan, and 15–18 % on ROCm. It's the same against either baseline.

One caveat on the capped run: the harness logged WARN: 1-min loadavg 15.93 > 1 before start: something else is running, numbers will be low. Both baselines started below 0.2. The "something else" was most likely the 60 s PPT stress test run just before it. Idle power at the start read 7.4 W against 6.7–6.8 W for the baselines, and the single-thread 7z score didn't move (7347 against 7323 and 7321). The llama.cpp rows didn't start until 00:55:56, nearly six minutes after the warning, so a leftover one-minute load average had long decayed by then. Still, it's a reason to read the smaller deltas below with care. The prefill loss is far larger than anything that leftover explains.

Generation is murkier. Against 20:44, Vulkan tg loses about 1 %. Against 23:37, it loses 3–4 %. The disagreement is the finding: the two controls disagree with each other by up to 3.7 % (Qwen3-30B tg, 89.49 vs 92.84) with no setting changed between them. On Vulkan, the tg "cost" of 85 W is about the size of the run-to-run noise. ROCm is the exception: its two baselines agree within 0.4 % on tg, and at 85 W its tg drops 2.0–6.6 %. Small, and likely real, with the load-average caveat above.

That split fits the usual reading of LLM inference: prefill is compute-bound and feels the clock cut, decode is memory-bound and mostly doesn't. The 35 W saving from the stress test doesn't carry over either. What the cap clips is the peaks: 76–95 W at 20:44, 67–81 W under the cap. For long-context work, prefill is the wait. PPT went back to Auto at 01:08 on 2026-10-10, before the long-context model runs.

The F3 trap: Optimized Defaults restores 64 G of graphics memory#

One more trap turned up along the way: F3, Optimized Defaults, puts Dedicated Graphics Memory back to 64 G, quietly undoing the previous part's setup. In the byte diff, +244, the likely selector, went to 0xFF.

Ten bytes with names#

Curve Optimizer is next, and it isn't tried yet. It's an AMD Overclocking setting, and those are stored in AodSetupStxh, a separate 805-byte variable. The plan is the same loop: one setting, one snapshot, one cmp -l.

AmdSetup carries 1,522 bytes of settings. So far, ten of them have names.

Filed under: amd, bios, uefi, firmware, linux

Sources#