
BC-250 Cores Bisect¶
Tells bad CPU cores apart from an unstable core unlock on the AMD BC-250 (Cyan Skillfish), before you trust or persist the 8C/16T unlock.
The BC-250 ships with 6 of its 8 Zen 2 cores enabled (presence mask 0x77: core 3 of each CCX fused
off, symmetric — which smells like product segmentation, not harvesting). The SMU exposes a quirk
(queue-3 message 0x98) that writes 0xFF to any SMN address, including the core presence mask at
SMN 0x5A870, which the host can't write. That enables all 8 cores on the next warm reboot.
The catch: that primitive is all-or-nothing. Unlike a GPU CU unlock there is no per-core mask, so you can't leave one bad core disabled. If either extra core is bad, the unlock is unusable on your board — and you want to know that before you persist it and let a flaky core corrupt data.
bc250-cores-bisect.sh does its own unlock, so no other tool is involved, and tests in a way that
separates the possible causes. Sibling project of
bc250-cu-bisect, which does the same for the GPU CUs.
Warning: this talks to the SMU and runs heavy CPU load. A bad core can freeze the system or corrupt data. Save your work first. Use at your own risk.
How it works¶
Since the cores can't be enabled one at a time, the isolation happens after the unlock, with the
load pinned (taskset) to exactly the threads under test:
| Item | When | Pinned to | Separates |
|---|---|---|---|
control |
before unlock | all stock cores (6C/12T) | board / power / heat / load tool problems |
post-control |
after unlock | one stock core (2 threads) | "the unlock destabilises the SoC" from bad cores |
coreN |
after unlock | one new core on its own | which new core (if any) is bad |
combined |
after unlock | both new cores together | cores that pass alone but fail together |
core0 core1 core2 core4 core5 core6 core3 core7
██ ██ ██ ██ ██ ██ .. ..
██ stock, always enabled (6C/12T) .. fused off, enabled by the unlock, tested one by one
The order isn't a typo: the map groups the stock cores first and the unlocked ones last, where their
verdict cells live. Why cores 3 and 7 specifically? The mask 0x77 is binary 0111 0111 — one
nibble per 4-core CCX, and in each CCX it's core 3 that is fused off. CCX0 keeps cores 0,1,2 and
CCX1 keeps 4,5,6; the unlock adds core 3 (CCX0) and core 7 (CCX1's "core 3"). That symmetry — the
same position disabled in both CCXs — is what smells like product segmentation rather than harvesting
of defective silicon.
The script shows this map live, with >> on the core under test and ok/xx/?? once a core has a
verdict (GOOD / FAILS EVERY TIME / RANDOM).
Failures are detected four ways:
- Machine-check events in the kernel log (
journalctl -k) — the classic symptom of marginal cores. - Wrong results, not only crashes:
stress-ng --verifyrecomputes and checks every stressor result. - Full crashes, crash-safe: before every attempt the script saves what it's doing. After a freeze, reboot and run it again: it records the crash and shows the MCE lines of the previous boot.
- Optional burn-in extras:
mprime's torture test (--load mprime|both) and hardware error counters fromrasdaemon(--rasdaemon); see Longer burn-in.
The test plan, same philosophy as the CU bisect:
- Control first: the unlock is only offered after the stock cores pass every control round. A failing control means nothing can be concluded about the new cores.
- Rounds: every item runs several times (default 3), interleaved, so heat and time don't favor one.
- One attempt per boot (default): warm reboots preserve the unlock, so isolation costs nothing.
- State machine across boots: control → unlock → verify the cores actually came up → bisect →
verdict. A cold boot mid-run is detected (mask back to
0x77) and handled. - Mask gate: boards whose stock mask isn't
0x77are refused — a different mask strongly suggests genuinely defective silicon that was purposefully skipped.
The verdict and the decision¶
controlfails → board, power, heat or load tool. Fix that first.post-controlfails → the unlock destabilises even the stock cores on your board. Cold boot to revert; don't persist.- A core FAILS EVERY TIME → likely genuinely bad silicon. Because the unlock is all-or-nothing: don't use it at all. Cold boot (full power off) reverts everything.
- RANDOM failures → points to power or heat, not silicon. Improve cooling and retest.
- Everything GOOD, alone and combined → the unlock looks safe; persist it (below).
Requirements¶
- AMD BC-250, Linux with bash 4+. Tested against Bazzite (Fedora Atomic).
setpci(pciutils) — SMN/SMU access goes through the PCI config space of0000:00:00.0.stress-ngandtaskset(util-linux).- sudo rights (for setpci and the kernel log only).
- Optional:
mprime(Prime95) for the torture test andrasdaemon(ras-mc-ctl) for hardware error counters. Both are only needed if you ask for them.
Before you start¶
- Start from a cold boot (full power off) so the stock
0x77state is guaranteed. - Close background workloads; the load must land only on the pinned cores.
- The script stops
cyan-skillfish-governor-smufor the few milliseconds of the SMU write (the governor shares the mailbox) and restarts it afterwards.
Usage¶
Run it as your normal user:
chmod +x bc250-cores-bisect.sh
./bc250-cores-bisect.sh # 3 rounds, 600 s verified load per attempt
./bc250-cores-bisect.sh -t 1800 -r 5 # longer load, more rounds (recommended before trusting it)
./bc250-cores-bisect.sh --same-boot # no reboots between bisect attempts (faster, less isolation)
./bc250-cores-bisect.sh --auto # no prompts, auto-reboot, continues after every login
./bc250-cores-bisect.sh --load both # stress-ng *and* the mprime torture test per attempt
./bc250-cores-bisect.sh --rasdaemon # also count hardware errors via ras-mc-ctl
./bc250-cores-bisect.sh --status # results so far, report + logs in ~/Desktop/bc250-cores-bisect/
./bc250-cores-bisect.sh --reset # delete the results and start over
./bc250-cores-bisect.sh --help
Flow: run → control rounds (one per boot) → it offers the unlock → warm reboot → it detects the new cores → bisect rounds (one per warm reboot) → final summary, report and decision.
At the end of a run (and on every --status) everything is copied to ~/Desktop/bc250-cores-bisect/
for easy sharing:
| File | Contents |
|---|---|
bc250-cores-bisect-results-<date>.txt |
The report: core map, verdicts, decision, every attempt. |
runs.tsv |
The raw results table, one line per attempt. |
config |
The run's settings and detected cores. |
logs/<date>-r<round>-<item>.log |
stress-ng output of each attempt (verification failures etc). |
logs/<date>-r<round>-<item>-mprime.log |
mprime torture output, when mprime is part of the load. |
ras-mc-ctl-errors.txt |
rasdaemon's error table, when --rasdaemon is used. |
auto.log |
Everything the script printed in --auto runs. |
The working copies live in ~/.local/share/bc250-cores-bisect/; --reset deletes those, not the
Desktop folder.
Pick -t/-r before you start and keep them. You can change them mid-run (results so far are
kept and the script asks you to confirm), but the evidence is then mixed: the report lists the load of
every attempt, and a control that finished with fewer rounds keeps them. For a uniform result, use
--reset and start over.
For --auto, install the autostart unit (see bc250-cores-bisect-auto.service.example) so the script
resumes after every login.
Running truly unattended (--auto checklist)¶
--auto makes the script hands-off, but the system must get back to your session on its own:
- Autologin — after each reboot someone must log in, or the run stalls at the login screen. Enable it in your desktop (Bazzite/KDE: System Settings → Colors & Themes → Login Screen (SDDM) → Behavior → auto-login, or Users → auto-login on GNOME).
- Passwordless sudo for your user — the script needs
setpciandjournalctl -kwithout a prompt (there is no terminal to type a password into when it runs from the unit):
echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/bc250-bisect
sudo chmod 440 /etc/sudoers.d/bc250-bisect
sudo visudo -cf /etc/sudoers.d/bc250-bisect # must print "parsed OK"
sudo -k && sudo -n true && echo OK # must print OK without asking
sudo rm /etc/sudoers.d/bc250-bisect.
If you edit the file by hand, use sudo visudo -f /etc/sudoers.d/bc250-bisect: it refuses to save a broken line.
A broken line makes every sudo command print a warning, or stops sudo working at all.
3. The autostart unit installed — without it nothing restarts the script after login and you'd
have to start it manually every boot.
4. No credential popups at login (KDE Wallet / ksshaskpass) — with autologin the KDE Wallet
can't auto-unlock, so a "SSH credentials" / wallet dialog can appear at session start and block
the chain until answered. Either give the wallet a blank password (open it in kwalletmanager5
first, then Change Password → empty), or disable the wallet subsystem (Plasma 6 has no UI
checkbox for it anymore):
kwriteconfig6 --file kwalletrc --group Wallet --key Enabled false # kwriteconfig5 on Plasma 5
... --key Enabled true. Also uncheck "Use KWallet for the Secret
Service interface" in System Settings → KDE Wallet so apps don't route secrets to the disabled
wallet.
Check on it while it runs:
systemctl --user status bc250-cores-bisect-auto.service # ran / running / failed this boot
tail -f ~/.local/share/bc250-cores-bisect/auto.log # live output of the current attempt
./bc250-cores-bisect.sh --status # progress and results so far
When all attempts are done the script stops rebooting on its own; in --auto mode it also disables
the autostart unit and removes the live-output entry (below) by itself, so nothing starts at the next
login. If you ran without --auto, disable the unit manually
(systemctl --user disable --now bc250-cores-bisect-auto.service).
Want the live output on the board's screen instead of a shell? Add a desktop autostart entry that
opens a terminal following the log at every login (KDE example; see the comments in
bc250-cores-bisect-auto.service.example):
cat > ~/.config/autostart/bc250-cores-bisect-watch.desktop <<'EOF'
[Desktop Entry]
Type=Application
Name=BC-250 bisect live output
Exec=konsole --title "bc250-cores-bisect" -e tail -n 40 -f .local/share/bc250-cores-bisect/auto.log
EOF
How long does it take?¶
A full run is 5 × rounds attempts (control, then post-control + core3 + core7 + combined), each
running the load for --time seconds, plus one warm reboot for the unlock. In the default
one-attempt-per-boot mode add a reboot-and-login (~1–2 min) per attempt; with --same-boot only 2–3
reboots total plus a 30 s cooldown between attempts. Indications:
| Invocation | Attempts | Pure load | Wall clock (approx.) |
|---|---|---|---|
default (-t 600 -r 3) |
15 | 2 h 30 m | ~3 h (15 reboots) |
--same-boot (-t 600 -r 3) |
15 | 2 h 30 m | ~2 h 45 m (3 reboots) |
-t 1800 -r 5 (recommended) |
25 | 12 h 30 m | ~13 h (25 reboots) |
-t 1800 -r 5 --same-boot |
25 | 12 h 30 m | ~12 h 45 m (3 reboots) |
The longer runs are meant to be unattended: combine them with --auto (and the autostart unit) and
let it finish overnight. You don't have to do it in one sitting either — the state survives reboots,
so run the script again at any time and it continues where it left off (--status shows progress).
Longer burn-in (mprime / rasdaemon)¶
stress-ng --verify is the default load and is enough for most boards. If you want a harder,
longer burn-in before you trust the unlock, two optional tools can be folded in:
./bc250-cores-bisect.sh --load mprime # mprime torture test instead of stress-ng
./bc250-cores-bisect.sh --load both # stress-ng first, then mprime
./bc250-cores-bisect.sh --mprime-bin ~/mprime/mprime
./bc250-cores-bisect.sh --rasdaemon # count hardware errors around every attempt
./bc250-cores-bisect.sh -t 1800 -r 5 --load both --rasdaemon --auto --same-boot
--load stress-ng|mprime|both(defaultstress-ng).mprimeruns Prime95's torture test (StressTester=1, no PrimeNet) pinned to exactly the same CPUs as stress-ng, from a private working directory under~/.local/share/bc250-cores-bisect/mprime/<item>/. It is an AVX/FMA-heavy, in-cache FFT workload, which catches a different class of marginal silicon than stress-ng's mixed stressors. A torture error gives the attempt the outcomeMPRIME-FAIL.- With
--load boththe two tools run one after the other, never at the same time, so they don't fight over the same threads. Each attempt therefore takes 2 ×--time— budget double the wall clock from the table above. --mprime-bin PATHpoints at an mprime binary that isn't on$PATH(the official builds from mersenne.org are a plain tarball, so this is the normal case).--rasdaemonreadsras-mc-ctl --errorsbefore and after every attempt and records the difference. Any new hardware error makes the attemptRAS-ERROR. It needs a runningrasdaemonservice; ifras-mc-ctlis missing or unusable the script says so and carries on without it. The full error table is exported asras-mc-ctl-errors.txt.- Both extras are recorded per attempt:
runs.tsvgained a load tool and a rasdaemon errors column, and the report lists the load of every attempt, so a mixed run stays readable. Older result files from before this feature are still read correctly.
Like -t/-r, pick these before you start: the settings are stored in config, reused after
every reboot, and carried into the --auto service's command line.
Making it persistent¶
Only after a clean verdict:
sudo ./bc250-cores-unlock.sh --install
The unlock is volatile: it survives warm reboots, a full power off reverts it, and a fresh write only
takes effect on the next reboot. So --install copies the script to /usr/local/sbin (root-owned)
and enables a root service that checks the mask early at every boot; after a cold boot it re-applies
the unlock and warm-reboots once (a guard file prevents reboot loops if the unlock ever stops
taking effect). --uninstall removes the service and the copy; the stock 6C/12T returns after the
next full power off. --status shows mask, threads, service and guard state.
A BIOS-mod route exists for true permanence (RescueMei/BC250-DXEv3-BIOSMOD) but is out of scope here: this project stays software-only and reversible.
GUI¶

Two separate PyQt6 front-ends ship with the project: a setup GUI that starts a bisect run, and an unlock GUI for the persistence step afterwards. Both are optional — everything they do can be typed by hand.
Recommended: install.sh (from a clone or the extracted release tarball). It installs both
GUIs at once: a root-owned copy of the toolset in /opt/bc250-cores-bisect-v<version> (with
/opt/bc250-cores-bisect pointing at it), a private PyQt6 venv in
~/.local/share/bc250-cores-bisect-app/venv, both launchers in ~/.local/bin, app menu entries
and Desktop icons. Run it as your own user — only the copy to /opt asks for the sudo password.
./install.sh # install or update both GUIs
./install.sh --bisect-only # or --unlock-only
./install.sh --no-desktop-shortcut # app menu entries only
./install.sh --uninstall # remove the apps; keeps results, settings and logs
./install.sh --uninstall --purge # also remove results, settings, logs and the autostart unit
Your bisect results stay in ~/.local/share/bc250-cores-bisect; the installer never touches that
folder unless you pass --purge. Installing also clears out any older per-user install made by the
packaging/bazzite/ scripts below, so you don't end up with two menu entries per GUI.
Setup GUI (cores_bisect)¶
A one-screen "initial setup" for bc250-cores-bisect.sh: pick the load time, the rounds per item,
the load tool and the two run-mode switches (--same-boot, --auto), then click
Start Cores Bisect. The
window closes and the real run continues in a terminal, exactly as if you had typed the command —
the GUI only assembles the command line, so the script stays the single source of truth.
- An estimate of the total runtime updates as you change the options.
- Load tool picks between stress-ng, mprime and both; Also count hardware errors with
rasdaemon adds
--rasdaemon. See Longer burn-in. - Unattended can also install and enable the auto-resume login service for you
(
~/.config/systemd/user/bc250-cores-bisect-auto.service), the same thing the--autochecklist describes. The script removes it again once every item is done. - Show status (
--status) and Reset (--reset) run in a terminal too, so the script can still ask for its own confirmation. - Help shows the script's own
--help, so it can never drift out of sync.
Per-user alternative, one GUI only (no /opt, nothing root-owned):
bash packaging/bazzite/install-bisect-gui.sh # venv + launcher + menu entry "BC-250 Cores Bisect"
bash packaging/bazzite/uninstall-bisect-gui.sh # removes all of that again (keeps your results)
Installed per user under ~/.local/share/bc250-cores-bisect-gui, launcher
~/.local/bin/bc250-cores-bisect-gui.
Unlock GUI (cores_unlock)¶
cores_unlock is a small PyQt6 front-end for the persistence step.
It doesn't run the bisect — that stays a terminal job, because it reboots and runs for hours — but
it reads the results that bc250-cores-bisect.sh left in ~/.local/share/bc250-cores-bisect and
puts --install behind a verdict gate:
- ACCEPTED only when the bisect phase is finished, no attempt is still in progress, control and post-control are clean, and every new core and the combined item passed every round. Notes (e.g. control ran fewer rounds, or loads changed mid-run) are shown but don't block.
- NOT ACCEPTED YET otherwise, with the reason (unfinished, failing core, random failures, …). Install stays disabled; Uninstall, Refresh status, Status with sudo and Re-check always work.
- A core map shows the 6 stock cores and the 2 unlocked ones (
ok/xxfails /??random /..not tested).
Install/Uninstall run bc250-cores-unlock.sh via sudo; you're only asked for a password when sudo
actually needs one. Refresh status runs unprivileged (the mask is shown if sudo needs no password);
Status with sudo runs the same status as root, asking for the password if needed, so the mask is always shown.
Bazzite (or any desktop, no root or rpm-ostree needed): per-user alternative to install.sh,
this GUI only:
bash packaging/bazzite/install-gui.sh # venv + launcher + menu entry "BC-250 Cores Unlock"
bash packaging/bazzite/uninstall-gui.sh # removes all of that again (not the unlock service)
It installs per user under ~/.local/share/bc250-cores-unlock-gui, with the launcher
~/.local/bin/bc250-cores-unlock-gui. The names don't clash with each other or with the
bc250-cu-bisect GUIs, so they can all be installed side by side.
From a venv (development):
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m cores_bisect # --script PATH to use another bc250-cores-bisect.sh
.venv/bin/python -m cores_unlock # --script PATH to use another bc250-cores-unlock.sh
Known side effect¶
After the unlock, pp_dpm_sclk and hwmon freq1_input report nonsense GPU clocks (tens of MHz).
This is cosmetic — the SMU's own clock getters stay correct — and this script ignores GPU clocks.
Diagnostics that read those files (e.g. tests 19/40 of
Bazzite Test) will
flag it; that's expected while the unlock is active.
Credits¶
The SMU quirk (queue-3 message 0x98 → 0xFF at SMN 0x5A870) was discovered and documented by
rw-r-r-0644/bc250-core-unlock (MIT). This project
is an independent implementation built around those hardware facts, adding the control/bisect/verdict
methodology, crash-safe multi-boot state, and reversible persistence.
License¶
GNU GPLv3, see LICENSE.