BC-250 console image: custom GNOME recipe with remote admin
Build Bazzite BC-250 / Check Bazzite channel digests (push) Has been cancelled
Build Bazzite BC-250 / Build Custom Image (push) Has been cancelled
Build Bazzite BC-250 / Build Custom 40CU Image (push) Has been cancelled
Build Bazzite BC-250 / Save Bazzite channel digest cache (push) Has been cancelled
Build Bazzite BC-250 / Publish GitHub Release (push) Has been cancelled
Build Bazzite BC-250 / Check Bazzite channel digests (push) Has been cancelled
Build Bazzite BC-250 / Build Custom Image (push) Has been cancelled
Build Bazzite BC-250 / Build Custom 40CU Image (push) Has been cancelled
Build Bazzite BC-250 / Save Bazzite channel digest cache (push) Has been cancelled
Build Bazzite BC-250 / Publish GitHub Release (push) Has been cancelled
Based on 62fixolab/Latest-Bazzite-AMD-BC-250-Patched-Images @ 347fd4d. Adds on top of the fork: - recipes/bc250-console-gnome.yml: governor + gnome-remote-desktop + openssh-server, sshd enabled, hhd.service masked, no signing module (local build + ISO path) - files/console/usr/bin/bc250-remote-setup: one-time on-box SSH/RDP setup - files/console/usr/lib/bootc/kargs.d/50-bc250-ttm.toml: ttm memory kargs - BUILD-CONSOLE.md: build -> ISO -> validation instructions files/console/ is separate from files/system/ so the 40-CU unlock tooling stays out of this stable 24-CU image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
6c3969ddee40e894297869e6ca30537f274619cb
|
||||
Vendored
+213
@@ -0,0 +1,213 @@
|
||||
# BC-250 40 CU Unlock
|
||||
|
||||
Re-enable all 40 CUs on the AMD BC-250 (gfx1013 / Cyan Skillfish / salvaged PS5 APU).
|
||||
|
||||
The BC-250 ships with 24 of 40 RDNA2 CUs active. This patch unlocks all 40 by writing two hardware registers during amdgpu driver init. No firmware mods, no permanent changes — just a kernel module parameter.
|
||||
|
||||
## Results
|
||||
|
||||
**pp512 (Vulkan LLM inference, Qwen3.5-9B Q4_K_XL):**
|
||||
|
||||
| Config | pp512 tok/s | Power | Temp | SCLK |
|
||||
|--------|------------|-------|------|------|
|
||||
| Stock 24 CU | 230 | 95W | 79C | 1500MHz |
|
||||
| **40 CU unlocked** | **372** | **125W** | **83C** | **1500MHz** |
|
||||
| **Ratio** | **1.61x** | +30W | +4C | same |
|
||||
|
||||
At 2 GHz (governor default): 302 → 466 tok/s = 1.54x, but hits 96C. 1500 MHz / 900 mV is the recommended sweet spot.
|
||||
|
||||
## How It Works
|
||||
|
||||
Two registers control CU availability — both must be modified:
|
||||
|
||||
| Register | What it does | Stock | Unlocked |
|
||||
|----------|-------------|-------|----------|
|
||||
| `CC_GC_SHADER_ARRAY_CONFIG` | Enumeration mask (tells driver how many CUs) | `0xfff80000` (24 CU) | `0xffe00000` (40 CU) |
|
||||
| `SPI_PG_ENABLE_STATIC_WGP_MASK` | Dispatch gate (tells SPI where to send waves) | `0x7` (WGP 0-2) | `0x1F` (WGP 0-4) |
|
||||
|
||||
**Neither alone is sufficient.** CC alone changes what the driver reports but SPI still dispatches to 24 CUs. SPI alone enables hardware dispatch but the driver only generates work for 24 CUs.
|
||||
|
||||
The patch writes both during `gfx_v10_0_get_cu_info()`, guarded by `device == 0x13FE` (BC-250 only) and `bc250_cc_write_mode=3` (off by default).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Build Script (any distro)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/duggasco/bc250-40cu-unlock.git
|
||||
cd bc250-40cu-unlock
|
||||
sudo ./scripts/bc250-enable-40cu.sh build
|
||||
sudo ./scripts/bc250-enable-40cu.sh enable # reboots
|
||||
```
|
||||
|
||||
Requirements: `gcc`, `make`, `zstd`, kernel headers (`linux-headers-$(uname -r)`)
|
||||
|
||||
### Option 2: Apply Patch Manually
|
||||
|
||||
```bash
|
||||
# Get your kernel source
|
||||
cd /path/to/linux-source/drivers/gpu/drm/amd/amdgpu/
|
||||
|
||||
# Apply
|
||||
patch -p5 < /path/to/bc250-40cu-unlock/patch/bc250-40cu-amdgpu.patch
|
||||
|
||||
# Build just amdgpu
|
||||
make -C /lib/modules/$(uname -r)/build M=$(pwd) -j$(nproc) modules
|
||||
|
||||
# Install
|
||||
sudo cp amdgpu.ko.zst /lib/modules/$(uname -r)/kernel/drivers/gpu/drm/amd/amdgpu/
|
||||
sudo depmod -a
|
||||
|
||||
# Enable
|
||||
echo 'options amdgpu bc250_cc_write_mode=3' | sudo tee /etc/modprobe.d/bc250-40cu.conf
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
### Option 3: CachyOS / Arch
|
||||
|
||||
Apply `patch/bc250-40cu-amdgpu.patch` to your kernel PKGBUILD patch set, rebuild, add the modprobe config.
|
||||
|
||||
## Verification
|
||||
|
||||
After reboot:
|
||||
|
||||
```bash
|
||||
# Check CU count
|
||||
dmesg | grep active_cu_number
|
||||
# Expected: active_cu_number 40
|
||||
|
||||
# Check register writes
|
||||
dmesg | grep bc250-40cu
|
||||
# Expected: bc250-40cu-enable: mode=3 se=0 sh=0 CC=0xfff80000->0xffe00000 SPI=0x00000007->0x0000001f
|
||||
|
||||
# Check RADV
|
||||
RADV_DEBUG=info vulkaninfo --summary 2>&1 | grep num_cu
|
||||
# Expected: num_cu = 40
|
||||
```
|
||||
|
||||
## CU Harvest Map
|
||||
|
||||
Check your board's stock CU layout (run without the patch):
|
||||
|
||||
```bash
|
||||
./scripts/cu_map.sh
|
||||
```
|
||||
|
||||
Our boards show contiguous harvesting:
|
||||
```
|
||||
SE0 SH0: ■■■■■■□□□□
|
||||
SE0 SH1: ■■■■■■□□□□
|
||||
SE1 SH0: ■■■■■■□□□□
|
||||
SE1 SH1: ■■■■■■□□□□
|
||||
24/40 CUs active, 16 harvested
|
||||
```
|
||||
|
||||
We're collecting maps from across the fleet to find out if all BC-250s share this pattern.
|
||||
|
||||
## Governor / Thermal
|
||||
|
||||
40 CU at 2 GHz draws ~181W and hits 96C. Recommended: cap at 1500 MHz / 900 mV via `cyan-skillfish-governor`:
|
||||
|
||||
```toml
|
||||
# /etc/cyan-skillfish-governor/config.toml
|
||||
[[safe-points]]
|
||||
frequency = 350
|
||||
voltage = 700
|
||||
|
||||
[[safe-points]]
|
||||
frequency = 1500
|
||||
voltage = 900
|
||||
```
|
||||
|
||||
## Selective CU Masking
|
||||
|
||||
Not all unlocked CUs may be healthy — boards with scattered harvest patterns (`■■□□■■□□■■`) may have defective silicon. You can enable all 40 CUs but selectively mask bad ones via `amdgpu.disable_cu`.
|
||||
|
||||
### WGP / CU Mapping (per shader array)
|
||||
|
||||
```
|
||||
WGP 0 = CU 0,1 (stock active)
|
||||
WGP 1 = CU 2,3 (stock active)
|
||||
WGP 2 = CU 4,5 (stock active)
|
||||
WGP 3 = CU 6,7 (unlocked — test these)
|
||||
WGP 4 = CU 8,9 (unlocked — test these)
|
||||
|
||||
WGP CU Map Preview Example:
|
||||
0 1 2 3 4
|
||||
■■■■■■□□□□
|
||||
```
|
||||
|
||||
Disabling works at **WGP granularity** — disabling CU 6 also disables CU 7 (same WGP).
|
||||
|
||||
Format: `amdgpu.disable_cu=SE.SH.WGP` (comma-separated, added to modprobe config)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Enable all 40, but mask WGP 3 in SE1/SH0 (CUs 6-7) — gives 38 CUs
|
||||
options amdgpu bc250_cc_write_mode=3 disable_cu=1.0.3
|
||||
|
||||
# Mask WGP 4 across all shader arrays — gives 32 CUs
|
||||
options amdgpu bc250_cc_write_mode=3 disable_cu=0.0.4,0.1.4,1.0.4,1.1.4
|
||||
```
|
||||
|
||||
### Automated Health Testing
|
||||
|
||||
```bash
|
||||
# Run per-WGP isolation test (20 reboots, tests each WGP individually)
|
||||
sudo ./scripts/bc250-cu-health-test.sh start
|
||||
|
||||
# Quick correctness test on current config (no reboot)
|
||||
./scripts/bc250-compute-verify.sh
|
||||
|
||||
# Generate disable_cu config from health results
|
||||
./scripts/bc250-cu-mask.sh --results /var/lib/bc250-cu-health-test/results.tsv
|
||||
|
||||
# Install the mask (adds to modprobe config)
|
||||
sudo ./scripts/bc250-cu-mask.sh --results /var/lib/bc250-cu-health-test/results.tsv --install
|
||||
|
||||
# View harvest map with health overlay
|
||||
./scripts/cu_map.sh --health /var/lib/bc250-cu-health-test/results.tsv
|
||||
```
|
||||
|
||||
## Disabling
|
||||
|
||||
```bash
|
||||
sudo ./scripts/bc250-enable-40cu.sh disable # removes config, reboots to 24 CU
|
||||
sudo ./scripts/bc250-enable-40cu.sh restore # restores original amdgpu module
|
||||
```
|
||||
|
||||
## Whitepaper
|
||||
|
||||
The full academic writeup is available as a PDF:
|
||||
|
||||
**[Re-enabling Fused-Off Compute Units on the AMD BC-250 APU via Register-Level Modification](docs/whitepaper-cu-unlock.pdf)** (8 pages)
|
||||
|
||||
Covers the complete methodology, 4-state controlled experiment, community harvest map survey (n=58), performance characterization, and dual-register gating architecture analysis. LaTeX source included at [docs/whitepaper-cu-unlock.tex](docs/whitepaper-cu-unlock.tex).
|
||||
|
||||
## Technical Details
|
||||
|
||||
See [docs/technical-report.md](docs/technical-report.md) for additional technical notes including:
|
||||
- Register map (UMR dumps)
|
||||
- Architecture analysis (CC vs SPI vs RLC vs SMU)
|
||||
- Why `ignore_cu_harvest` doesn't work
|
||||
- Power/thermal characterization
|
||||
|
||||
## Safety
|
||||
|
||||
- Default off (`bc250_cc_write_mode=0`) — does nothing unless explicitly enabled
|
||||
- Guarded by PCI device ID `0x13FE` — only fires on BC-250
|
||||
- No permanent hardware changes — reboot without the config returns to stock 24 CU
|
||||
- The harvested CUs have power, clocks, and matching CGTS config — they were disabled by firmware policy, not silicon defects (RLC_PG_CNTL = 0, no power gating active)
|
||||
|
||||
## Credits
|
||||
|
||||
- **duggasco** — research, testing, documentation
|
||||
- **filippor** — independent testing, `ignore_cu_harvest` kernel patch, cyan-skillfish-governor
|
||||
- **Claude** — analysis, tooling, SPI register discovery
|
||||
- **Codex** — identified SPI_PG_ENABLE_STATIC_WGP_MASK architecture
|
||||
- **BC-250 Discord** — thermal/voltage guidance, fleet testing
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0 (same as the Linux kernel)
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# BC-250 40 CU Re-enablement — Technical Summary
|
||||
|
||||
Date: 2026-05-18
|
||||
|
||||
## Result
|
||||
|
||||
40 physical CUs on BC-250 (gfx1013, Cyan Skillfish) re-enabled with **1.61x compute scaling** at 1500 MHz, verified via controlled A/B/A testing.
|
||||
|
||||
## Two Registers Required
|
||||
|
||||
Neither alone is sufficient. Both must be modified:
|
||||
|
||||
1. **CC_GC_SHADER_ARRAY_CONFIG** (GC offset 0x0226f) — enumeration mask
|
||||
- Tells amdgpu/RADV/KFD how many CUs exist
|
||||
- Stock: `0xfff80000` (WGP 3-4 inactive) → Cleared: `0xffe00000` (all WGPs active)
|
||||
- Set via patched amdgpu kernel module (`bc250_cc_write_mode=3`)
|
||||
|
||||
2. **SPI_PG_ENABLE_STATIC_WGP_MASK** (GC offset 0x1277) — hardware dispatch gate
|
||||
- Controls which WGPs the SPI sends wavefronts to
|
||||
- Stock: `0x7` (WGP 0-2 only) → Enabled: `0x1F` (WGP 0-4)
|
||||
- No Linux driver code touches this register; firmware sets it at boot
|
||||
- Set via UMR post-boot: `umr -w cyan_skillfish.gfx1013.mmSPI_PG_ENABLE_STATIC_WGP_MASK 0x1f`
|
||||
- Also set: `umr -w cyan_skillfish.gfx1013.mmRLC_PG_ALWAYS_ON_WGP_MASK 0x1f`
|
||||
|
||||
**Why both:** CC tells RADV "40 CUs exist" so it generates dispatches for all of them. SPI tells the hardware "route waves to all 5 WGPs." CC alone = driver sees 40 but SPI only dispatches to 24. SPI alone = hardware allows 40 but RADV only generates work for 24.
|
||||
|
||||
## 4-State A/B Test (bc250-2, pp512 @ 2 GHz)
|
||||
|
||||
```
|
||||
State CC SPI Enum Dispatch pp512 tok/s Power SCLK Temp
|
||||
1 0 0x07 24 24 302 56W 1000MHz 73C
|
||||
2 0 0x1F 24 40 302 140W 2000MHz 91C
|
||||
3 3 0x07 40 24 302 55W 1000MHz 74C
|
||||
4 3 0x1F 40 40 466 181W 2000MHz 96C
|
||||
```
|
||||
|
||||
## A/B/A at 1500 MHz / 900 mV (sweet spot)
|
||||
|
||||
```
|
||||
pp512 tok/s SCLK Voltage Power Temp
|
||||
24 CU (A): 230.38 1500MHz 881mV 95W 67→79C
|
||||
40 CU (B): 371.60 1500MHz 874mV 125W 70→83C
|
||||
24 CU (A): 230.44 1500MHz 881mV 94W 68→79C
|
||||
|
||||
Ratio: 371.6 / 230.4 = 1.61x
|
||||
```
|
||||
|
||||
1500 MHz is the recommended operating point: close to theoretical 1.67x, sustainable thermals (83C), only 30W extra power.
|
||||
|
||||
## Stock Harvest Pattern
|
||||
|
||||
Both boards have identical contiguous harvesting:
|
||||
|
||||
```
|
||||
SE0 SH0: ■■■■■■□□□□
|
||||
SE0 SH1: ■■■■■■□□□□
|
||||
SE1 SH0: ■■■■■■□□□□
|
||||
SE1 SH1: ■■■■■■□□□□
|
||||
24/40 CUs active, 16 harvested
|
||||
```
|
||||
|
||||
CU 0-5 active (WGP 0-2), CU 6-9 fused (WGP 3-4) per SA. Symmetrical across all 4 SAs. Fuse mask: `CC_GC_SHADER_ARRAY_CONFIG = 0xfff80000`.
|
||||
|
||||
## Why CC Alone Didn't Work
|
||||
|
||||
Previously we thought clearing CC_GC_SHADER_ARRAY_CONFIG was sufficient. It isn't — the register is only an enumeration/topology input. The actual dispatch gate is `SPI_PG_ENABLE_STATIC_WGP_MASK`. Evidence:
|
||||
|
||||
- CC=3, SPI=0x7: throughput identical to stock (302 tok/s), power identical (55W)
|
||||
- All 4 driver sources (dmesg, RADV, KFD, UMR) reported 40 CUs but silicon only computed with 24
|
||||
- filippor's `ignore_cu_harvest` kernel parameter independently confirmed: enumeration changes don't affect compute
|
||||
|
||||
The SPI mask was discovered via UMR register analysis. On Vangogh (another RDNA2 APU), the equivalent control is `SMU_MSG_RequestActiveWgp`. Cyan Skillfish doesn't expose that SMU message, but the SPI register is directly writable.
|
||||
|
||||
## Supporting Evidence
|
||||
|
||||
- **CGTS** clock gating registers show identical configuration for harvested WGPs (3-4) and active WGPs (0-2) — the clock tree serves all 5 WGPs
|
||||
- **RLC_PG_CNTL = 0** — all power gating is disabled on BC-250
|
||||
- **glmark2** (3D graphics): 9430 → 9844 (+4.4%) — expected, graphics is fill-rate bound not CU-bound
|
||||
- CUs pass Vulkan compute correctness tests (4M elements, zero errors) at 40 CU
|
||||
|
||||
## How To Apply
|
||||
|
||||
1. Build patched amdgpu module with `bc250_cc_write_mode` parameter
|
||||
2. Set `options amdgpu bc250_cc_write_mode=3` in modprobe config
|
||||
3. Reboot
|
||||
4. After boot, run:
|
||||
```
|
||||
umr -w cyan_skillfish.gfx1013.mmSPI_PG_ENABLE_STATIC_WGP_MASK 0x1f
|
||||
umr -w cyan_skillfish.gfx1013.mmRLC_PG_ALWAYS_ON_WGP_MASK 0x1f
|
||||
```
|
||||
5. Configure governor for 1500 MHz / 900 mV sweet spot
|
||||
|
||||
**TODO:** Integrate SPI write into the amdgpu kernel patch so both changes happen at driver init (no UMR needed post-boot).
|
||||
|
||||
## Credits
|
||||
|
||||
- duggasco — research, testing, documentation
|
||||
- filippor — independent testing, `ignore_cu_harvest` kernel patch, governor
|
||||
- Claude — analysis, tooling, register discovery
|
||||
- Codex — identified SPI_PG_ENABLE_STATIC_WGP_MASK architecture
|
||||
- BC-250 Discord community — thermal/voltage guidance, fleet survey
|
||||
Binary file not shown.
@@ -0,0 +1,721 @@
|
||||
\documentclass[11pt,twocolumn]{article}
|
||||
|
||||
\usepackage[margin=0.75in]{geometry}
|
||||
\usepackage{times}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{booktabs}
|
||||
\usepackage{hyperref}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{float}
|
||||
\usepackage{enumitem}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{titlesec}
|
||||
\usepackage{caption}
|
||||
\usepackage{array}
|
||||
\usepackage{tabularx}
|
||||
\usepackage{listings}
|
||||
\usepackage{tikz}
|
||||
\usepackage{pgfplots}
|
||||
\pgfplotsset{compat=1.18}
|
||||
\usetikzlibrary{positioning,arrows.meta,shapes.geometric,calc,patterns,fit,decorations.pathreplacing}
|
||||
|
||||
\hypersetup{
|
||||
colorlinks=false,
|
||||
linkcolor=black,
|
||||
citecolor=black,
|
||||
urlcolor=black,
|
||||
pdfborder={0 0 0.5},
|
||||
linkbordercolor={0.5 0.5 0.5},
|
||||
citebordercolor={0.5 0.5 0.5},
|
||||
urlbordercolor={0.5 0.5 0.5},
|
||||
}
|
||||
|
||||
\captionsetup{font=small,labelfont=bf}
|
||||
|
||||
\titleformat{\section}{\large\bfseries}{\thesection.}{0.5em}{}
|
||||
\titleformat{\subsection}{\normalsize\bfseries}{\thesubsection}{0.5em}{}
|
||||
|
||||
\pagestyle{fancy}
|
||||
\fancyhf{}
|
||||
\fancyfoot[C]{\thepage}
|
||||
\renewcommand{\headrulewidth}{0pt}
|
||||
|
||||
\lstset{
|
||||
basicstyle=\ttfamily\scriptsize,
|
||||
breaklines=true,
|
||||
breakatwhitespace=false,
|
||||
columns=fullflexible,
|
||||
keepspaces=true,
|
||||
frame=single,
|
||||
framesep=2pt,
|
||||
xleftmargin=3pt,
|
||||
xrightmargin=3pt,
|
||||
aboveskip=6pt,
|
||||
belowskip=6pt,
|
||||
backgroundcolor=\color{gray!8},
|
||||
rulecolor=\color{gray!40},
|
||||
}
|
||||
|
||||
\title{\Large\textbf{Re-enabling Fused-Off Compute Units\\on the AMD BC-250 APU\\via Register-Level Modification}}
|
||||
|
||||
\author{
|
||||
Scott Duggan\\
|
||||
\textit{Independent Research}\\
|
||||
\texttt{duggasco@gmail.com}
|
||||
}
|
||||
|
||||
\date{May 2026}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\twocolumn[
|
||||
\maketitle
|
||||
\begin{abstract}
|
||||
\noindent The AMD BC-250 is a standalone single-board computer built around a salvaged PlayStation~5 APU (``Ariel''), shipping with 24 of 40 physical RDNA~2 GPU compute units (CUs) active. Industry consensus holds that CU harvesting on modern AMD silicon is enforced by irreversible hardware fuses, rendering disabled CUs permanently inaccessible. We demonstrate that this assumption is incorrect for the BC-250: the CU disable mask resides in a software-writable configuration register (\texttt{CC\_GC\_SHADER\_ARRAY\_CONFIG}), and a second power-gating register (\texttt{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK}) gates hardware dispatch. By writing both registers from within the kernel GPU driver's initialization path, we re-enabled all 40~CUs, achieving a 1.54$\times$ improvement in LLM inference prefill throughput (302 to 466~tok/s) with zero compute errors across extensive stress testing. The technique has been verified on three separate boards, and the patch is publicly available~\cite{unlock}. To our knowledge, this is the first documented instance of CU re-enablement on a production AMD APU through software-only means.
|
||||
\end{abstract}
|
||||
\vspace{1em}
|
||||
]
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
\subsection{Background}
|
||||
|
||||
Semiconductor yield management routinely produces dies where a subset of functional units fail quality or power targets. Rather than discard these dies, manufacturers \textit{harvest} (disable) the failing units and sell the part as a lower-tier SKU. This practice is well-documented across AMD, NVIDIA, and Intel product lines and is a key economic lever in modern chip manufacturing.
|
||||
|
||||
The AMD BC-250 represents an unusual case. Originally fabricated as a full Navi~10 die---the same 7nm TSMC silicon used in the Radeon RX~5700~XT and PlayStation~5---these APUs were binned out of Sony's PS5 qualification process. AMD repurposed them for cryptocurrency mining by pairing the die with 6 active Zen~2 CPU cores, 16~GB of unified GDDR6 memory, and a minimal standalone PCB manufactured by ASRock Rack. The result is a complete single-board computer requiring no host system, available on the secondary market for \$50--150.
|
||||
|
||||
Each BC-250 ships with 24 of 40 GPU CUs active (40\% harvested), organized as 6 active CUs per shader array across 4 shader arrays. With the mining market's collapse, these boards have found a second life as ultra-low-cost AI inference nodes, where GPU CU count directly constrains throughput. An active community of over 3,500 members has developed custom BIOS firmware, GPU power governors, cooling solutions, and software tooling to support this use case.
|
||||
|
||||
\subsection{Prior Assumptions}
|
||||
|
||||
The prevailing model for AMD CU harvesting holds that:
|
||||
|
||||
\begin{enumerate}[nosep]
|
||||
\item On-die fuses are blown during manufacturing to permanently disable defective or surplus CUs.
|
||||
\item The Platform Security Processor (PSP) reads these fuses during early boot and programs the GPU's configuration registers accordingly.
|
||||
\item The resulting CU mask is a read-only hardware constant that the OS driver can only observe, never modify.
|
||||
\item \texttt{GC\_USER\_SHADER\_ARRAY\_CONFIG} allows software to disable \textit{additional} CUs but cannot re-enable fuse-disabled ones, since the effective mask is the logical OR of the hardware and user masks.
|
||||
\end{enumerate}
|
||||
|
||||
Our work challenges assumptions 2, 3, and 4.
|
||||
|
||||
\subsection{Contribution}
|
||||
|
||||
We present:
|
||||
\begin{itemize}[nosep]
|
||||
\item A register-level characterization of the CU harvesting mechanism on the BC-250 APU.
|
||||
\item Proof that \texttt{CC\_GC\_SHADER\_ARRAY\_CONFIG} is software-writable from inside the kernel driver.
|
||||
\item Discovery of a second required register (\texttt{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK}) that gates hardware dispatch to re-enabled CUs.
|
||||
\item A controlled 4-state experiment isolating each register's contribution.
|
||||
\item Performance and stability characterization under real LLM inference workloads.
|
||||
\item Verification across three separate boards, all showing identical results.
|
||||
\end{itemize}
|
||||
|
||||
\section{Hardware Platform}
|
||||
|
||||
The BC-250 is a unified APU---CPU, GPU, and memory share a single die and a single 16~GB GDDR6 memory pool. Table~\ref{tab:hw} summarizes the key specifications.
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\caption{BC-250 APU specifications.}
|
||||
\label{tab:hw}
|
||||
\small
|
||||
\begin{tabular}{@{}ll@{}}
|
||||
\toprule
|
||||
\textbf{Parameter} & \textbf{Value} \\
|
||||
\midrule
|
||||
Die & Navi 10 (``Ariel''), 7nm TSMC \\
|
||||
GPU architecture & RDNA 2 (gfx1013) \\
|
||||
Shader engines & 2 \\
|
||||
Shader arrays / SE & 2 (4 total) \\
|
||||
Physical CUs / SA & 10 (40 total) \\
|
||||
Active CUs (stock) & 6 / SA (24 total) \\
|
||||
WGPs / SA & 5 (1 WGP = 2 CUs) \\
|
||||
Memory & 16 GB GDDR6, 256-bit \\
|
||||
Bandwidth & $\sim$448 GB/s \\
|
||||
CPU & 6$\times$ Zen 2 cores, 12 threads \\
|
||||
TDP & 220W rated \\
|
||||
Market price & \$50--150 \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
The GFX10 architecture organizes CUs into Work-Group Processors (WGPs), each containing 2~CUs. The BC-250 has 5~WGPs per shader array, of which 3 are active at stock. The IP discovery table reports the full 40-CU physical die (\texttt{gc\_num\_tcps\,=\,40}) with no harvest entries---the harvesting is applied at a layer above hardware discovery.
|
||||
|
||||
Three physically separate boards (bc250-1, bc250-2, bc250-3) were tested. All three exhibited identical harvest patterns. Figure~\ref{fig:die} illustrates the die topology and harvest pattern.
|
||||
|
||||
\begin{figure*}[t]
|
||||
\centering
|
||||
\begin{tikzpicture}[
|
||||
cu/.style={minimum width=0.48cm, minimum height=0.48cm, draw=black, font=\tiny\sffamily, inner sep=0pt, line width=0.4pt},
|
||||
active/.style={cu, fill=black!15},
|
||||
fused/.style={cu, fill=white, pattern=north east lines, pattern color=black!40},
|
||||
unlocked/.style={cu, fill=black!40},
|
||||
salabel/.style={font=\scriptsize\bfseries, anchor=south},
|
||||
selabel/.style={font=\small\bfseries},
|
||||
]
|
||||
|
||||
% --- STOCK ---
|
||||
\node[font=\small\bfseries, anchor=west] at (-0.4, 1.3) {(a) Stock configuration: 24 CUs active (6 per SA)};
|
||||
|
||||
\foreach \xoff/\yoff/\salbl in {0/0/SA0, 5.6/0/SA1, 0/-1.4/SA0, 5.6/-1.4/SA1} {
|
||||
\begin{scope}[shift={(\xoff,\yoff)}]
|
||||
\node[salabel] at (2.45,0.48) {\salbl};
|
||||
\foreach \i in {0,...,5} {
|
||||
\node[active] at (\i*0.52, 0) {\i};
|
||||
}
|
||||
\foreach \i in {6,...,9} {
|
||||
\node[fused] at (\i*0.52, 0) {\i};
|
||||
}
|
||||
\end{scope}
|
||||
}
|
||||
|
||||
\node[selabel, anchor=east] at (-0.3, 0) {SE0};
|
||||
\node[selabel, anchor=east] at (-0.3, -1.4) {SE1};
|
||||
|
||||
% WGP brackets on SE0/SA0
|
||||
\draw[decorate, decoration={brace, amplitude=2.5pt, mirror}]
|
||||
(0,-0.33) -- (0.97,-0.33) node[midway, below=2.5pt, font=\tiny] {WGP0};
|
||||
\draw[decorate, decoration={brace, amplitude=2.5pt, mirror}]
|
||||
(1.04,-0.33) -- (2.01,-0.33) node[midway, below=2.5pt, font=\tiny] {WGP1};
|
||||
\draw[decorate, decoration={brace, amplitude=2.5pt, mirror}]
|
||||
(2.08,-0.33) -- (3.05,-0.33) node[midway, below=2.5pt, font=\tiny] {WGP2};
|
||||
\draw[decorate, decoration={brace, amplitude=2.5pt, mirror}]
|
||||
(3.12,-0.33) -- (4.09,-0.33) node[midway, below=2.5pt, font=\tiny] {WGP3};
|
||||
\draw[decorate, decoration={brace, amplitude=2.5pt, mirror}]
|
||||
(4.16,-0.33) -- (5.13,-0.33) node[midway, below=2.5pt, font=\tiny] {WGP4};
|
||||
|
||||
% Legend for stock
|
||||
\node[active, minimum width=0.4cm, minimum height=0.4cm] at (0.2, -2.6) {};
|
||||
\node[font=\scriptsize, anchor=west] at (0.55, -2.6) {Active (stock)};
|
||||
\node[fused, minimum width=0.4cm, minimum height=0.4cm] at (3.2, -2.6) {};
|
||||
\node[font=\scriptsize, anchor=west] at (3.55, -2.6) {Fused off};
|
||||
|
||||
% --- UNLOCKED ---
|
||||
\begin{scope}[shift={(0,-4.4)}]
|
||||
\node[font=\small\bfseries, anchor=west] at (-0.4, 1.3) {(b) After unlock: 40 CUs active (10 per SA)};
|
||||
|
||||
\foreach \xoff/\yoff/\salbl in {0/0/SA0, 5.6/0/SA1, 0/-1.4/SA0, 5.6/-1.4/SA1} {
|
||||
\begin{scope}[shift={(\xoff,\yoff)}]
|
||||
\node[salabel] at (2.45,0.48) {\salbl};
|
||||
\foreach \i in {0,...,5} {
|
||||
\node[active] at (\i*0.52, 0) {\i};
|
||||
}
|
||||
\foreach \i in {6,...,9} {
|
||||
\node[unlocked] at (\i*0.52, 0) {\i};
|
||||
}
|
||||
\end{scope}
|
||||
}
|
||||
\node[selabel, anchor=east] at (-0.3, 0) {SE0};
|
||||
\node[selabel, anchor=east] at (-0.3, -1.4) {SE1};
|
||||
|
||||
% Legend for unlocked
|
||||
\node[active, minimum width=0.4cm, minimum height=0.4cm] at (0.2, -2.4) {};
|
||||
\node[font=\scriptsize, anchor=west] at (0.55, -2.4) {Active (stock)};
|
||||
\node[unlocked, minimum width=0.4cm, minimum height=0.4cm] at (3.2, -2.4) {};
|
||||
\node[font=\scriptsize, anchor=west] at (3.55, -2.4) {Re-enabled};
|
||||
\end{scope}
|
||||
|
||||
\end{tikzpicture}
|
||||
\caption{BC-250 die topology showing CU harvest pattern across 2~shader engines $\times$ 2~shader arrays. (a)~Stock: CUs~0--5 active, CUs~6--9 fused off per SA (24 total). (b)~After register modification: all 40~CUs active. Pattern is identical across all three boards tested.}
|
||||
\label{fig:die}
|
||||
\end{figure*}
|
||||
|
||||
\section{Methodology}
|
||||
|
||||
\subsection{Approach Evolution}
|
||||
|
||||
Direct register access from an external kernel module was attempted first and proved unworkable. The GPU's power management (GFXOFF) and the \texttt{amdgpu} driver's exclusive ownership of indirect register access paths created an inherent race condition. Three separate attempts crashed the board via deadlock with the driver's \texttt{pcie\_idx\_lock}.
|
||||
|
||||
We pivoted to patching the \texttt{amdgpu} kernel driver source directly (Linux 6.19.14, Debian), placing all register reads and writes inside the driver's own GFX initialization path where the GRBM bank-selection lock is already held.
|
||||
|
||||
\subsection{Baseline Characterization}
|
||||
|
||||
We instrumented the driver's per-shader-array WGP bitmap function (\texttt{gfx\_v10\_0\_get\_wgp\_active\_bitmap\-\_per\_sh}) to log raw register state during CU enumeration:
|
||||
|
||||
\begin{lstlisting}
|
||||
SE0 SH0: cc_raw=0xfff80000
|
||||
cc_inactive=0x0000fff8
|
||||
active_wgp=0x00000007
|
||||
SE0 SH1: [identical]
|
||||
SE1 SH0: [identical]
|
||||
SE1 SH1: [identical]
|
||||
\end{lstlisting}
|
||||
|
||||
The out-of-range mask (\texttt{0xffffffe0}) marks WGP bits 5+ as physically nonexistent. Within the valid 5-WGP range, \texttt{cc\_inactive=0xfff8} disables WGPs~3 and~4 in every shader array on our three boards.
|
||||
|
||||
\subsection{Community Harvest Map Survey}
|
||||
|
||||
Following publication of the unlock patch, the BC-250 Discord community collected CU harvest maps from 55+ boards using a standardized script authored by Studebaker (Figure~\ref{fig:harvest}). Key findings:
|
||||
|
||||
\begin{itemize}[nosep]
|
||||
\item \textbf{100\% of boards} have exactly 24/40 CUs active (6 per SA).
|
||||
\item \textbf{$\sim$74\%} show the identical contiguous pattern across all 4~SAs: CUs~0--5 active, CUs~6--9 fused.
|
||||
\item \textbf{$\sim$26\%} have at least one SA with a non-contiguous harvest pattern.
|
||||
\item At least one board with a ``clean'' contiguous map exhibited 3~faulty CU pairs after unlock.
|
||||
\end{itemize}
|
||||
|
||||
The uniform 24-CU count across all boards confirms deliberate SKU binning. The mix of contiguous and non-contiguous patterns suggests AMD applied a fixed 6-CU-per-SA quota but allowed the specific CUs disabled to vary---likely reflecting actual defect locations on some dies while using a default contiguous mask on fully functional dies. The existence of boards with faulty CUs after unlock underscores the importance of compute verification (Section~4.3).
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
bar width=14pt,
|
||||
width=\columnwidth,
|
||||
height=4cm,
|
||||
ylabel={Boards},
|
||||
ylabel style={font=\scriptsize},
|
||||
symbolic x coords={Contiguous\\(all SAs),Non-contiguous\\(1+ SA),Fragmented},
|
||||
xtick=data,
|
||||
x tick label style={align=center, font=\tiny},
|
||||
ymin=0,
|
||||
ymax=50,
|
||||
nodes near coords,
|
||||
nodes near coords style={font=\scriptsize\bfseries},
|
||||
enlarge x limits=0.35,
|
||||
cycle list={{fill=black!25, draw=black!70}},
|
||||
]
|
||||
\addplot[fill=black!25, draw=black!70] coordinates {
|
||||
({Contiguous\\(all SAs)}, 43)
|
||||
({Non-contiguous\\(1+ SA)}, 14)
|
||||
({Fragmented}, 1)
|
||||
};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Harvest pattern distribution across 58 community-reported boards. Most show the standard contiguous pattern; a minority have non-contiguous harvesting in one or more shader arrays, likely reflecting genuine defect locations.}
|
||||
\label{fig:harvest}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Write-Probe Experiment}
|
||||
|
||||
We added a guarded boot parameter (\texttt{bc250\_cc\_write\_mode}) to the patched driver with multiple modes:
|
||||
|
||||
\begin{itemize}[nosep]
|
||||
\item \textbf{Mode 0}: No modification (baseline).
|
||||
\item \textbf{Mode 1}: Write zero, read back, restore.
|
||||
\item \textbf{Mode 2}: Clear SE0/SH0, no restore.
|
||||
\item \textbf{Mode 3}: Clear all 4~SAs, no restore.
|
||||
\item \textbf{Mode 4}: Write/read/restore all 4~SAs.
|
||||
\end{itemize}
|
||||
|
||||
Mode~1 (write-then-restore on SE0/SH0) yielded:
|
||||
|
||||
\begin{lstlisting}
|
||||
original = 0xfff80000
|
||||
after_zero = 0xffe00000
|
||||
after_restore= 0xfff80000
|
||||
\end{lstlisting}
|
||||
|
||||
The upper out-of-range bits remained set, but the valid inactive-WGP bits cleared. This proved that \texttt{CC\_GC\_SHADER\_ARRAY\_CONFIG} is \textbf{not} a read-only hardware fuse latch on this silicon.
|
||||
|
||||
\subsection{Boot Safety Mechanism}
|
||||
|
||||
To prevent a failed CU-enable from persisting across reboots, we used a self-deleting one-shot modprobe rule:
|
||||
|
||||
\begin{lstlisting}
|
||||
install amdgpu /bin/sh -c \
|
||||
'rm -f /etc/modprobe.d/bc250-cc-\
|
||||
oneshot.conf; \
|
||||
exec /sbin/modprobe --ignore-install \
|
||||
amdgpu bc250_cc_write_mode=3'
|
||||
\end{lstlisting}
|
||||
|
||||
The rule removes itself before invoking the driver, ensuring that a crash during the enable attempt does not loop on subsequent power cycles.
|
||||
|
||||
\subsection{Discovery of the Second Register}
|
||||
|
||||
Initial mode-3 boots produced 40-CU enumeration and correct Vulkan compute results, but LLM inference throughput showed no improvement. Investigation using AMD's UMR (User-Mode Register debugger) revealed a second gate: \texttt{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK} (offset \texttt{0x1277}).
|
||||
|
||||
At stock, this register is \texttt{0x7}---enabling hardware dispatch only to WGPs~0--2. Even with the CC mask cleared, the shader processor continued routing work to only the original 3~WGPs. Setting this register to \texttt{0x1F} via UMR at runtime, combined with the CC clear at boot, unlocked actual compute scaling.
|
||||
|
||||
A companion register, \texttt{RLC\_PG\_ALWAYS\_ON\_WGP\_MASK}, must also be set to \texttt{0x1F} to prevent the Run List Controller from power-gating re-enabled WGPs during idle periods. Figure~\ref{fig:gating} illustrates the dual-register gating architecture.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\resizebox{\columnwidth}{!}{%
|
||||
\begin{tikzpicture}[
|
||||
box/.style={draw=black, rounded corners=2pt, minimum width=2.4cm, minimum height=0.6cm, align=center, font=\scriptsize, line width=0.5pt},
|
||||
regbox/.style={box, fill=black!12},
|
||||
state/.style={box, fill=black!5},
|
||||
result/.style={box, fill=black!25, line width=0.8pt},
|
||||
noresult/.style={box, fill=white, densely dashed},
|
||||
arr/.style={-{Stealth[length=4pt]}, semithick},
|
||||
lbl/.style={font=\tiny, fill=white, inner sep=1pt},
|
||||
]
|
||||
|
||||
\node[regbox] (cc) at (0, 0) {\textbf{CC\_GC\_SHADER\_}\\\textbf{ARRAY\_CONFIG}};
|
||||
\node[regbox] (spi) at (0, -1.8) {\textbf{SPI\_PG\_ENABLE\_}\\\textbf{STATIC\_WGP\_MASK}};
|
||||
|
||||
\node[font=\tiny\itshape, anchor=east, align=right] at (-1.4, 0) {Enumeration\\gate};
|
||||
\node[font=\tiny\itshape, anchor=east, align=right] at (-1.4, -1.8) {Dispatch\\gate};
|
||||
|
||||
\node[state] (d1) at (3.5, 0) {Driver sees\\40 CUs?};
|
||||
\node[state] (d2) at (3.5, -1.8) {SPI routes to\\all WGPs?};
|
||||
|
||||
\node[noresult] (no1) at (6.0, 0) {24 CU behavior\\(stock)};
|
||||
\node[noresult] (no2) at (6.0, -1.8) {Phantom CUs\\(no scaling)};
|
||||
\node[result] (yes) at (6.0, -3.3) {\textbf{40 CU active}\\(1.54$\times$)};
|
||||
|
||||
\draw[arr] (cc) -- (d1);
|
||||
\draw[arr] (d1) -- node[lbl, above] {No} (no1);
|
||||
\draw[arr] (d1) -- node[lbl, right] {Yes} (d2);
|
||||
\draw[arr] (d2) -- node[lbl, above] {No} (no2);
|
||||
\draw[arr] (d2) -- node[lbl, right] {Yes} (yes);
|
||||
\draw[arr] (spi) -- (d2);
|
||||
|
||||
\end{tikzpicture}%
|
||||
}
|
||||
\caption{Dual-register gating architecture. Both the CC enumeration mask and SPI dispatch mask must be set; either alone produces no performance change.}
|
||||
\label{fig:gating}
|
||||
\end{figure}
|
||||
|
||||
\section{Results}
|
||||
|
||||
\subsection{4-State Controlled Experiment}
|
||||
|
||||
Table~\ref{tab:4state} presents the controlled experiment isolating each register's contribution, measured via 512-token LLM prefill at 2~GHz on bc250-2.
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\caption{4-state A/B test: register contributions.}
|
||||
\label{tab:4state}
|
||||
\small
|
||||
\begin{tabular}{@{}ccccl@{}}
|
||||
\toprule
|
||||
\textbf{CC} & \textbf{SPI} & \textbf{tok/s} & \textbf{W} & \textbf{Scale} \\
|
||||
\midrule
|
||||
Stock & 0x7 & 302 & 56 & 1.00$\times$ \\
|
||||
Stock & 0x1F & 302 & 56 & 1.00$\times$ \\
|
||||
Clear & 0x7 & 302 & 56 & 1.00$\times$ \\
|
||||
Clear & 0x1F & \textbf{466} & \textbf{181} & \textbf{1.54$\times$} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
Neither register alone produces any gain. Both must be set: \texttt{CC\_GC\_SHADER\_ARRAY\_CONFIG} controls CU \textit{enumeration} (driver visibility), while \texttt{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK} controls hardware \textit{dispatch} (physical wavefront routing).
|
||||
|
||||
\subsection{Operating Point Optimization}
|
||||
|
||||
At 2~GHz, the 40-CU configuration draws 181W and approaches thermal limits. Table~\ref{tab:oppoint} shows the sustainable 1500~MHz / 900~mV point.
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\caption{24-CU vs 40-CU at 1500~MHz.}
|
||||
\label{tab:oppoint}
|
||||
\small
|
||||
\begin{tabular}{@{}lcccc@{}}
|
||||
\toprule
|
||||
\textbf{Config} & \textbf{tok/s} & \textbf{W} & \textbf{$^\circ$C} & \textbf{Scale} \\
|
||||
\midrule
|
||||
24 CU @ 1500 & 230 & 55 & 71 & 1.00$\times$ \\
|
||||
40 CU @ 1500 & 372 & 125 & 83 & \textbf{1.61$\times$} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
The 1.61$\times$ scaling at 1500~MHz exceeds the 1.54$\times$ at 2~GHz because the lower clock avoids power limiters, allowing better utilization of the wider shader array. GPU frequency management is handled by the community-developed \texttt{cyan-skillfish-governor-smu} (Section~\ref{sec:community}). Figure~\ref{fig:perf} summarizes the performance results.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
bar width=10pt,
|
||||
width=\columnwidth,
|
||||
height=5cm,
|
||||
ylabel={Prefill tok/s (pp512)},
|
||||
ylabel style={font=\scriptsize},
|
||||
symbolic x coords={Stock\\24CU\\2GHz, CC only\\2GHz, SPI only\\2GHz, Both\\2GHz, Stock\\24CU\\1.5GHz, Both\\1.5GHz},
|
||||
xtick=data,
|
||||
x tick label style={align=center, font=\tiny},
|
||||
ymin=0,
|
||||
ymax=520,
|
||||
nodes near coords,
|
||||
nodes near coords style={font=\tiny\bfseries},
|
||||
enlarge x limits=0.12,
|
||||
cycle list={{fill=black!20, draw=black!70}},
|
||||
]
|
||||
\addplot[fill=black!20, draw=black!70] coordinates {
|
||||
({Stock\\24CU\\2GHz}, 302)
|
||||
({CC only\\2GHz}, 302)
|
||||
({SPI only\\2GHz}, 302)
|
||||
({Both\\2GHz}, 466)
|
||||
({Stock\\24CU\\1.5GHz}, 230)
|
||||
({Both\\1.5GHz}, 372)
|
||||
};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{LLM prefill throughput across all tested configurations. Only the combined CC+SPI modification produces a gain. The 1.5~GHz point achieves better scaling (1.61$\times$) than 2~GHz (1.54$\times$) due to reduced power limiting.}
|
||||
\label{fig:perf}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Compute Correctness}
|
||||
|
||||
Vulkan compute verification was performed at each state:
|
||||
|
||||
\begin{itemize}[nosep]
|
||||
\item \textbf{28-CU} (1~SA cleared): 1M and 4M element Vulkan shader dispatch, zero errors.
|
||||
\item \textbf{40-CU} (all SAs cleared): 1M and 4M element dispatch, zero errors.
|
||||
\item \textbf{Stress}: 10 consecutive 4M-element runs at 40~CUs---zero errors, zero GPU recovery events, zero ring timeouts, zero VM faults.
|
||||
\end{itemize}
|
||||
|
||||
No \texttt{amdgpu} reset or fault messages appeared in kernel logs during any test. GPU temperature during sustained 40-CU stress: 70--71$^\circ$C at 1500~MHz.
|
||||
|
||||
\subsection{Reproducibility Across Boards}
|
||||
|
||||
The enable procedure was validated across multiple boot cycles on three physically separate boards:
|
||||
|
||||
\begin{enumerate}[nosep]
|
||||
\item Boot to baseline 24~CUs (verified via RADV).
|
||||
\item Install one-shot modprobe config, reboot.
|
||||
\item Verify 40~CUs: all SAs report \texttt{cu\_mask\,=\,0x3ff}.
|
||||
\item Set SPI dispatch mask via UMR.
|
||||
\item Run workload, verify scaling.
|
||||
\item Reboot---returns to baseline 24~CUs.
|
||||
\end{enumerate}
|
||||
|
||||
All three boards (bc250-1, bc250-2, bc250-3) showed identical behavior and identical harvest patterns. The boards were independently sourced at different times and showed no variation in the CC mask, SPI mask, or CU layout.
|
||||
|
||||
\subsection{Cluster Deployment}
|
||||
|
||||
Following single-board validation, all three boards were deployed as a Vulkan RPC inference cluster, each running at 40~CUs. The cluster runs a 22~GB Qwen3.6 35B-A3B MoE model split across boards, achieving 187~tok/s prefill and 28~tok/s generation at 128-token context, with 32K context achievable using Q4\_0 KV cache quantization. The boards boot disklessly via PXE/iSCSI from a NAS appliance.
|
||||
|
||||
\section{Discussion}
|
||||
|
||||
\subsection{Why Are These CUs Functional?}
|
||||
|
||||
The community harvest map survey (Section~3.3, $n=58$) reveals a nuanced picture. Approximately 74\% of boards show a perfectly contiguous pattern across all 4~SAs, consistent with a deliberate SKU mask applied to fully functional silicon. The remaining 26\% show at least one SA with a non-contiguous pattern, suggesting genuine defect-driven harvesting on those dies.
|
||||
|
||||
All boards share the same 24/40 CU quota, confirming a fixed SKU definition. The BC-250 uses Navi~10 dies that failed PS5 qualification (which requires 36/40~CUs). AMD appears to have applied a uniform 6-CU-per-SA quota, using a default contiguous mask on defect-free dies and mapping actual defect locations on others.
|
||||
|
||||
On our three boards (all contiguous-pattern), re-enabled CUs compute correctly and survive stress testing. However, at least one community member with a contiguous map reported 3~faulty CU pairs after unlock---demonstrating that the harvest map alone is not a reliable predictor of CU health, and compute verification is essential.
|
||||
|
||||
\subsection{Dual-Register Gating Architecture}
|
||||
|
||||
The two-register mechanism provides defense in depth:
|
||||
|
||||
\begin{enumerate}[nosep]
|
||||
\item \textbf{CC\_GC\_SHADER\_ARRAY\_CONFIG} controls CU \textit{enumeration}. When WGPs are marked inactive, the driver reports fewer CUs and the runtime allocates work accordingly.
|
||||
\item \textbf{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK} controls hardware \textit{dispatch}. Even if the driver sees 40~CUs, the shader processor only routes wavefronts to WGPs enabled here.
|
||||
\end{enumerate}
|
||||
|
||||
Clearing only the enumeration mask creates a ``phantom CU'' state---the driver believes additional CUs exist, but work is silently routed to the enabled subset. This produces no errors and no performance change.
|
||||
|
||||
\subsection{Economics of Salvage Compute}
|
||||
|
||||
Table~\ref{tab:economics} compares the per-CU economics at current secondary market prices (\$50--150).
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\caption{Economics: stock vs.\ unlocked BC-250 (at \$150).}
|
||||
\label{tab:economics}
|
||||
\small
|
||||
\begin{tabular}{@{}lcc@{}}
|
||||
\toprule
|
||||
\textbf{Metric} & \textbf{24 CU} & \textbf{40 CU} \\
|
||||
\midrule
|
||||
pp512 @ 1500 MHz & 230 tok/s & 372 tok/s \\
|
||||
\$/CU & \$6.25 & \$3.75 \\
|
||||
Perf/\$ (tok/s/\$) & 1.53 & 2.48 \\
|
||||
tok/s/W & 4.18 & 2.98 \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
At the low end of the price range (\$50), the unlocked 40-CU configuration delivers 7.44~tok/s/\$---competitive with consumer GPUs costing an order of magnitude more, provided the workload fits in 16~GB of unified memory.
|
||||
|
||||
\subsection{Broader Applicability}
|
||||
|
||||
The methodology---instrumenting the driver's own init path to test register writability under proper locking---is applicable to other AMD products. Whether other harvested parts (e.g., RX~7800~XT vs RX~7900~XTX) use similarly writable CC masks is an open question. The gfx1013 variant's origin as a repurposed console APU may make it uniquely permissive.
|
||||
|
||||
\subsection{Limitations}
|
||||
|
||||
\begin{itemize}[nosep]
|
||||
\item \textbf{Persistence}: The CC mask reverts on reboot; the SPI mask must be set post-boot. A production deployment should integrate both writes into the driver init path.
|
||||
\item \textbf{CPU cores}: The APU's CPU harvesting (6/8 Zen~2 cores) is PSP-enforced and not clearable via this technique. INIT/SIPI signals to disabled cores return error status 0x4.
|
||||
\item \textbf{Thermal ceiling}: 40~CUs at 2~GHz draw 181W. The 1500~MHz / 900~mV point (125W) is recommended for sustained workloads.
|
||||
\item \textbf{Sample size}: Three boards tested with identical results. Population-level confidence requires broader community replication.
|
||||
\item \textbf{Defect risk}: Some boards may carry genuinely defective CUs. Extended compute verification is essential before relying on re-enabled CUs.
|
||||
\end{itemize}
|
||||
|
||||
\section{Community Ecosystem}
|
||||
\label{sec:community}
|
||||
|
||||
This work builds on a substantial open-source ecosystem developed by the BC-250 community (3,500+ members on Discord). While the CU re-enablement technique is novel, the platform's viability as a compute node depends on prior community contributions:
|
||||
|
||||
\textbf{Custom BIOS firmware.}
|
||||
TuxThePenguin0 (``Segfault'') developed modified BIOS images that unlock dynamic VRAM allocation and chipset configuration menus, enabling the flexible memory split between CPU and GPU that makes inference workloads practical on a 16~GB unified memory system~\cite{bios}.
|
||||
|
||||
\textbf{GPU power management.}
|
||||
FilippoR's \texttt{cyan-skillfish-governor-smu}~\cite{governor_smu} provides SMU-based GPU clock/voltage scaling. Without a governor, the BC-250 GPU locks at 1500~MHz regardless of load. The governor enables the variable clock states used in our operating-point analysis. mothenjoyer69's earlier \texttt{oberon-governor}~\cite{governor} established the initial approach.
|
||||
|
||||
\textbf{CPU overclocking and ACPI.}
|
||||
The bc250-collective's \texttt{bc250\_smu\_oc} tool enables Zen~2 CPU frequency adjustment via SMU mailbox. Their \texttt{bc250-acpi-fix} provides SSDT tables for proper C-State and P-State enumeration~\cite{smuoc,acpifix}.
|
||||
|
||||
\textbf{Kernel contributions.}
|
||||
Magnap identified the critical TTM page limit fix (\texttt{ttm.pages\_limit=4194304}) that unlocks full 16~GB memory allocation; without this parameter, the GPU is capped at $\sim$7.4~GiB.
|
||||
|
||||
\textbf{Documentation.}
|
||||
elektricM compiled comprehensive documentation from over 9,700 Discord messages~\cite{elektric}. mothenjoyer69 authored the original hardware documentation~\cite{mothen}. akandr developed and published inference benchmarks across 31 models, establishing the BC-250's performance envelope~\cite{akandr}.
|
||||
|
||||
\textbf{Hardware ecosystem.}
|
||||
provod developed a ray tracing compatibility patch. Fred78290 authored the \texttt{nct6687d} kernel module~\cite{nct6687d} for fan/thermal control via the Nuvoton NCT6686D SuperIO. Over 145 3D-printable case designs exist on community repositories.
|
||||
|
||||
\textbf{Community replication.}
|
||||
Following publication of the unlock patch, 58 community members submitted CU harvest maps (Section~3.3), and multiple users have successfully applied the unlock procedure to their boards. At least one user reported faulty CU pairs after unlock, confirming that compute verification is essential.
|
||||
|
||||
\section{Conclusion}
|
||||
|
||||
We demonstrated that the AMD BC-250 APU's GPU CU harvesting is enforced by two software-writable configuration registers rather than irreversible hardware fuses. By clearing \texttt{CC\_GC\_SHADER\_ARRAY\_CONFIG} during kernel driver initialization and setting \texttt{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK} to enable dispatch to all work-group processors, we re-enabled all 40 physical CUs on a part shipped with 24---a 67\% increase in GPU compute resources from a purely software modification.
|
||||
|
||||
The re-enabled CUs are fully functional: they produce correct results, survive sustained stress testing, and deliver 1.54--1.61$\times$ throughput improvement on LLM inference workloads. The technique is reproducible across boot cycles and across three separate boards now operating as a production inference cluster.
|
||||
|
||||
This finding challenges the assumption that semiconductor harvesting always represents a permanent physical constraint. For the growing community repurposing salvaged silicon for AI inference, it demonstrates that register-level analysis of hardware gating mechanisms can recover significant compute left on the table by the original manufacturer.
|
||||
|
||||
\onecolumn
|
||||
|
||||
\section*{Acknowledgments}
|
||||
|
||||
This work was conducted as independent research with hardware purchased at secondary market prices. The kernel driver patch and enable tooling are published at \url{https://github.com/duggasco/bc250-40cu-unlock}. AMD's open-source \texttt{amdgpu} kernel driver and Mesa/RADV Vulkan driver made this work possible---the ability to read, instrument, and modify the driver source was the key enabler.
|
||||
|
||||
Special thanks to the community contributors whose work made the BC-250 a viable compute platform: TuxThePenguin0 for custom BIOS development~\cite{bios}, FilippoR for the \texttt{cyan-skillfish-governor-smu}~\cite{governor_smu}, mothenjoyer69 for hardware documentation and the original GPU governor~\cite{mothen,governor}, elektricM for compiling community knowledge from over 9,700 Discord messages~\cite{elektric}, akandr for publishing inference benchmarks across 31 models~\cite{akandr}, Magnap for the critical TTM page limit kernel fix, the bc250-collective for SMU overclocking and ACPI tooling~\cite{smuoc,acpifix}, Fred78290 for the fan control kernel module~\cite{nct6687d}, and the 58 Discord community members (\texttt{discord.gg/8eZfFWhczz}) who submitted CU harvest maps for the population survey.
|
||||
|
||||
\begin{thebibliography}{10}
|
||||
|
||||
\bibitem{unlock}
|
||||
S.~Duggan, ``BC-250 40-CU Unlock,''
|
||||
\url{https://github.com/duggasco/bc250-40cu-unlock}, 2026.
|
||||
|
||||
\bibitem{bios}
|
||||
TuxThePenguin0, ``BC-250 Modified BIOS,''
|
||||
\url{https://gitlab.com/TuxThePenguin0/bc250-bios/}, 2025.
|
||||
|
||||
\bibitem{governor_smu}
|
||||
FilippoR, ``Cyan Skillfish Governor (SMU),''
|
||||
\url{https://github.com/filippor/cyan-skillfish-governor}, 2025.
|
||||
|
||||
\bibitem{governor}
|
||||
mothenjoyer69, ``Oberon Governor,''
|
||||
\url{https://gitlab.com/mothenjoyer69/oberon-governor}, 2025.
|
||||
|
||||
\bibitem{smuoc}
|
||||
bc250-collective, ``BC-250 SMU Overclock,''
|
||||
\url{https://github.com/bc250-collective/bc250_smu_oc}, 2025.
|
||||
|
||||
\bibitem{acpifix}
|
||||
bc250-collective, ``BC-250 ACPI Fix,''
|
||||
\url{https://github.com/bc250-collective/bc250-acpi-fix}, 2025.
|
||||
|
||||
\bibitem{nct6687d}
|
||||
Fred78290, ``NCT6687D Kernel Module,''
|
||||
\url{https://github.com/Fred78290/nct6687d}, 2025.
|
||||
|
||||
\bibitem{elektric}
|
||||
elektricM, ``AMD BC-250 Documentation,''
|
||||
\url{https://github.com/elektricM/amd-bc250-docs}, 2025.
|
||||
|
||||
\bibitem{mothen}
|
||||
mothenjoyer69, ``BC-250 Documentation,''
|
||||
\url{https://github.com/mothenjoyer69/bc250-documentation}, 2024.
|
||||
|
||||
\bibitem{akandr}
|
||||
akandr, ``BC-250 Inference Benchmarks,''
|
||||
\url{https://github.com/akandr/bc250}, 2026.
|
||||
|
||||
\end{thebibliography}
|
||||
|
||||
\appendix
|
||||
|
||||
\section{Register Details}
|
||||
|
||||
\subsection*{CC\_GC\_SHADER\_ARRAY\_CONFIG}
|
||||
|
||||
DWORD offset \texttt{0x2042C}, per-instance \texttt{0x0226f}.
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\begin{tabular}{@{}llll@{}}
|
||||
\toprule
|
||||
\textbf{Bits} & \textbf{Field} & \textbf{Stock} & \textbf{Cleared} \\
|
||||
\midrule
|
||||
31:20 & Out-of-range WGP mask & 0xFFE & 0xFFE \\
|
||||
19:16 & Inactive WGPs (valid) & 0xF (WGP 3--4 off) & 0x0 (all active) \\
|
||||
15:0 & Reserved & 0x0000 & 0x0000 \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
Full raw value: \texttt{0xFFF80000} (stock) $\rightarrow$ \texttt{0xFFE00000} (cleared).
|
||||
|
||||
\subsection*{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK}
|
||||
|
||||
Offset \texttt{0x1277}. Each bit enables hardware shader dispatch to one WGP within each shader array.
|
||||
|
||||
\begin{table}[H]
|
||||
\centering
|
||||
\begin{tabular}{@{}cclcc@{}}
|
||||
\toprule
|
||||
\textbf{Bit} & \textbf{WGP} & \textbf{CUs} & \textbf{Stock} & \textbf{Enabled} \\
|
||||
\midrule
|
||||
0 & WGP 0 & CU 0--1 & 1 & 1 \\
|
||||
1 & WGP 1 & CU 2--3 & 1 & 1 \\
|
||||
2 & WGP 2 & CU 4--5 & 1 & 1 \\
|
||||
3 & WGP 3 & CU 6--7 & 0 & 1 \\
|
||||
4 & WGP 4 & CU 8--9 & 0 & 1 \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{table}
|
||||
|
||||
Stock value: \texttt{0x7} (WGPs 0--2 enabled). Unlocked value: \texttt{0x1F} (all 5 WGPs enabled).
|
||||
|
||||
\subsection*{RLC\_PG\_ALWAYS\_ON\_WGP\_MASK}
|
||||
|
||||
Must be set to \texttt{0x1F} concurrently with \texttt{SPI\_PG\_ENABLE\_STATIC\_WGP\_MASK} to prevent the Run List Controller from power-gating re-enabled WGPs during idle periods.
|
||||
|
||||
\section{Harvest Pattern Visualization}
|
||||
|
||||
Per-SA CU layout showing the dominant (74\%) contiguous harvest pattern:
|
||||
|
||||
\begin{lstlisting}
|
||||
CU index: 0 1 2 3 4 5 6 7 8 9
|
||||
[*] [*] [*] [*] [*] [*] [ ] [ ] [ ] [ ]
|
||||
|--- active (stock) --| |--- fused ---|
|
||||
WGP 0 WGP 1 WGP 2 WGP 3 WGP 4
|
||||
\end{lstlisting}
|
||||
|
||||
Non-contiguous variants observed in $\sim$26\% of boards (examples from community survey):
|
||||
\begin{lstlisting}
|
||||
Variant A: [*] [*] [ ] [ ] [*] [*] [*] [*] [ ] [ ]
|
||||
Variant B: [ ] [ ] [*] [*] [*] [*] [*] [*] [ ] [ ]
|
||||
Variant C: [*] [*] [*] [*] [ ] [ ] [*] [*] [ ] [ ]
|
||||
\end{lstlisting}
|
||||
|
||||
All variants maintain exactly 6 active CUs (3 active WGPs) per shader array. After re-enablement, all 10 CUs per SA are active and dispatching compute work regardless of the original harvest pattern.
|
||||
|
||||
\section{Enable Procedure}
|
||||
|
||||
\begin{lstlisting}
|
||||
# 1. Build patched amdgpu module with bc250_cc_write_mode parameter
|
||||
# (see https://github.com/duggasco/bc250-40cu-unlock)
|
||||
|
||||
# 2. Install one-shot boot config:
|
||||
echo 'install amdgpu /bin/sh -c '\''rm -f /etc/modprobe.d/bc250-cc-oneshot.conf; \
|
||||
exec /sbin/modprobe --ignore-install amdgpu bc250_cc_write_mode=3'\''' \
|
||||
> /etc/modprobe.d/bc250-cc-oneshot.conf
|
||||
|
||||
# 3. Reboot
|
||||
|
||||
# 4. Set SPI dispatch mask (requires umr):
|
||||
umr -w cyan_skillfish.gfx1013.mmSPI_PG_ENABLE_STATIC_WGP_MASK 0x1f
|
||||
umr -w cyan_skillfish.gfx1013.mmRLC_PG_ALWAYS_ON_WGP_MASK 0x1f
|
||||
|
||||
# 5. Verify:
|
||||
RADV_DEBUG=info vulkaninfo 2>&1 | grep cu_mask
|
||||
# Expected: cu_mask = 0x3ff (all SAs)
|
||||
\end{lstlisting}
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,98 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: duggasco <duggasco@gmail.com>
|
||||
Date: Mon, 18 May 2026 20:00:00 +0000
|
||||
Subject: [PATCH] drm/amdgpu/gfx10: BC-250 40 CU unlock via CC + SPI register
|
||||
writes
|
||||
|
||||
Re-enable all 40 harvested CUs on the AMD BC-250 (gfx1013 / Cyan
|
||||
Skillfish / salvaged PS5 APU) by writing two hardware registers during
|
||||
CU enumeration in gfx_v10_0_get_cu_info():
|
||||
|
||||
1. CC_GC_SHADER_ARRAY_CONFIG — clears the harvest enumeration mask so
|
||||
the driver, RADV, and KFD see all 40 CUs.
|
||||
2. SPI_PG_ENABLE_STATIC_WGP_MASK — enables the SPI (Shader Processor
|
||||
Input) to dispatch wavefronts to all 5 WGPs per shader array.
|
||||
Without this, CC alone changes reporting but SPI still dispatches
|
||||
to only 3 WGPs (24 CUs).
|
||||
3. RLC_PG_ALWAYS_ON_WGP_MASK — keeps all WGPs powered.
|
||||
|
||||
Both CC and SPI writes are required — neither alone produces compute
|
||||
scaling. Controlled via module parameter amdgpu.bc250_cc_write_mode
|
||||
(default 0 = off, 3 = enable all). Guarded by PCI device ID 0x13FE.
|
||||
|
||||
Verified: pp512 302 tok/s (24 CU) -> 466 tok/s (40 CU) = 1.54x at 2GHz.
|
||||
At 1500MHz/900mV: 230 -> 372 tok/s = 1.61x with sustainable thermals.
|
||||
|
||||
4-state A/B test confirmed neither register alone has any effect:
|
||||
CC=0 SPI=0x07 (stock): 302 tok/s
|
||||
CC=0 SPI=0x1F (SPI only): 302 tok/s (no gain)
|
||||
CC=3 SPI=0x07 (CC only): 302 tok/s (no gain)
|
||||
CC=3 SPI=0x1F (both): 466 tok/s (1.54x)
|
||||
|
||||
Reference: https://github.com/duggasco/bc250-40cu-unlock
|
||||
|
||||
Signed-off-by: duggasco <duggasco@gmail.com>
|
||||
---
|
||||
drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c | 46 ++++++++++++++++++++++++
|
||||
1 file changed, 46 insertions(+)
|
||||
|
||||
diff --git a/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c b/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c
|
||||
--- a/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c
|
||||
+++ b/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c
|
||||
@@ -26,6 +26,13 @@
|
||||
#include <linux/firmware.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/pci.h>
|
||||
+
|
||||
+/* BC-250 40 CU unlock: clears harvest mask + enables SPI dispatch to all WGPs */
|
||||
+static int bc250_cc_write_mode;
|
||||
+module_param(bc250_cc_write_mode, int, 0444);
|
||||
+MODULE_PARM_DESC(bc250_cc_write_mode,
|
||||
+ "BC-250: 0=off 1=probe-SE0SH0 2=clear-SE0SH0 3=clear-all-SAs 4=probe-all-SAs");
|
||||
+#define BC250_PCI_DEVICE_ID 0x13FE
|
||||
#include "amdgpu.h"
|
||||
#include "amdgpu_gfx.h"
|
||||
#include "amdgpu_psp.h"
|
||||
@@ -10127,6 +10134,40 @@
|
||||
amdgpu_gfx_parse_disable_cu(disable_masks, 4, 2);
|
||||
|
||||
mutex_lock(&adev->grbm_idx_mutex);
|
||||
+
|
||||
+ /* BC-250: unlock harvested CUs -- CC (enumeration) + SPI (dispatch) + RLC (power) */
|
||||
+ if (bc250_cc_write_mode > 0 && adev->pdev->device == BC250_PCI_DEVICE_ID) {
|
||||
+ int bc_se, bc_sh;
|
||||
+ for (bc_se = 0; bc_se < adev->gfx.config.max_shader_engines; bc_se++) {
|
||||
+ for (bc_sh = 0; bc_sh < adev->gfx.config.max_sh_per_se; bc_sh++) {
|
||||
+ u32 bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after;
|
||||
+ if (bc250_cc_write_mode == 2 && (bc_se > 0 || bc_sh > 0))
|
||||
+ continue;
|
||||
+ gfx_v10_0_select_se_sh(adev, bc_se, bc_sh, 0xffffffff, 0);
|
||||
+ bc_cc_orig = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
+ WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, 0);
|
||||
+ bc_cc_after = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
+ bc_spi_orig = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
+ WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, 0x1f);
|
||||
+ bc_spi_after = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
+ WREG32_SOC15(GC, 0, mmRLC_PG_ALWAYS_ON_WGP_MASK, 0x1f);
|
||||
+ if (bc250_cc_write_mode == 1 || bc250_cc_write_mode == 4) {
|
||||
+ WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, bc_cc_orig);
|
||||
+ WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, bc_spi_orig);
|
||||
+ dev_info(adev->dev,
|
||||
+ "bc250-40cu-probe: se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x (restored)",
|
||||
+ bc_se, bc_sh, bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
+ } else {
|
||||
+ dev_info(adev->dev,
|
||||
+ "bc250-40cu-enable: mode=%d se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x",
|
||||
+ bc250_cc_write_mode, bc_se, bc_sh,
|
||||
+ bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ gfx_v10_0_select_se_sh(adev, 0xffffffff, 0xffffffff, 0xffffffff, 0);
|
||||
+ }
|
||||
+
|
||||
for (i = 0; i < adev->gfx.config.max_shader_engines; i++) {
|
||||
for (j = 0; j < adev->gfx.config.max_sh_per_se; j++) {
|
||||
bitmap = i * adev->gfx.config.max_sh_per_se + j;
|
||||
--
|
||||
2.53.0
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-40cu-benchmark.sh — Reproducible A/B benchmark for 40 CU unlock
|
||||
#
|
||||
# Runs llama-bench at 24 CU and 40 CU with matched clocks and cooldown.
|
||||
# Requires: UMR, Vulkan llama-bench, a GGUF model, patched amdgpu (bc250_cc_write_mode=3)
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./bc250-40cu-benchmark.sh [model_path] [llama_bench_path]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MODEL="${1:-}"
|
||||
BENCH="${2:-}"
|
||||
UMR="${UMR:-}"
|
||||
COOL_TARGET=76000
|
||||
|
||||
info() { printf '\033[0;32m[+]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[0;31m[E]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
find_umr() {
|
||||
for p in /opt/umr/build/src/app/umr /usr/local/bin/umr /usr/bin/umr; do
|
||||
[ -x "$p" ] && UMR="$p" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
find_model() {
|
||||
for p in /opt/models/*.gguf /opt/models/*/*.gguf /root/models/*.gguf; do
|
||||
[ -f "$p" ] && echo "$p" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
find_bench() {
|
||||
for p in \
|
||||
/opt/llama.cpp/build-vulkan/bin/llama-bench \
|
||||
/opt/bc250/llama-vulkan-build/bin/llama-bench \
|
||||
/usr/local/bin/llama-bench; do
|
||||
[ -x "$p" ] && echo "$p" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
get_temp() {
|
||||
cat /sys/class/drm/card0/device/hwmon/hwmon*/temp1_input 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
get_power() {
|
||||
cat /sys/kernel/debug/dri/0/amdgpu_pm_info 2>/dev/null | grep "current SoC" | awk '{print $1}'
|
||||
}
|
||||
|
||||
get_sclk() {
|
||||
cat /sys/kernel/debug/dri/0/amdgpu_pm_info 2>/dev/null | grep "SCLK" | head -1 | awk '{print $1}'
|
||||
}
|
||||
|
||||
get_vddgfx() {
|
||||
cat /sys/kernel/debug/dri/0/amdgpu_pm_info 2>/dev/null | grep "VDDGFX" | awk '{print $1}'
|
||||
}
|
||||
|
||||
wait_cool() {
|
||||
local t
|
||||
while true; do
|
||||
t=$(get_temp)
|
||||
[ "$t" -lt "$COOL_TARGET" ] 2>/dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
set_24cu() {
|
||||
"$UMR" -w cyan_skillfish.gfx1013.mmSPI_PG_ENABLE_STATIC_WGP_MASK 0x7 2>/dev/null
|
||||
"$UMR" -w cyan_skillfish.gfx1013.mmRLC_PG_ALWAYS_ON_WGP_MASK 0x3 2>/dev/null
|
||||
}
|
||||
|
||||
set_40cu() {
|
||||
"$UMR" -w cyan_skillfish.gfx1013.mmSPI_PG_ENABLE_STATIC_WGP_MASK 0x1f 2>/dev/null
|
||||
"$UMR" -w cyan_skillfish.gfx1013.mmRLC_PG_ALWAYS_ON_WGP_MASK 0x1f 2>/dev/null
|
||||
}
|
||||
|
||||
run_bench() {
|
||||
local label="$1" outfile="$2"
|
||||
local spi
|
||||
spi=$("$UMR" -r cyan_skillfish.gfx1013.mmSPI_PG_ENABLE_STATIC_WGP_MASK 2>&1 | grep "=>" | awk '{print $NF}')
|
||||
local t0
|
||||
t0=$(get_temp)
|
||||
|
||||
local benchlib
|
||||
benchlib=$(dirname "$BENCH")
|
||||
LD_LIBRARY_PATH="$benchlib" "$BENCH" \
|
||||
-m "$MODEL" -p 512 -n 0 -ngl 99 -r 1 > "$outfile" 2>&1 &
|
||||
local bpid=$!
|
||||
sleep 2
|
||||
local pwr clk vdd t1
|
||||
pwr=$(get_power)
|
||||
clk=$(get_sclk)
|
||||
vdd=$(get_vddgfx)
|
||||
t1=$(get_temp)
|
||||
wait $bpid
|
||||
local tps
|
||||
tps=$(grep "pp512" "$outfile" | sed 's/.*|[[:space:]]*//' | awk '{print $1}')
|
||||
|
||||
printf " %-35s %7s tok/s %5s MHz %5s mV %6s W %s->%sC\n" \
|
||||
"$label (SPI=$spi)" "$tps" "$clk" "$vdd" "$pwr" "$((t0/1000))" "$((t1/1000))"
|
||||
}
|
||||
|
||||
# --- auto-detect ---
|
||||
[ "$(id -u)" = "0" ] || die "Must run as root"
|
||||
|
||||
if [ -z "$UMR" ]; then
|
||||
find_umr || die "UMR not found. Install from: https://gitlab.freedesktop.org/tomstdenis/umr"
|
||||
fi
|
||||
if [ -z "$MODEL" ]; then
|
||||
MODEL=$(find_model) || die "No GGUF model found. Pass path as first argument."
|
||||
fi
|
||||
if [ -z "$BENCH" ]; then
|
||||
BENCH=$(find_bench) || die "No Vulkan llama-bench found. Pass path as second argument."
|
||||
fi
|
||||
|
||||
[ -f "$MODEL" ] || die "Model not found: $MODEL"
|
||||
[ -x "$BENCH" ] || die "llama-bench not found: $BENCH"
|
||||
|
||||
# verify patched amdgpu
|
||||
cc_mode=$(cat /sys/module/amdgpu/parameters/bc250_cc_write_mode 2>/dev/null || echo "N/A")
|
||||
if [ "$cc_mode" != "3" ]; then
|
||||
die "bc250_cc_write_mode=$cc_mode (need 3). Enable the patched amdgpu first."
|
||||
fi
|
||||
|
||||
cu_count=$(dmesg | grep -o 'active_cu_number [0-9]*' | tail -1 | awk '{print $2}')
|
||||
|
||||
echo "================================================================="
|
||||
echo " BC-250 40 CU A/B/A Benchmark"
|
||||
echo "================================================================="
|
||||
echo ""
|
||||
echo " Model: $(basename "$MODEL")"
|
||||
echo " Bench: $BENCH"
|
||||
echo " UMR: $UMR"
|
||||
echo " CU enum: $cu_count CC mode: $cc_mode"
|
||||
echo " Governor: $(cat /etc/cyan-skillfish-governor/config.toml 2>/dev/null | grep 'frequency' | tail -1 | tr -d ' ')"
|
||||
echo ""
|
||||
printf " %-35s %7s %5s %5s %6s %s\n" \
|
||||
"State" "tok/s" "SCLK" "VDDGFX" "Power" "Temp"
|
||||
printf " %-35s %7s %5s %5s %6s %s\n" \
|
||||
"---" "-----" "----" "------" "-----" "----"
|
||||
|
||||
# A: 24 CU
|
||||
set_24cu
|
||||
wait_cool
|
||||
run_bench "24 CU (SPI=0x7)" /tmp/bench_24cu.txt
|
||||
|
||||
# B: 40 CU
|
||||
set_40cu
|
||||
wait_cool
|
||||
run_bench "40 CU (SPI=0x1F)" /tmp/bench_40cu.txt
|
||||
|
||||
# A: 24 CU confirm
|
||||
set_24cu
|
||||
wait_cool
|
||||
run_bench "24 CU confirm (SPI=0x7)" /tmp/bench_24cu_confirm.txt
|
||||
|
||||
# Restore 40 CU
|
||||
set_40cu
|
||||
|
||||
echo ""
|
||||
|
||||
# Extract tok/s for ratio
|
||||
tps_24=$(grep "pp512" /tmp/bench_24cu.txt | sed 's/.*|[[:space:]]*//' | awk '{print $1}')
|
||||
tps_40=$(grep "pp512" /tmp/bench_40cu.txt | sed 's/.*|[[:space:]]*//' | awk '{print $1}')
|
||||
ratio=$(awk "BEGIN {printf \"%.2f\", $tps_40 / $tps_24}")
|
||||
|
||||
echo " Ratio: $tps_40 / $tps_24 = ${ratio}x"
|
||||
echo ""
|
||||
echo " Expected: ~1.5-1.67x for compute-bound PP"
|
||||
echo " If ~1.0x: SPI write may not have taken effect (check dmesg)"
|
||||
echo ""
|
||||
echo "================================================================="
|
||||
+688
@@ -0,0 +1,688 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-compute-verify.sh - heavy Vulkan compute correctness test for BC-250.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ELEMENTS=16777216
|
||||
PASSES=3
|
||||
ITERS=64
|
||||
KEEP_TMP=0
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [--elements N] [--passes N] [--iters N] [--keep-tmp]
|
||||
|
||||
Runs a Vulkan compute correctness test with:
|
||||
- FP32 fma chains
|
||||
- integer multiply/add
|
||||
- bitwise rotate/xor/shift patterns
|
||||
- LDS shared-memory read/write
|
||||
- full per-element CPU golden comparison
|
||||
|
||||
ELEMENTS must be a multiple of 256. Default: $ELEMENTS
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--elements)
|
||||
ELEMENTS="${2:?missing value for --elements}"
|
||||
shift 2
|
||||
;;
|
||||
--passes)
|
||||
PASSES="${2:?missing value for --passes}"
|
||||
shift 2
|
||||
;;
|
||||
--iters)
|
||||
ITERS="${2:?missing value for --iters}"
|
||||
shift 2
|
||||
;;
|
||||
--keep-tmp)
|
||||
KEEP_TMP=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$ELEMENTS:$PASSES:$ITERS" in
|
||||
*[!0-9:]*|"")
|
||||
echo "ERROR: --elements, --passes, and --iters must be positive integers" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ELEMENTS" -le 0 ] || [ "$PASSES" -le 0 ] || [ "$ITERS" -le 0 ]; then
|
||||
echo "ERROR: --elements, --passes, and --iters must be positive integers" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ $((ELEMENTS % 256)) -ne 0 ]; then
|
||||
echo "ERROR: --elements must be a multiple of 256" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command -v glslangValidator >/dev/null 2>&1 || {
|
||||
echo "ERROR: glslangValidator not found" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v gcc >/dev/null 2>&1 || {
|
||||
echo "ERROR: gcc not found" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
TMPDIR="$(mktemp -d)"
|
||||
if [ "$KEEP_TMP" -eq 0 ]; then
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
else
|
||||
echo "Keeping temporary files in $TMPDIR"
|
||||
fi
|
||||
|
||||
cat >"$TMPDIR/bc250_compute_verify.comp" <<'GLSL'
|
||||
#version 450
|
||||
|
||||
layout(local_size_x = 256) in;
|
||||
|
||||
layout(std430, set = 0, binding = 0) readonly buffer InputA {
|
||||
uint a[];
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 1) readonly buffer InputB {
|
||||
uint b[];
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 2) writeonly buffer OutputInt {
|
||||
uint out_int[];
|
||||
};
|
||||
|
||||
layout(std430, set = 0, binding = 3) writeonly buffer OutputFp {
|
||||
uint out_fp[];
|
||||
};
|
||||
|
||||
layout(push_constant) uniform Params {
|
||||
uint n;
|
||||
uint seed;
|
||||
uint pass;
|
||||
uint iters;
|
||||
} pc;
|
||||
|
||||
shared uint lds[256];
|
||||
|
||||
uint rotl32(uint v, uint s)
|
||||
{
|
||||
s &= 31u;
|
||||
return s == 0u ? v : ((v << s) | (v >> (32u - s)));
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
uint idx = gl_GlobalInvocationID.x;
|
||||
uint lid = gl_LocalInvocationID.x;
|
||||
uint x = a[idx] ^ pc.seed ^ (pc.pass * 0x9e3779b9u);
|
||||
uint y = b[idx] + rotl32(idx ^ pc.seed, pc.pass + 7u);
|
||||
float f = uintBitsToFloat(0x3f800000u | (x & 0x007fffffu));
|
||||
|
||||
for (uint j = 0u; j < pc.iters; ++j) {
|
||||
x = x * 1664525u + 1013904223u + j + pc.pass;
|
||||
x ^= rotl32(y + j * 0x45d9f3bu, j + pc.pass);
|
||||
y += x ^ (j * 0x27d4eb2du) ^ (x >> ((j & 7u) + 1u));
|
||||
f = fma(f, 1.0009765625, float(int(y & 255u) - 128) * 0.00000011920928955078125);
|
||||
}
|
||||
|
||||
lds[lid] = x ^ y ^ pc.seed;
|
||||
barrier();
|
||||
|
||||
uint peer0 = lds[(lid * 17u + pc.pass) & 255u];
|
||||
uint peer1 = lds[(lid + 1u) & 255u];
|
||||
x ^= peer0 + rotl32(peer1, lid);
|
||||
y ^= rotl32(peer0 ^ peer1, pc.pass + 11u);
|
||||
|
||||
out_int[idx] = x ^ y ^ rotl32(idx + pc.seed, pc.pass);
|
||||
out_fp[idx] = floatBitsToUint(f);
|
||||
}
|
||||
GLSL
|
||||
|
||||
cat >"$TMPDIR/bc250_compute_verify.c" <<'C'
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define LOCAL_SIZE 256u
|
||||
|
||||
#define CHECK(call) do { \
|
||||
VkResult _res = (call); \
|
||||
if (_res != VK_SUCCESS) { \
|
||||
fprintf(stderr, "%s failed: %d at line %d\n", #call, _res, __LINE__); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
struct params {
|
||||
uint32_t n;
|
||||
uint32_t seed;
|
||||
uint32_t pass;
|
||||
uint32_t iters;
|
||||
};
|
||||
|
||||
static uint32_t rotl32(uint32_t v, uint32_t s)
|
||||
{
|
||||
s &= 31u;
|
||||
return s == 0u ? v : (uint32_t)((v << s) | (v >> (32u - s)));
|
||||
}
|
||||
|
||||
static uint32_t f32_bits(float f)
|
||||
{
|
||||
uint32_t u;
|
||||
memcpy(&u, &f, sizeof(u));
|
||||
return u;
|
||||
}
|
||||
|
||||
static float bits_f32(uint32_t u)
|
||||
{
|
||||
float f;
|
||||
memcpy(&f, &u, sizeof(f));
|
||||
return f;
|
||||
}
|
||||
|
||||
static uint32_t fp32_ordered_bits(uint32_t bits)
|
||||
{
|
||||
if (bits & 0x80000000u)
|
||||
return 0x80000000u - (bits & 0x7fffffffu);
|
||||
return 0x80000000u + bits;
|
||||
}
|
||||
|
||||
static uint32_t fp32_ulp_distance(uint32_t a, uint32_t b)
|
||||
{
|
||||
uint32_t oa = fp32_ordered_bits(a);
|
||||
uint32_t ob = fp32_ordered_bits(b);
|
||||
|
||||
return oa > ob ? oa - ob : ob - oa;
|
||||
}
|
||||
|
||||
static void pre_lds_expected(uint32_t idx, const uint32_t *a, const uint32_t *b,
|
||||
const struct params *p, uint32_t *x_out,
|
||||
uint32_t *y_out, uint32_t *fp_out)
|
||||
{
|
||||
uint32_t x = a[idx] ^ p->seed ^ (p->pass * 0x9e3779b9u);
|
||||
uint32_t y = b[idx] + rotl32(idx ^ p->seed, p->pass + 7u);
|
||||
float f = bits_f32(0x3f800000u | (x & 0x007fffffu));
|
||||
|
||||
for (uint32_t j = 0; j < p->iters; ++j) {
|
||||
x = x * 1664525u + 1013904223u + j + p->pass;
|
||||
x ^= rotl32(y + j * 0x45d9f3bu, j + p->pass);
|
||||
y += x ^ (j * 0x27d4eb2du) ^ (x >> ((j & 7u) + 1u));
|
||||
f = fmaf(f, 1.0009765625f,
|
||||
(float)((int)(y & 255u) - 128) * 0.00000011920928955078125f);
|
||||
}
|
||||
|
||||
*x_out = x;
|
||||
*y_out = y;
|
||||
*fp_out = f32_bits(f);
|
||||
}
|
||||
|
||||
static void final_expected(uint32_t idx, const uint32_t *lds,
|
||||
uint32_t x, uint32_t y, const struct params *p,
|
||||
uint32_t *int_out)
|
||||
{
|
||||
uint32_t lid = idx & (LOCAL_SIZE - 1u);
|
||||
uint32_t peer0 = lds[(lid * 17u + p->pass) & 255u];
|
||||
uint32_t peer1 = lds[(lid + 1u) & 255u];
|
||||
|
||||
x ^= peer0 + rotl32(peer1, lid);
|
||||
y ^= rotl32(peer0 ^ peer1, p->pass + 11u);
|
||||
*int_out = x ^ y ^ rotl32(idx + p->seed, p->pass);
|
||||
}
|
||||
|
||||
static uint32_t find_memory_type(VkPhysicalDevice pd, uint32_t bits,
|
||||
VkMemoryPropertyFlags flags)
|
||||
{
|
||||
VkPhysicalDeviceMemoryProperties props;
|
||||
|
||||
vkGetPhysicalDeviceMemoryProperties(pd, &props);
|
||||
for (uint32_t i = 0; i < props.memoryTypeCount; ++i) {
|
||||
if ((bits & (1u << i)) &&
|
||||
(props.memoryTypes[i].propertyFlags & flags) == flags)
|
||||
return i;
|
||||
}
|
||||
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
static int read_file(const char *path, char **buf, size_t *size)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
long len;
|
||||
|
||||
if (!f)
|
||||
return 1;
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
len = ftell(f);
|
||||
if (len <= 0) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
rewind(f);
|
||||
*buf = malloc((size_t)len);
|
||||
if (!*buf) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
if (fread(*buf, 1, (size_t)len, f) != (size_t)len) {
|
||||
fclose(f);
|
||||
free(*buf);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
*size = (size_t)len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double now_sec(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *spv_path;
|
||||
uint32_t n;
|
||||
uint32_t passes;
|
||||
uint32_t iters;
|
||||
const VkDeviceSize bytes_in = 0;
|
||||
VkApplicationInfo app = {
|
||||
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
|
||||
.pApplicationName = "bc250-compute-verify",
|
||||
.apiVersion = VK_API_VERSION_1_1,
|
||||
};
|
||||
VkInstanceCreateInfo ici = {
|
||||
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
|
||||
.pApplicationInfo = &app,
|
||||
};
|
||||
VkInstance instance;
|
||||
VkPhysicalDevice pds[16];
|
||||
uint32_t pd_count = 16;
|
||||
VkPhysicalDevice pd = VK_NULL_HANDLE;
|
||||
VkPhysicalDeviceProperties pd_props;
|
||||
uint32_t queue_family = UINT32_MAX;
|
||||
VkQueueFamilyProperties qprops[32];
|
||||
uint32_t qcount = 32;
|
||||
float priority = 1.0f;
|
||||
VkDeviceQueueCreateInfo qci = {
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
|
||||
.queueCount = 1,
|
||||
.pQueuePriorities = &priority,
|
||||
};
|
||||
VkDeviceCreateInfo dci = {
|
||||
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
|
||||
.queueCreateInfoCount = 1,
|
||||
.pQueueCreateInfos = &qci,
|
||||
};
|
||||
VkDevice dev;
|
||||
VkQueue queue;
|
||||
VkBuffer buffers[4] = {0};
|
||||
VkDeviceMemory memories[4] = {0};
|
||||
void *maps[4] = {0};
|
||||
VkDescriptorSetLayoutBinding bindings[4];
|
||||
VkDescriptorSetLayoutCreateInfo dsli = {
|
||||
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
||||
.bindingCount = 4,
|
||||
.pBindings = bindings,
|
||||
};
|
||||
VkDescriptorSetLayout dsl;
|
||||
VkPushConstantRange pcr = {
|
||||
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
|
||||
.offset = 0,
|
||||
.size = sizeof(struct params),
|
||||
};
|
||||
VkPipelineLayoutCreateInfo plci = {
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
|
||||
.setLayoutCount = 1,
|
||||
.pSetLayouts = &dsl,
|
||||
.pushConstantRangeCount = 1,
|
||||
.pPushConstantRanges = &pcr,
|
||||
};
|
||||
VkPipelineLayout pipeline_layout;
|
||||
char *spv = NULL;
|
||||
size_t spv_size = 0;
|
||||
VkShaderModuleCreateInfo smci = {
|
||||
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
|
||||
};
|
||||
VkShaderModule shader;
|
||||
VkComputePipelineCreateInfo cpci = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||
};
|
||||
VkPipeline pipeline;
|
||||
VkDescriptorPoolSize pool_size = {
|
||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.descriptorCount = 4,
|
||||
};
|
||||
VkDescriptorPoolCreateInfo dpci = {
|
||||
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
|
||||
.maxSets = 1,
|
||||
.poolSizeCount = 1,
|
||||
.pPoolSizes = &pool_size,
|
||||
};
|
||||
VkDescriptorPool pool;
|
||||
VkDescriptorSetAllocateInfo dsai = {
|
||||
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
|
||||
.descriptorSetCount = 1,
|
||||
};
|
||||
VkDescriptorSet ds;
|
||||
VkCommandPoolCreateInfo cmdp_ci = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
|
||||
};
|
||||
VkCommandPool cmd_pool;
|
||||
VkFenceCreateInfo fci = {
|
||||
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
|
||||
};
|
||||
VkFence fence;
|
||||
uint64_t total_errors = 0;
|
||||
uint64_t total_fp_errors = 0;
|
||||
uint64_t total_int_errors = 0;
|
||||
uint32_t first_error_pass = UINT32_MAX;
|
||||
|
||||
(void)bytes_in;
|
||||
if (argc != 5) {
|
||||
fprintf(stderr, "usage: %s shader.spv elements passes iters\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
spv_path = argv[1];
|
||||
n = (uint32_t)strtoul(argv[2], NULL, 0);
|
||||
passes = (uint32_t)strtoul(argv[3], NULL, 0);
|
||||
iters = (uint32_t)strtoul(argv[4], NULL, 0);
|
||||
if (!n || !passes || !iters || (n % LOCAL_SIZE) != 0) {
|
||||
fprintf(stderr, "invalid elements/passes/iters\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const VkDeviceSize bytes = (VkDeviceSize)n * sizeof(uint32_t);
|
||||
|
||||
CHECK(vkCreateInstance(&ici, NULL, &instance));
|
||||
CHECK(vkEnumeratePhysicalDevices(instance, &pd_count, pds));
|
||||
for (uint32_t i = 0; i < pd_count; ++i) {
|
||||
vkGetPhysicalDeviceProperties(pds[i], &pd_props);
|
||||
if (pd_props.vendorID == 0x1002 && strstr(pd_props.deviceName, "BC-250")) {
|
||||
pd = pds[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pd == VK_NULL_HANDLE) {
|
||||
for (uint32_t i = 0; i < pd_count; ++i) {
|
||||
vkGetPhysicalDeviceProperties(pds[i], &pd_props);
|
||||
if (pd_props.vendorID == 0x1002) {
|
||||
pd = pds[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pd == VK_NULL_HANDLE) {
|
||||
fprintf(stderr, "AMD Vulkan device not found\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
vkGetPhysicalDeviceProperties(pd, &pd_props);
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(pd, &qcount, qprops);
|
||||
for (uint32_t i = 0; i < qcount; ++i) {
|
||||
if (qprops[i].queueFlags & VK_QUEUE_COMPUTE_BIT) {
|
||||
queue_family = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (queue_family == UINT32_MAX) {
|
||||
fprintf(stderr, "compute queue not found\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
qci.queueFamilyIndex = queue_family;
|
||||
CHECK(vkCreateDevice(pd, &dci, NULL, &dev));
|
||||
vkGetDeviceQueue(dev, queue_family, 0, &queue);
|
||||
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
VkBufferCreateInfo bci = {
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
||||
.size = bytes,
|
||||
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
};
|
||||
VkMemoryRequirements req;
|
||||
VkMemoryAllocateInfo mai = {
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||
};
|
||||
uint32_t mem_type;
|
||||
|
||||
CHECK(vkCreateBuffer(dev, &bci, NULL, &buffers[i]));
|
||||
vkGetBufferMemoryRequirements(dev, buffers[i], &req);
|
||||
mem_type = find_memory_type(pd, req.memoryTypeBits,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
|
||||
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
if (mem_type == UINT32_MAX) {
|
||||
fprintf(stderr, "host visible coherent memory not found\n");
|
||||
return 1;
|
||||
}
|
||||
mai.allocationSize = req.size;
|
||||
mai.memoryTypeIndex = mem_type;
|
||||
CHECK(vkAllocateMemory(dev, &mai, NULL, &memories[i]));
|
||||
CHECK(vkBindBufferMemory(dev, buffers[i], memories[i], 0));
|
||||
CHECK(vkMapMemory(dev, memories[i], 0, bytes, 0, &maps[i]));
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
((uint32_t *)maps[0])[i] = i * 17u + 3u;
|
||||
((uint32_t *)maps[1])[i] = rotl32(i ^ 0x9e3779b9u, i & 31u) + 0x85ebca6bu;
|
||||
((uint32_t *)maps[2])[i] = 0;
|
||||
((uint32_t *)maps[3])[i] = 0;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
bindings[i].binding = i;
|
||||
bindings[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
bindings[i].descriptorCount = 1;
|
||||
bindings[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
bindings[i].pImmutableSamplers = NULL;
|
||||
}
|
||||
CHECK(vkCreateDescriptorSetLayout(dev, &dsli, NULL, &dsl));
|
||||
CHECK(vkCreatePipelineLayout(dev, &plci, NULL, &pipeline_layout));
|
||||
if (read_file(spv_path, &spv, &spv_size)) {
|
||||
fprintf(stderr, "failed to read SPIR-V shader: %s\n", spv_path);
|
||||
return 1;
|
||||
}
|
||||
smci.codeSize = spv_size;
|
||||
smci.pCode = (const uint32_t *)spv;
|
||||
CHECK(vkCreateShaderModule(dev, &smci, NULL, &shader));
|
||||
cpci.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
cpci.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
cpci.stage.module = shader;
|
||||
cpci.stage.pName = "main";
|
||||
cpci.layout = pipeline_layout;
|
||||
CHECK(vkCreateComputePipelines(dev, VK_NULL_HANDLE, 1, &cpci, NULL, &pipeline));
|
||||
|
||||
CHECK(vkCreateDescriptorPool(dev, &dpci, NULL, &pool));
|
||||
dsai.descriptorPool = pool;
|
||||
dsai.pSetLayouts = &dsl;
|
||||
CHECK(vkAllocateDescriptorSets(dev, &dsai, &ds));
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
VkDescriptorBufferInfo dbi = {
|
||||
.buffer = buffers[i],
|
||||
.offset = 0,
|
||||
.range = bytes,
|
||||
};
|
||||
VkWriteDescriptorSet wds = {
|
||||
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
|
||||
.dstSet = ds,
|
||||
.dstBinding = i,
|
||||
.descriptorCount = 1,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
.pBufferInfo = &dbi,
|
||||
};
|
||||
vkUpdateDescriptorSets(dev, 1, &wds, 0, NULL);
|
||||
}
|
||||
|
||||
cmdp_ci.queueFamilyIndex = queue_family;
|
||||
CHECK(vkCreateCommandPool(dev, &cmdp_ci, NULL, &cmd_pool));
|
||||
CHECK(vkCreateFence(dev, &fci, NULL, &fence));
|
||||
|
||||
printf("device=%s queue_family=%u elements=%u passes=%u iters=%u\n",
|
||||
pd_props.deviceName, queue_family, n, passes, iters);
|
||||
|
||||
for (uint32_t pass = 0; pass < passes; ++pass) {
|
||||
VkCommandBufferAllocateInfo cbai = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.commandPool = cmd_pool,
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = 1,
|
||||
};
|
||||
VkCommandBuffer cmd;
|
||||
VkCommandBufferBeginInfo cbbi = {
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
};
|
||||
struct params p = {
|
||||
.n = n,
|
||||
.seed = 0xa5a5a5a5u ^ pass * 0x12345u,
|
||||
.pass = pass,
|
||||
.iters = iters,
|
||||
};
|
||||
uint64_t pass_errors = 0;
|
||||
uint64_t pass_fp_errors = 0;
|
||||
uint64_t pass_int_errors = 0;
|
||||
double t0;
|
||||
double t1;
|
||||
|
||||
memset(maps[2], 0, (size_t)bytes);
|
||||
memset(maps[3], 0, (size_t)bytes);
|
||||
|
||||
CHECK(vkAllocateCommandBuffers(dev, &cbai, &cmd));
|
||||
CHECK(vkBeginCommandBuffer(cmd, &cbbi));
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout,
|
||||
0, 1, &ds, 0, NULL);
|
||||
vkCmdPushConstants(cmd, pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT,
|
||||
0, sizeof(p), &p);
|
||||
vkCmdDispatch(cmd, n / LOCAL_SIZE, 1, 1);
|
||||
CHECK(vkEndCommandBuffer(cmd));
|
||||
|
||||
{
|
||||
VkSubmitInfo si = {
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &cmd,
|
||||
};
|
||||
t0 = now_sec();
|
||||
CHECK(vkQueueSubmit(queue, 1, &si, fence));
|
||||
CHECK(vkWaitForFences(dev, 1, &fence, VK_TRUE, UINT64_MAX));
|
||||
t1 = now_sec();
|
||||
CHECK(vkResetFences(dev, 1, &fence));
|
||||
}
|
||||
|
||||
for (uint32_t base = 0; base < n; base += LOCAL_SIZE) {
|
||||
uint32_t x[LOCAL_SIZE];
|
||||
uint32_t y[LOCAL_SIZE];
|
||||
uint32_t fp[LOCAL_SIZE];
|
||||
uint32_t lds[LOCAL_SIZE];
|
||||
|
||||
for (uint32_t lane = 0; lane < LOCAL_SIZE; ++lane) {
|
||||
uint32_t idx = base + lane;
|
||||
pre_lds_expected(idx, maps[0], maps[1], &p,
|
||||
&x[lane], &y[lane], &fp[lane]);
|
||||
lds[lane] = x[lane] ^ y[lane] ^ p.seed;
|
||||
}
|
||||
|
||||
for (uint32_t lane = 0; lane < LOCAL_SIZE; ++lane) {
|
||||
uint32_t idx = base + lane;
|
||||
uint32_t want_int;
|
||||
uint32_t got_int = ((uint32_t *)maps[2])[idx];
|
||||
uint32_t got_fp = ((uint32_t *)maps[3])[idx];
|
||||
|
||||
final_expected(idx, lds, x[lane], y[lane], &p, &want_int);
|
||||
if (got_int != want_int) {
|
||||
if (pass_errors < 16) {
|
||||
fprintf(stderr,
|
||||
"int mismatch pass=%u idx=%u got=0x%08x want=0x%08x\n",
|
||||
pass, idx, got_int, want_int);
|
||||
}
|
||||
pass_errors++;
|
||||
pass_int_errors++;
|
||||
}
|
||||
{
|
||||
uint32_t ulp_diff = fp32_ulp_distance(got_fp, fp[lane]);
|
||||
|
||||
if (ulp_diff > (p.iters / 3 + 2)) {
|
||||
if (pass_errors < 16) {
|
||||
fprintf(stderr,
|
||||
"fp mismatch pass=%u idx=%u got=0x%08x want=0x%08x ulp=%" PRIu32 "\n",
|
||||
pass, idx, got_fp, fp[lane], ulp_diff);
|
||||
}
|
||||
pass_errors++;
|
||||
pass_fp_errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("pass=%u dispatch_sec=%.6f errors=%" PRIu64 " int_errors=%" PRIu64 " fp_errors=%" PRIu64 "\n",
|
||||
pass, t1 - t0, pass_errors, pass_int_errors, pass_fp_errors);
|
||||
|
||||
if (pass_errors && first_error_pass == UINT32_MAX)
|
||||
first_error_pass = pass;
|
||||
total_errors += pass_errors;
|
||||
total_int_errors += pass_int_errors;
|
||||
total_fp_errors += pass_fp_errors;
|
||||
vkFreeCommandBuffers(dev, cmd_pool, 1, &cmd);
|
||||
}
|
||||
|
||||
printf("summary elements=%u passes=%u total_checked=%" PRIu64 " errors=%" PRIu64 " int_errors=%" PRIu64 " fp_errors=%" PRIu64 "\n",
|
||||
n, passes, (uint64_t)n * passes * 2u, total_errors,
|
||||
total_int_errors, total_fp_errors);
|
||||
|
||||
if (first_error_pass != UINT32_MAX)
|
||||
printf("first_error_pass=%u\n", first_error_pass);
|
||||
|
||||
vkDestroyFence(dev, fence, NULL);
|
||||
vkDestroyCommandPool(dev, cmd_pool, NULL);
|
||||
vkDestroyDescriptorPool(dev, pool, NULL);
|
||||
vkDestroyPipeline(dev, pipeline, NULL);
|
||||
vkDestroyShaderModule(dev, shader, NULL);
|
||||
vkDestroyPipelineLayout(dev, pipeline_layout, NULL);
|
||||
vkDestroyDescriptorSetLayout(dev, dsl, NULL);
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
vkUnmapMemory(dev, memories[i]);
|
||||
vkFreeMemory(dev, memories[i], NULL);
|
||||
vkDestroyBuffer(dev, buffers[i], NULL);
|
||||
}
|
||||
vkDestroyDevice(dev, NULL);
|
||||
vkDestroyInstance(instance, NULL);
|
||||
free(spv);
|
||||
|
||||
return total_errors ? 2 : 0;
|
||||
}
|
||||
C
|
||||
|
||||
echo "Compiling compute verifier..."
|
||||
glslangValidator -V "$TMPDIR/bc250_compute_verify.comp" -o "$TMPDIR/bc250_compute_verify.spv" >/dev/null
|
||||
gcc -std=c11 -O2 -Wall -Wextra -o "$TMPDIR/bc250_compute_verify" \
|
||||
"$TMPDIR/bc250_compute_verify.c" -lvulkan -lm
|
||||
|
||||
echo "Running BC-250 compute verifier..."
|
||||
"$TMPDIR/bc250_compute_verify" "$TMPDIR/bc250_compute_verify.spv" "$ELEMENTS" "$PASSES" "$ITERS"
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-cu-health-test.sh - reboot-resuming per-WGP health test for BC-250.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STATEDIR="${BC250_CU_HEALTH_STATE:-/var/lib/bc250-cu-health-test}"
|
||||
CONF="${BC250_CU_HEALTH_CONF:-/etc/modprobe.d/bc250-cu-health-isolate.conf}"
|
||||
SERVICE="${BC250_CU_HEALTH_SERVICE:-bc250-cu-health-resume.service}"
|
||||
VERIFY="${BC250_CU_VERIFY:-$(dirname "$(readlink -f "$0")")/bc250-compute-verify.sh}"
|
||||
ELEMENTS="${BC250_CU_HEALTH_ELEMENTS:-16777216}"
|
||||
PASSES="${BC250_CU_HEALTH_PASSES:-2}"
|
||||
ITERS="${BC250_CU_HEALTH_ITERS:-64}"
|
||||
REBOOT_DELAY="${BC250_CU_HEALTH_REBOOT_DELAY:-5}"
|
||||
FINAL_REBOOT="${BC250_CU_HEALTH_FINAL_REBOOT:-1}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 start|resume|quick|status|reset
|
||||
|
||||
Commands:
|
||||
start Install a systemd resume service and test all 20 WGPs across reboots.
|
||||
resume Internal command used by systemd after each reboot.
|
||||
quick Run the heavy verifier once in the current CU configuration.
|
||||
status Print saved progress/results.
|
||||
reset Remove state, resume service, and isolation config.
|
||||
|
||||
Environment:
|
||||
BC250_CU_HEALTH_ELEMENTS=$ELEMENTS
|
||||
BC250_CU_HEALTH_PASSES=$PASSES
|
||||
BC250_CU_HEALTH_ITERS=$ITERS
|
||||
BC250_CU_HEALTH_FINAL_REBOOT=$FINAL_REBOOT
|
||||
|
||||
Results:
|
||||
$STATEDIR/results.tsv
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_root() {
|
||||
[ "$(id -u)" = "0" ] || die "must run as root"
|
||||
}
|
||||
|
||||
target_to_tuple() {
|
||||
local idx="$1"
|
||||
local se sh wgp
|
||||
|
||||
se=$((idx / 10))
|
||||
sh=$(((idx / 5) % 2))
|
||||
wgp=$((idx % 5))
|
||||
echo "$se $sh $wgp"
|
||||
}
|
||||
|
||||
disable_cu_for_target() {
|
||||
local target="$1"
|
||||
local out=()
|
||||
local idx se sh wgp tse tsh twgp cu
|
||||
|
||||
read -r tse tsh twgp < <(target_to_tuple "$target")
|
||||
for idx in $(seq 0 19); do
|
||||
read -r se sh wgp < <(target_to_tuple "$idx")
|
||||
if [ "$se" -eq "$tse" ] && [ "$sh" -eq "$tsh" ] && [ "$wgp" -eq "$twgp" ]; then
|
||||
continue
|
||||
fi
|
||||
out+=("$se.$sh.$((wgp * 2))")
|
||||
out+=("$se.$sh.$((wgp * 2 + 1))")
|
||||
done
|
||||
IFS=,
|
||||
echo "${out[*]}"
|
||||
}
|
||||
|
||||
write_config_for_target() {
|
||||
local target="$1"
|
||||
local disable_csv
|
||||
local se sh wgp
|
||||
|
||||
read -r se sh wgp < <(target_to_tuple "$target")
|
||||
disable_csv="$(disable_cu_for_target "$target")"
|
||||
cat >"$CONF" <<EOF
|
||||
# BC-250 per-WGP health-test isolation config.
|
||||
# Target: index=$target SE$se SH$sh WGP$wgp CUs $((wgp * 2))-$((wgp * 2 + 1)).
|
||||
# Generated by $0 on $(date -Iseconds).
|
||||
options amdgpu bc250_cc_write_mode=3 disable_cu=$disable_csv
|
||||
EOF
|
||||
}
|
||||
|
||||
write_full_config() {
|
||||
cat >"$CONF" <<EOF
|
||||
# BC-250 full 40-CU config restored after health test.
|
||||
options amdgpu bc250_cc_write_mode=3
|
||||
EOF
|
||||
}
|
||||
|
||||
refresh_initramfs() {
|
||||
if command -v update-initramfs >/dev/null 2>&1; then
|
||||
update-initramfs -u -k "$(uname -r)" || true
|
||||
elif command -v dracut >/dev/null 2>&1; then
|
||||
dracut -f || true
|
||||
fi
|
||||
}
|
||||
|
||||
install_service() {
|
||||
local self
|
||||
|
||||
self="$(readlink -f "$0")"
|
||||
cat >/etc/systemd/system/"$SERVICE" <<EOF
|
||||
[Unit]
|
||||
Description=Resume BC-250 per-WGP CU health test
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash $self resume
|
||||
StandardOutput=journal+console
|
||||
StandardError=journal+console
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE"
|
||||
}
|
||||
|
||||
remove_service() {
|
||||
systemctl disable "$SERVICE" >/dev/null 2>&1 || true
|
||||
rm -f /etc/systemd/system/"$SERVICE"
|
||||
systemctl daemon-reload || true
|
||||
}
|
||||
|
||||
current_cu_count() {
|
||||
dmesg | grep -o 'active_cu_number [0-9]*' | tail -1 | awk '{print $2}'
|
||||
}
|
||||
|
||||
run_verify_for_target() {
|
||||
local target="$1"
|
||||
local se sh wgp log status rc cu_count started finished
|
||||
|
||||
read -r se sh wgp < <(target_to_tuple "$target")
|
||||
log="$STATEDIR/logs/wgp-${target}-se${se}-sh${sh}-wgp${wgp}.log"
|
||||
started="$(date -Iseconds)"
|
||||
cu_count="$(current_cu_count || true)"
|
||||
status="PASS"
|
||||
rc=0
|
||||
|
||||
echo "Testing target index=$target SE$se SH$sh WGP$wgp..."
|
||||
set +e
|
||||
"$VERIFY" --elements "$ELEMENTS" --passes "$PASSES" --iters "$ITERS" 2>&1 | tee "$log"
|
||||
rc=${PIPESTATUS[0]}
|
||||
set -e
|
||||
[ "$rc" -eq 0 ] || status="FAIL"
|
||||
finished="$(date -Iseconds)"
|
||||
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$target" "$se" "$sh" "$wgp" "$status" "$rc" \
|
||||
"${cu_count:-unknown}" "$started" "$finished" >>"$STATEDIR/results.tsv"
|
||||
}
|
||||
|
||||
next_reboot() {
|
||||
local target="$1"
|
||||
local se sh wgp
|
||||
|
||||
read -r se sh wgp < <(target_to_tuple "$target")
|
||||
write_config_for_target "$target"
|
||||
echo "$target" >"$STATEDIR/current_target"
|
||||
refresh_initramfs
|
||||
echo "Configured next isolated target: index=$target SE$se SH$sh WGP$wgp."
|
||||
echo "Rebooting in $REBOOT_DELAY seconds..."
|
||||
sleep "$REBOOT_DELAY"
|
||||
reboot
|
||||
}
|
||||
|
||||
finish_test() {
|
||||
write_full_config
|
||||
refresh_initramfs
|
||||
remove_service
|
||||
echo "done" >"$STATEDIR/phase"
|
||||
echo "Per-WGP health test complete."
|
||||
echo "Results: $STATEDIR/results.tsv"
|
||||
awk -F'\t' 'BEGIN{pass=0; fail=0} $5=="PASS"{pass++} $5=="FAIL"{fail++} END{printf "PASS WGPs: %d FAIL WGPs: %d usable CUs: %d/40\n", pass, fail, 40 - fail * 2}' "$STATEDIR/results.tsv"
|
||||
if [ "$FINAL_REBOOT" = "1" ]; then
|
||||
echo "Rebooting in $REBOOT_DELAY seconds to reload amdgpu with the restored full-40CU config..."
|
||||
sleep "$REBOOT_DELAY"
|
||||
reboot
|
||||
else
|
||||
echo "Full 40-CU config is written for the next boot; current boot remains on the last isolated WGP."
|
||||
fi
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
start)
|
||||
need_root
|
||||
[ -x "$VERIFY" ] || die "verifier not executable: $VERIFY"
|
||||
mkdir -p "$STATEDIR/logs"
|
||||
cat >"$STATEDIR/results.tsv" <<'EOF'
|
||||
#idx se sh wgp status rc active_cu started finished
|
||||
EOF
|
||||
echo "running" >"$STATEDIR/phase"
|
||||
install_service
|
||||
next_reboot 0
|
||||
;;
|
||||
resume)
|
||||
need_root
|
||||
[ -x "$VERIFY" ] || die "verifier not executable: $VERIFY"
|
||||
mkdir -p "$STATEDIR/logs"
|
||||
phase="$(cat "$STATEDIR/phase" 2>/dev/null || echo missing)"
|
||||
[ "$phase" = "running" ] || die "health test is not running (phase=$phase)"
|
||||
target="$(cat "$STATEDIR/current_target" 2>/dev/null || echo 0)"
|
||||
run_verify_for_target "$target"
|
||||
next=$((target + 1))
|
||||
if [ "$next" -lt 20 ]; then
|
||||
next_reboot "$next"
|
||||
else
|
||||
finish_test
|
||||
fi
|
||||
;;
|
||||
quick)
|
||||
[ -x "$VERIFY" ] || die "verifier not executable: $VERIFY"
|
||||
"$VERIFY" --elements "$ELEMENTS" --passes "$PASSES" --iters "$ITERS"
|
||||
;;
|
||||
status)
|
||||
if [ -f "$STATEDIR/results.tsv" ]; then
|
||||
cat "$STATEDIR/results.tsv"
|
||||
else
|
||||
echo "No health-test results in $STATEDIR"
|
||||
fi
|
||||
;;
|
||||
reset)
|
||||
need_root
|
||||
remove_service
|
||||
rm -rf "$STATEDIR"
|
||||
rm -f "$CONF"
|
||||
refresh_initramfs
|
||||
echo "Removed health-test state, service, and isolation config."
|
||||
;;
|
||||
-h|--help|"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
die "unknown command: $cmd"
|
||||
;;
|
||||
esac
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-cu-mask.sh - generate/install selective WGP masks for BC-250 40-CU unlock.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RESULTS="${BC250_CU_HEALTH_RESULTS:-/var/lib/bc250-cu-health-test/results.tsv}"
|
||||
CONF="${BC250_CU_MASK_CONF:-/etc/modprobe.d/bc250-40cu-selective-mask.conf}"
|
||||
INSTALL=0
|
||||
BAD_ITEMS=()
|
||||
TMP_CONF="$(mktemp)"
|
||||
trap 'rm -f "$TMP_CONF"' EXIT
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [--results FILE] [--bad SE.SH.WGP[,SE.SH.WGP...]] [--bad-cu SE.SH.CU[,SE.SH.CU...]] [--install]
|
||||
|
||||
Examples:
|
||||
$0 --bad 1.0.3
|
||||
$0 --results /var/lib/bc250-cu-health-test/results.tsv --install
|
||||
$0 --bad-cu 1.0.6,1.0.7 --install
|
||||
|
||||
Output modprobe config:
|
||||
options amdgpu bc250_cc_write_mode=3 disable_cu=SE.SH.CU,...
|
||||
|
||||
WGP is the hardware disable granularity. Each bad WGP expands to both CUs.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
split_csv() {
|
||||
local item
|
||||
IFS=',' read -ra _items <<<"$1"
|
||||
for item in "${_items[@]}"; do
|
||||
[ -n "$item" ] && BAD_ITEMS+=("$item")
|
||||
done
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--results)
|
||||
RESULTS="${2:?missing value for --results}"
|
||||
shift 2
|
||||
;;
|
||||
--bad)
|
||||
split_csv "${2:?missing value for --bad}"
|
||||
shift 2
|
||||
;;
|
||||
--bad-cu)
|
||||
IFS=',' read -ra _cus <<<"${2:?missing value for --bad-cu}"
|
||||
for cu_item in "${_cus[@]}"; do
|
||||
IFS='.' read -r se sh cu <<<"$cu_item"
|
||||
[[ "$se" =~ ^[0-1]$ && "$sh" =~ ^[0-1]$ && "$cu" =~ ^[0-9]$ ]] ||
|
||||
die "invalid CU entry: $cu_item"
|
||||
BAD_ITEMS+=("$se.$sh.$((cu / 2))")
|
||||
done
|
||||
shift 2
|
||||
;;
|
||||
--install)
|
||||
INSTALL=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "${#BAD_ITEMS[@]}" -eq 0 ] && [ -f "$RESULTS" ]; then
|
||||
while IFS=$'\t' read -r idx se sh wgp status rest; do
|
||||
case "$idx" in ""|\#*) continue ;; esac
|
||||
[ "$status" = "FAIL" ] || continue
|
||||
BAD_ITEMS+=("$se.$sh.$wgp")
|
||||
done <"$RESULTS"
|
||||
fi
|
||||
|
||||
declare -A seen_wgp=()
|
||||
declare -A seen_cu=()
|
||||
bad_wgps=()
|
||||
disable_cus=()
|
||||
|
||||
for item in "${BAD_ITEMS[@]}"; do
|
||||
IFS='.' read -r se sh wgp <<<"$item"
|
||||
[[ "$se" =~ ^[0-1]$ && "$sh" =~ ^[0-1]$ && "$wgp" =~ ^[0-4]$ ]] ||
|
||||
die "invalid WGP entry: $item"
|
||||
key="$se.$sh.$wgp"
|
||||
if [ -z "${seen_wgp[$key]+x}" ]; then
|
||||
seen_wgp[$key]=1
|
||||
bad_wgps+=("$key")
|
||||
fi
|
||||
for cu in $((wgp * 2)) $((wgp * 2 + 1)); do
|
||||
cu_key="$se.$sh.$cu"
|
||||
if [ -z "${seen_cu[$cu_key]+x}" ]; then
|
||||
seen_cu[$cu_key]=1
|
||||
disable_cus+=("$cu_key")
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
disable_csv="$(IFS=,; echo "${disable_cus[*]}")"
|
||||
|
||||
{
|
||||
echo "# BC-250 selective 40-CU mask."
|
||||
echo "# Generated by $0 on $(date -Iseconds)."
|
||||
echo "# Bad WGPs: ${bad_wgps[*]:-none}"
|
||||
echo "# Requires patched amdgpu with bc250_cc_write_mode support."
|
||||
if [ -n "$disable_csv" ]; then
|
||||
echo "options amdgpu bc250_cc_write_mode=3 disable_cu=$disable_csv"
|
||||
else
|
||||
echo "options amdgpu bc250_cc_write_mode=3"
|
||||
fi
|
||||
} >"$TMP_CONF"
|
||||
|
||||
if [ "$INSTALL" -eq 1 ]; then
|
||||
[ "$(id -u)" = "0" ] || die "--install requires root"
|
||||
install -m 0644 "$TMP_CONF" "$CONF"
|
||||
echo "Installed $CONF"
|
||||
if command -v update-initramfs >/dev/null 2>&1; then
|
||||
update-initramfs -u -k "$(uname -r)" || true
|
||||
elif command -v dracut >/dev/null 2>&1; then
|
||||
dracut -f || true
|
||||
fi
|
||||
else
|
||||
cat "$TMP_CONF"
|
||||
fi
|
||||
|
||||
usable=$((40 - ${#bad_wgps[@]} * 2))
|
||||
echo "# Usable after mask: $usable/40 CUs (${#bad_wgps[@]} bad WGPs masked)"
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-enable-40cu-arch.sh — Build and install a patched amdgpu for 40 CU on BC-250
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./bc250-enable-40cu-arch.sh build # patch + compile + install
|
||||
# sudo ./bc250-enable-40cu-arch.sh enable # set 40 CU mode and reboot
|
||||
# sudo ./bc250-enable-40cu-arch.sh disable # return to stock 24 CU and reboot
|
||||
# sudo ./bc250-enable-40cu-arch.sh status # show current CU state
|
||||
# sudo ./bc250-enable-40cu-arch.sh restore # restore original amdgpu module
|
||||
#
|
||||
# Requirements: linux-headers, gcc, make, zstd, curl. Must run as root on BC-250.
|
||||
# Tested on: Arch Linux
|
||||
#
|
||||
# Authors: duggasco, Claude | License: GPL-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KVER="$(uname -r)"
|
||||
KVER_BASE="${KVER%%-*}" # e.g. 6.9.3 from 6.9.3-arch1-1
|
||||
MODDIR="/usr/lib/modules/${KVER}"
|
||||
MODPATH="${MODDIR}/kernel/drivers/gpu/drm/amd/amdgpu/amdgpu.ko"
|
||||
MODSRC=""
|
||||
BUILDDIR="/tmp/bc250-40cu-build"
|
||||
CONF40="/etc/modprobe.d/bc250-40cu.conf"
|
||||
BACKUP_SUFFIX=".bc250-backup-$(date +%Y%m%d)"
|
||||
BC250_PCI_ID="13fe"
|
||||
|
||||
info() { printf '\033[0;32m[+]\033[0m %s\n' "$*" >&2; }
|
||||
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*" >&2; }
|
||||
err() { printf '\033[0;31m[E]\033[0m %s\n' "$*" >&2; }
|
||||
die() { err "$@"; exit 1; }
|
||||
|
||||
write_param_patch() {
|
||||
cat > "$1" << 'ENDPARAM'
|
||||
|
||||
/* BC-250 40 CU unlock: clears harvest mask + enables SPI dispatch to all WGPs */
|
||||
static int bc250_cc_write_mode;
|
||||
module_param(bc250_cc_write_mode, int, 0444);
|
||||
MODULE_PARM_DESC(bc250_cc_write_mode,
|
||||
"BC-250: 0=off 1=probe-SE0SH0 2=clear-SE0SH0 3=clear-all-SAs 4=probe-all-SAs");
|
||||
#define BC250_PCI_DEVICE_ID 0x13FE
|
||||
|
||||
ENDPARAM
|
||||
}
|
||||
|
||||
write_cc_patch() {
|
||||
cat > "$1" << 'ENDCC'
|
||||
|
||||
/* BC-250: unlock harvested CUs — CC (enumeration) + SPI (dispatch) + RLC (power) */
|
||||
if (bc250_cc_write_mode > 0 && adev->pdev->device == BC250_PCI_DEVICE_ID) {
|
||||
int bc_se, bc_sh;
|
||||
for (bc_se = 0; bc_se < adev->gfx.config.max_shader_engines; bc_se++) {
|
||||
for (bc_sh = 0; bc_sh < adev->gfx.config.max_sh_per_se; bc_sh++) {
|
||||
u32 bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after;
|
||||
if (bc250_cc_write_mode == 2 && (bc_se > 0 || bc_sh > 0))
|
||||
continue;
|
||||
gfx_v10_0_select_se_sh(adev, bc_se, bc_sh, 0xffffffff, 0);
|
||||
bc_cc_orig = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, 0);
|
||||
bc_cc_after = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
bc_spi_orig = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, 0x1f);
|
||||
bc_spi_after = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
WREG32_SOC15(GC, 0, mmRLC_PG_ALWAYS_ON_WGP_MASK, 0x1f);
|
||||
if (bc250_cc_write_mode == 1 || bc250_cc_write_mode == 4) {
|
||||
WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, bc_cc_orig);
|
||||
WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, bc_spi_orig);
|
||||
dev_info(adev->dev,
|
||||
"bc250-40cu-probe: se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x (restored)",
|
||||
bc_se, bc_sh, bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
} else {
|
||||
dev_info(adev->dev,
|
||||
"bc250-40cu-enable: mode=%d se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x",
|
||||
bc250_cc_write_mode, bc_se, bc_sh,
|
||||
bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_v10_0_select_se_sh(adev, 0xffffffff, 0xffffffff, 0xffffffff, 0);
|
||||
}
|
||||
|
||||
ENDCC
|
||||
}
|
||||
|
||||
check_bc250() {
|
||||
if ! lspci -nn 2>/dev/null | grep -qi "${BC250_PCI_ID}"; then
|
||||
warn "No BC-250 (PCI ID 13fe) detected. This patch is BC-250 specific."
|
||||
printf "Continue anyway? [y/N] "
|
||||
read -r ans
|
||||
case "$ans" in y|Y) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
local missing=""
|
||||
command -v gcc >/dev/null 2>&1 || missing="${missing} gcc"
|
||||
command -v make >/dev/null 2>&1 || missing="${missing} make"
|
||||
command -v zstd >/dev/null 2>&1 || missing="${missing} zstd"
|
||||
command -v curl >/dev/null 2>&1 || missing="${missing} curl"
|
||||
if [ ! -d "${MODDIR}/build" ]; then
|
||||
missing="${missing} linux-headers"
|
||||
fi
|
||||
if [ -n "$missing" ]; then
|
||||
err "Missing dependencies:${missing}"
|
||||
err "Install with: pacman -S base-devel zstd curl linux-headers"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
find_source() {
|
||||
local d
|
||||
# Check common source locations (including Arch /usr/src/linux)
|
||||
for d in \
|
||||
"/usr/src/linux-${KVER}" \
|
||||
"/usr/src/linux-${KVER_BASE}" \
|
||||
"/usr/src/linux" \
|
||||
"/usr/src/linux-source-${KVER_BASE}"; do
|
||||
if [ -f "$d/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for a pre-extracted tarball left from a previous build
|
||||
if [ -f "${BUILDDIR}/src/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="${BUILDDIR}/src"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Download minimal amdgpu subtree directly from kernel.org
|
||||
local major="${KVER_BASE%%.*}"
|
||||
local url="https://cdn.kernel.org/pub/linux/kernel/v${major}.x/linux-${KVER_BASE}.tar.xz"
|
||||
info "Kernel source not found locally."
|
||||
info "Downloading amdgpu source from kernel.org (~120 MB)..."
|
||||
info " ${url}"
|
||||
|
||||
mkdir -p "${BUILDDIR}/src"
|
||||
if curl -fL --progress-bar "$url" | \
|
||||
tar xJ -C "${BUILDDIR}/src" --strip-components=1 \
|
||||
--wildcards \
|
||||
'*/drivers/gpu/drm/amd/' \
|
||||
'*/include/drm/' \
|
||||
'*/include/uapi/drm/' \
|
||||
2>/dev/null; then
|
||||
if [ -f "${BUILDDIR}/src/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="${BUILDDIR}/src"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
die "Cannot find kernel source for ${KVER_BASE}.
|
||||
Option 1: pacman -S asp && asp checkout linux (then makepkg -o)
|
||||
Option 2: place extracted source at /usr/src/linux-${KVER_BASE}"
|
||||
}
|
||||
|
||||
patch_source() {
|
||||
local gfx="${MODSRC}/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c"
|
||||
[ -f "$gfx" ] || die "gfx_v10_0.c not found at ${gfx}"
|
||||
|
||||
if grep -q 'bc250_cc_write_mode' "$gfx"; then
|
||||
info "Source already patched."
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Patching gfx_v10_0.c..."
|
||||
cp "$gfx" "${gfx}.orig"
|
||||
|
||||
# Step 1: insert module parameter before '#include "amdgpu.h"'
|
||||
if ! grep -q '#include "amdgpu.h"' "$gfx"; then
|
||||
die "Cannot find anchor: #include amdgpu.h"
|
||||
fi
|
||||
|
||||
local param_file
|
||||
param_file="$(mktemp)"
|
||||
write_param_patch "$param_file"
|
||||
sed -i "/#include \"amdgpu.h\"/r ${param_file}" "$gfx"
|
||||
rm -f "$param_file"
|
||||
|
||||
# Step 2: insert CC write block in gfx_v10_0_get_cu_info after mutex_lock
|
||||
local cc_file
|
||||
cc_file="$(mktemp)"
|
||||
write_cc_patch "$cc_file"
|
||||
|
||||
# Two-phase awk: avoid matching the forward declaration of gfx_v10_0_get_cu_info.
|
||||
# A forward declaration ends with "); " on a later line, while the actual body
|
||||
# starts with a standalone "{" on its own line right after the signature.
|
||||
awk -v insertfile="$cc_file" '
|
||||
/static.*gfx_v10_0_get_cu_info/ { maybe_func = 1 }
|
||||
maybe_func && /;/ { maybe_func = 0 }
|
||||
maybe_func && /^\{/ { in_cu_info = 1; maybe_func = 0 }
|
||||
in_cu_info && /mutex_lock/ && !inserted {
|
||||
print
|
||||
while ((getline line < insertfile) > 0) print line
|
||||
close(insertfile)
|
||||
inserted = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$gfx" > "${gfx}.new"
|
||||
|
||||
if grep -q 'bc250-40cu-enable' "${gfx}.new"; then
|
||||
mv "${gfx}.new" "$gfx"
|
||||
rm -f "$cc_file"
|
||||
info "Patch applied successfully."
|
||||
else
|
||||
rm -f "${gfx}.new" "$cc_file"
|
||||
mv "${gfx}.orig" "$gfx"
|
||||
die "Failed to insert CC write block. Kernel source layout may differ."
|
||||
fi
|
||||
}
|
||||
|
||||
build_module() {
|
||||
local amdgpu_dir="${MODSRC}/drivers/gpu/drm/amd/amdgpu"
|
||||
[ -d "$amdgpu_dir" ] || die "amdgpu source directory not found"
|
||||
|
||||
# define_trace.h (in the kernel headers) resolves the trace header as:
|
||||
# ../../drivers/gpu/drm/amd/amdgpu/amdgpu_trace.h
|
||||
# relative to its own location, ending up at:
|
||||
# ${kbuild}/drivers/gpu/drm/amd/amdgpu/amdgpu_trace.h
|
||||
# That directory already exists in linux-headers (contains only Kconfig),
|
||||
# so we copy the trace header there temporarily for the build.
|
||||
local kbuild="${MODDIR}/build"
|
||||
local kbuild_amdgpu="${kbuild}/drivers/gpu/drm/amd/amdgpu"
|
||||
local trace_dst="${kbuild_amdgpu}/amdgpu_trace.h"
|
||||
local trace_copied=0
|
||||
if [ ! -f "$trace_dst" ]; then
|
||||
mkdir -p "$kbuild_amdgpu"
|
||||
cp "${amdgpu_dir}/amdgpu_trace.h" "$trace_dst"
|
||||
trace_copied=1
|
||||
fi
|
||||
|
||||
info "Building amdgpu module for kernel ${KVER} (2-5 min)..."
|
||||
make -C "$kbuild" M="$amdgpu_dir" -j"$(nproc)" modules 2>&1 | tail -10 >&2
|
||||
local make_rc=${PIPESTATUS[0]}
|
||||
|
||||
[ "$trace_copied" -eq 1 ] && rm -f "$trace_dst"
|
||||
|
||||
[ "$make_rc" -eq 0 ] || die "Build failed (make exited $make_rc)"
|
||||
|
||||
local built="${amdgpu_dir}/amdgpu.ko"
|
||||
[ -f "$built" ] || die "Build failed - amdgpu.ko not produced"
|
||||
|
||||
# Use grep -qa to avoid SIGPIPE/pipefail issue with `strings | grep -q` on large .ko files
|
||||
if ! grep -qa 'bc250_cc_write_mode' "$built"; then
|
||||
die "Built module missing bc250_cc_write_mode - patch failed"
|
||||
fi
|
||||
|
||||
info "Build successful: ${built} ($(du -h "$built" | cut -f1))"
|
||||
echo "$built"
|
||||
}
|
||||
|
||||
install_module() {
|
||||
local built="$1"
|
||||
local target="${MODPATH}"
|
||||
|
||||
if [ -f "${target}.zst" ]; then
|
||||
target="${target}.zst"
|
||||
elif [ ! -f "$target" ]; then
|
||||
target="${target}.zst"
|
||||
fi
|
||||
|
||||
if [ -f "$target" ] && [ ! -f "${target}${BACKUP_SUFFIX}" ]; then
|
||||
info "Backing up original to ${target}${BACKUP_SUFFIX}"
|
||||
cp "$target" "${target}${BACKUP_SUFFIX}"
|
||||
fi
|
||||
|
||||
if [ "${target%.zst}" != "$target" ]; then
|
||||
info "Compressing and installing module..."
|
||||
zstd -f "$built" -o "$target"
|
||||
else
|
||||
cp "$built" "$target"
|
||||
fi
|
||||
|
||||
depmod -a "$KVER"
|
||||
info "Module installed at ${target}"
|
||||
}
|
||||
|
||||
do_build() {
|
||||
check_bc250
|
||||
check_deps
|
||||
find_source
|
||||
patch_source
|
||||
local built
|
||||
built="$(build_module)"
|
||||
install_module "$built"
|
||||
echo ""
|
||||
info "Done! Patched amdgpu module installed."
|
||||
info "Next: sudo $0 enable"
|
||||
}
|
||||
|
||||
do_enable() {
|
||||
printf '# BC-250 40 CU re-enablement\noptions amdgpu bc250_cc_write_mode=3\n' > "$CONF40"
|
||||
info "40 CU mode configured in ${CONF40}"
|
||||
if ! ( set +o pipefail; modinfo amdgpu 2>/dev/null | grep -q 'bc250_cc_write_mode' ); then
|
||||
warn "Patched module not detected. Run: sudo $0 build"
|
||||
rm -f "$CONF40"
|
||||
exit 1
|
||||
fi
|
||||
info "Rebooting..."
|
||||
sleep 2
|
||||
reboot
|
||||
}
|
||||
|
||||
do_disable() {
|
||||
rm -f "$CONF40"
|
||||
info "40 CU config removed. Rebooting to stock 24 CU..."
|
||||
sleep 2
|
||||
reboot
|
||||
}
|
||||
|
||||
do_restore() {
|
||||
local target="${MODPATH}"
|
||||
if [ -f "${target}.zst" ]; then target="${target}.zst"; fi
|
||||
local backup
|
||||
backup="$(ls -1 "${target}.bc250-backup-"* 2>/dev/null | head -1)"
|
||||
[ -n "$backup" ] || die "No backup found"
|
||||
cp "$backup" "$target"
|
||||
rm -f "$CONF40"
|
||||
depmod -a "$KVER"
|
||||
info "Original module restored. Reboot to apply."
|
||||
}
|
||||
|
||||
do_status() {
|
||||
printf '\033[1m=== BC-250 CU Status ===\033[0m\n\n'
|
||||
|
||||
if lspci -nn 2>/dev/null | grep -qi "${BC250_PCI_ID}"; then
|
||||
printf ' PCI device: \033[0;32mBC-250 detected\033[0m\n'
|
||||
else
|
||||
printf ' PCI device: \033[0;31mBC-250 not found\033[0m\n'
|
||||
fi
|
||||
|
||||
if ( set +o pipefail; modinfo amdgpu 2>/dev/null | grep -q 'bc250_cc_write_mode' ); then
|
||||
printf ' amdgpu module: \033[0;32mpatched\033[0m\n'
|
||||
else
|
||||
printf ' amdgpu module: \033[0;33mstock (unpatched)\033[0m\n'
|
||||
fi
|
||||
|
||||
local mode
|
||||
mode="$(cat /sys/module/amdgpu/parameters/bc250_cc_write_mode 2>/dev/null || echo 'N/A')"
|
||||
printf ' write_mode: %s\n' "$mode"
|
||||
|
||||
local cu_line
|
||||
cu_line="$(dmesg 2>/dev/null | grep 'active_cu_number' | tail -1)"
|
||||
if [ -n "$cu_line" ]; then
|
||||
local cus
|
||||
cus="$(echo "$cu_line" | grep -o 'active_cu_number [0-9]*' | awk '{print $2}')"
|
||||
if [ "$cus" = "40" ]; then
|
||||
printf ' active CUs: \033[0;32m\033[1m40\033[0m (full die)\n'
|
||||
elif [ "$cus" = "24" ]; then
|
||||
printf ' active CUs: \033[0;33m24\033[0m (stock)\n'
|
||||
else
|
||||
printf ' active CUs: %s\n' "$cus"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$CONF40" ]; then
|
||||
printf ' modprobe conf: \033[0;32m%s (40 CU enabled)\033[0m\n' "$CONF40"
|
||||
else
|
||||
printf ' modprobe conf: (none - stock mode)\n'
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
build) do_build ;;
|
||||
enable) do_enable ;;
|
||||
disable) do_disable ;;
|
||||
restore) do_restore ;;
|
||||
status) do_status ;;
|
||||
*)
|
||||
echo "BC-250 40 CU Re-enablement Tool (Arch Linux)"
|
||||
echo ""
|
||||
echo "Usage: sudo $0 <command>"
|
||||
echo ""
|
||||
echo " build Patch, compile, install patched amdgpu (~5 min)"
|
||||
echo " enable Activate 40 CU mode and reboot"
|
||||
echo " disable Return to stock 24 CU and reboot"
|
||||
echo " status Show current CU state"
|
||||
echo " restore Restore original amdgpu module"
|
||||
echo ""
|
||||
echo "Quick start:"
|
||||
echo " sudo $0 build && sudo $0 enable"
|
||||
echo ""
|
||||
echo "Dependencies: pacman -S base-devel zstd curl linux-headers"
|
||||
;;
|
||||
esac
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-enable-40cu-fedora.sh — Build and install a patched amdgpu for 40 CU on BC-250
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./bc250-enable-40cu-fedora.sh build # patch + compile + install
|
||||
# sudo ./bc250-enable-40cu-fedora.sh enable # set 40 CU mode and reboot
|
||||
# sudo ./bc250-enable-40cu-fedora.sh disable # return to stock 24 CU and reboot
|
||||
# sudo ./bc250-enable-40cu-fedora.sh status # show current CU state
|
||||
# sudo ./bc250-enable-40cu-fedora.sh restore # restore original amdgpu module
|
||||
#
|
||||
# Requirements: kernel-devel, gcc, make, zstd, curl. Must run as root on BC-250.
|
||||
# Tested on: Fedora 43 with kernel 7.0.9-105.fc43.x86_64
|
||||
#
|
||||
# Authors: duggasco, Claude, Martin | License: GPL-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KVER="$(uname -r)"
|
||||
KVER_BASE="${KVER%%-*}" # e.g. 6.9.3 from 6.9.3-arch1-1
|
||||
MODDIR="/usr/lib/modules/${KVER}"
|
||||
MODPATH="${MODDIR}/kernel/drivers/gpu/drm/amd/amdgpu/amdgpu.ko"
|
||||
MODSRC=""
|
||||
BUILDDIR="/tmp/bc250-40cu-build"
|
||||
CONF40="/etc/modprobe.d/bc250-40cu.conf"
|
||||
BACKUP_SUFFIX=".bc250-backup-$(date +%Y%m%d)"
|
||||
BC250_PCI_ID="13fe"
|
||||
|
||||
info() { printf '\033[0;32m[+]\033[0m %s\n' "$*" >&2; }
|
||||
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*" >&2; }
|
||||
err() { printf '\033[0;31m[E]\033[0m %s\n' "$*" >&2; }
|
||||
die() { err "$@"; exit 1; }
|
||||
|
||||
write_param_patch() {
|
||||
cat > "$1" << 'ENDPARAM'
|
||||
|
||||
/* BC-250 40 CU unlock: clears harvest mask + enables SPI dispatch to all WGPs */
|
||||
static int bc250_cc_write_mode;
|
||||
module_param(bc250_cc_write_mode, int, 0444);
|
||||
MODULE_PARM_DESC(bc250_cc_write_mode,
|
||||
"BC-250: 0=off 1=probe-SE0SH0 2=clear-SE0SH0 3=clear-all-SAs 4=probe-all-SAs");
|
||||
#define BC250_PCI_DEVICE_ID 0x13FE
|
||||
|
||||
ENDPARAM
|
||||
}
|
||||
|
||||
write_cc_patch() {
|
||||
cat > "$1" << 'ENDCC'
|
||||
|
||||
/* BC-250: unlock harvested CUs — CC (enumeration) + SPI (dispatch) + RLC (power) */
|
||||
if (bc250_cc_write_mode > 0 && adev->pdev->device == BC250_PCI_DEVICE_ID) {
|
||||
int bc_se, bc_sh;
|
||||
for (bc_se = 0; bc_se < adev->gfx.config.max_shader_engines; bc_se++) {
|
||||
for (bc_sh = 0; bc_sh < adev->gfx.config.max_sh_per_se; bc_sh++) {
|
||||
u32 bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after;
|
||||
if (bc250_cc_write_mode == 2 && (bc_se > 0 || bc_sh > 0))
|
||||
continue;
|
||||
gfx_v10_0_select_se_sh(adev, bc_se, bc_sh, 0xffffffff, 0);
|
||||
bc_cc_orig = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, 0);
|
||||
bc_cc_after = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
bc_spi_orig = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, 0x1f);
|
||||
bc_spi_after = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
WREG32_SOC15(GC, 0, mmRLC_PG_ALWAYS_ON_WGP_MASK, 0x1f);
|
||||
if (bc250_cc_write_mode == 1 || bc250_cc_write_mode == 4) {
|
||||
WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, bc_cc_orig);
|
||||
WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, bc_spi_orig);
|
||||
dev_info(adev->dev,
|
||||
"bc250-40cu-probe: se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x (restored)",
|
||||
bc_se, bc_sh, bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
} else {
|
||||
dev_info(adev->dev,
|
||||
"bc250-40cu-enable: mode=%d se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x",
|
||||
bc250_cc_write_mode, bc_se, bc_sh,
|
||||
bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_v10_0_select_se_sh(adev, 0xffffffff, 0xffffffff, 0xffffffff, 0);
|
||||
}
|
||||
|
||||
ENDCC
|
||||
}
|
||||
|
||||
check_bc250() {
|
||||
if ! lspci -nn 2>/dev/null | grep -qi "${BC250_PCI_ID}"; then
|
||||
warn "No BC-250 (PCI ID 13fe) detected. This patch is BC-250 specific."
|
||||
printf "Continue anyway? [y/N] "
|
||||
read -r ans
|
||||
case "$ans" in y|Y) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
local missing=""
|
||||
command -v gcc >/dev/null 2>&1 || missing="${missing} gcc"
|
||||
command -v make >/dev/null 2>&1 || missing="${missing} make"
|
||||
command -v zstd >/dev/null 2>&1 || missing="${missing} zstd"
|
||||
command -v curl >/dev/null 2>&1 || missing="${missing} curl"
|
||||
if [ ! -d "${MODDIR}/build" ]; then
|
||||
missing="${missing} linux-headers"
|
||||
fi
|
||||
if [ -n "$missing" ]; then
|
||||
err "Missing dependencies:${missing}"
|
||||
err "Install with: dnf install kernel-devel-$(uname -r) gcc make zstd curl"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
find_source() {
|
||||
local d
|
||||
# Check common source locations
|
||||
for d in \
|
||||
"/usr/src/linux-${KVER}" \
|
||||
"/usr/src/linux-${KVER_BASE}" \
|
||||
"/usr/src/linux" \
|
||||
"/usr/src/kernels/${KVER}-full"; do
|
||||
if [ -f "$d/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for a pre-extracted tarball left from a previous build
|
||||
if [ -f "${BUILDDIR}/src/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="${BUILDDIR}/src"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Download minimal amdgpu subtree directly from kernel.org
|
||||
local major="${KVER_BASE%%.*}"
|
||||
local url="https://cdn.kernel.org/pub/linux/kernel/v${major}.x/linux-${KVER_BASE}.tar.xz"
|
||||
info "Kernel source not found locally."
|
||||
info "Downloading amdgpu source from kernel.org (~120 MB)..."
|
||||
info " ${url}"
|
||||
|
||||
mkdir -p "${BUILDDIR}/src"
|
||||
if curl -fL --progress-bar "$url" | \
|
||||
tar xJ -C "${BUILDDIR}/src" --strip-components=1 \
|
||||
--wildcards \
|
||||
'*/drivers/gpu/drm/amd/' \
|
||||
'*/include/drm/' \
|
||||
'*/include/uapi/drm/' \
|
||||
2>/dev/null; then
|
||||
if [ -f "${BUILDDIR}/src/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="${BUILDDIR}/src"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
die "Cannot find kernel source for ${KVER_BASE}.
|
||||
Option 1: dnf download --source kernel && rpm2cpio kernel-*.src.rpm | cpio -idm
|
||||
then unpack linux-${KVER_BASE}.tar.xz into /usr/src/
|
||||
Option 2: place extracted source at /usr/src/linux-${KVER_BASE}"
|
||||
}
|
||||
|
||||
patch_source() {
|
||||
local gfx="${MODSRC}/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c"
|
||||
[ -f "$gfx" ] || die "gfx_v10_0.c not found at ${gfx}"
|
||||
|
||||
if grep -q 'bc250_cc_write_mode' "$gfx"; then
|
||||
info "Source already patched."
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Patching gfx_v10_0.c..."
|
||||
cp "$gfx" "${gfx}.orig"
|
||||
|
||||
# Step 1: insert module parameter before '#include "amdgpu.h"'
|
||||
if ! grep -q '#include "amdgpu.h"' "$gfx"; then
|
||||
die "Cannot find anchor: #include amdgpu.h"
|
||||
fi
|
||||
|
||||
local param_file
|
||||
param_file="$(mktemp)"
|
||||
write_param_patch "$param_file"
|
||||
sed -i "/#include \"amdgpu.h\"/r ${param_file}" "$gfx"
|
||||
rm -f "$param_file"
|
||||
|
||||
# Step 2: insert CC write block in gfx_v10_0_get_cu_info after mutex_lock
|
||||
local cc_file
|
||||
cc_file="$(mktemp)"
|
||||
write_cc_patch "$cc_file"
|
||||
|
||||
# Two-phase awk: avoid matching the forward declaration of gfx_v10_0_get_cu_info.
|
||||
# A forward declaration ends with "); " on a later line, while the actual body
|
||||
# starts with a standalone "{" on its own line right after the signature.
|
||||
awk -v insertfile="$cc_file" '
|
||||
/static.*gfx_v10_0_get_cu_info/ { maybe_func = 1 }
|
||||
maybe_func && /;/ { maybe_func = 0 }
|
||||
maybe_func && /^\{/ { in_cu_info = 1; maybe_func = 0 }
|
||||
in_cu_info && /mutex_lock/ && !inserted {
|
||||
print
|
||||
while ((getline line < insertfile) > 0) print line
|
||||
close(insertfile)
|
||||
inserted = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$gfx" > "${gfx}.new"
|
||||
|
||||
if grep -q 'bc250-40cu-enable' "${gfx}.new"; then
|
||||
mv "${gfx}.new" "$gfx"
|
||||
rm -f "$cc_file"
|
||||
info "Patch applied successfully."
|
||||
else
|
||||
rm -f "${gfx}.new" "$cc_file"
|
||||
mv "${gfx}.orig" "$gfx"
|
||||
die "Failed to insert CC write block. Kernel source layout may differ."
|
||||
fi
|
||||
}
|
||||
|
||||
build_module() {
|
||||
local amdgpu_dir="${MODSRC}/drivers/gpu/drm/amd/amdgpu"
|
||||
[ -d "$amdgpu_dir" ] || die "amdgpu source directory not found"
|
||||
|
||||
# define_trace.h (in the kernel headers) resolves the trace header as:
|
||||
# ../../drivers/gpu/drm/amd/amdgpu/amdgpu_trace.h
|
||||
# relative to its own location, ending up at:
|
||||
# ${kbuild}/drivers/gpu/drm/amd/amdgpu/amdgpu_trace.h
|
||||
# That directory already exists in linux-headers (contains only Kconfig),
|
||||
# so we copy the trace header there temporarily for the build.
|
||||
local kbuild="${MODDIR}/build"
|
||||
local kbuild_amdgpu="${kbuild}/drivers/gpu/drm/amd/amdgpu"
|
||||
local trace_dst="${kbuild_amdgpu}/amdgpu_trace.h"
|
||||
local trace_copied=0
|
||||
if [ ! -f "$trace_dst" ]; then
|
||||
mkdir -p "$kbuild_amdgpu"
|
||||
cp "${amdgpu_dir}/amdgpu_trace.h" "$trace_dst"
|
||||
trace_copied=1
|
||||
fi
|
||||
|
||||
info "Building amdgpu module for kernel ${KVER} (2-5 min)..."
|
||||
make -C "$kbuild" M="$amdgpu_dir" -j"$(nproc)" modules 2>&1 | tail -10 >&2
|
||||
local make_rc=${PIPESTATUS[0]}
|
||||
|
||||
[ "$trace_copied" -eq 1 ] && rm -f "$trace_dst"
|
||||
|
||||
[ "$make_rc" -eq 0 ] || die "Build failed (make exited $make_rc)"
|
||||
|
||||
local built="${amdgpu_dir}/amdgpu.ko"
|
||||
[ -f "$built" ] || die "Build failed - amdgpu.ko not produced"
|
||||
|
||||
# Use grep -qa to avoid SIGPIPE/pipefail issue with `strings | grep -q` on large .ko files
|
||||
if ! grep -qa 'bc250_cc_write_mode' "$built"; then
|
||||
die "Built module missing bc250_cc_write_mode - patch failed"
|
||||
fi
|
||||
|
||||
info "Build successful: ${built} ($(du -h "$built" | cut -f1))"
|
||||
echo "$built"
|
||||
}
|
||||
|
||||
install_module() {
|
||||
local built="$1"
|
||||
local target="${MODPATH}"
|
||||
|
||||
if [ -f "${target}.zst" ]; then
|
||||
target="${target}.zst"
|
||||
elif [ ! -f "$target" ]; then
|
||||
target="${target}.zst"
|
||||
fi
|
||||
|
||||
if [ -f "$target" ] && [ ! -f "${target}${BACKUP_SUFFIX}" ]; then
|
||||
info "Backing up original to ${target}${BACKUP_SUFFIX}"
|
||||
cp "$target" "${target}${BACKUP_SUFFIX}"
|
||||
fi
|
||||
|
||||
if [ "${target%.zst}" != "$target" ]; then
|
||||
info "Compressing and installing module..."
|
||||
zstd -f "$built" -o "$target"
|
||||
else
|
||||
cp "$built" "$target"
|
||||
fi
|
||||
|
||||
depmod -a "$KVER"
|
||||
info "Module installed at ${target}"
|
||||
}
|
||||
|
||||
do_build() {
|
||||
check_bc250
|
||||
check_deps
|
||||
find_source
|
||||
patch_source
|
||||
local built
|
||||
built="$(build_module)"
|
||||
install_module "$built"
|
||||
echo ""
|
||||
info "Done! Patched amdgpu module installed."
|
||||
info "Next: sudo $0 enable"
|
||||
}
|
||||
|
||||
do_enable() {
|
||||
printf '# BC-250 40 CU re-enablement\noptions amdgpu bc250_cc_write_mode=3\n' > "$CONF40"
|
||||
info "40 CU mode configured in ${CONF40}"
|
||||
if ! ( set +o pipefail; modinfo amdgpu 2>/dev/null | grep -q 'bc250_cc_write_mode' ); then
|
||||
warn "Patched module not detected. Run: sudo $0 build"
|
||||
rm -f "$CONF40"
|
||||
exit 1
|
||||
fi
|
||||
info "Rebooting..."
|
||||
sleep 2
|
||||
reboot
|
||||
}
|
||||
|
||||
do_disable() {
|
||||
rm -f "$CONF40"
|
||||
info "40 CU config removed. Rebooting to stock 24 CU..."
|
||||
sleep 2
|
||||
reboot
|
||||
}
|
||||
|
||||
do_restore() {
|
||||
local target="${MODPATH}"
|
||||
if [ -f "${target}.zst" ]; then target="${target}.zst"; fi
|
||||
local backup
|
||||
backup="$(ls -1 "${target}.bc250-backup-"* 2>/dev/null | head -1)"
|
||||
[ -n "$backup" ] || die "No backup found"
|
||||
cp "$backup" "$target"
|
||||
rm -f "$CONF40"
|
||||
depmod -a "$KVER"
|
||||
info "Original module restored. Reboot to apply."
|
||||
}
|
||||
|
||||
do_status() {
|
||||
printf '\033[1m=== BC-250 CU Status ===\033[0m\n\n'
|
||||
|
||||
if lspci -nn 2>/dev/null | grep -qi "${BC250_PCI_ID}"; then
|
||||
printf ' PCI device: \033[0;32mBC-250 detected\033[0m\n'
|
||||
else
|
||||
printf ' PCI device: \033[0;31mBC-250 not found\033[0m\n'
|
||||
fi
|
||||
|
||||
if ( set +o pipefail; modinfo amdgpu 2>/dev/null | grep -q 'bc250_cc_write_mode' ); then
|
||||
printf ' amdgpu module: \033[0;32mpatched\033[0m\n'
|
||||
else
|
||||
printf ' amdgpu module: \033[0;33mstock (unpatched)\033[0m\n'
|
||||
fi
|
||||
|
||||
local mode
|
||||
mode="$(cat /sys/module/amdgpu/parameters/bc250_cc_write_mode 2>/dev/null || echo 'N/A')"
|
||||
printf ' write_mode: %s\n' "$mode"
|
||||
|
||||
local cu_line
|
||||
cu_line="$(dmesg 2>/dev/null | grep 'active_cu_number' | tail -1)"
|
||||
if [ -n "$cu_line" ]; then
|
||||
local cus
|
||||
cus="$(echo "$cu_line" | grep -o 'active_cu_number [0-9]*' | awk '{print $2}')"
|
||||
if [ "$cus" = "40" ]; then
|
||||
printf ' active CUs: \033[0;32m\033[1m40\033[0m (full die)\n'
|
||||
elif [ "$cus" = "24" ]; then
|
||||
printf ' active CUs: \033[0;33m24\033[0m (stock)\n'
|
||||
else
|
||||
printf ' active CUs: %s\n' "$cus"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$CONF40" ]; then
|
||||
printf ' modprobe conf: \033[0;32m%s (40 CU enabled)\033[0m\n' "$CONF40"
|
||||
else
|
||||
printf ' modprobe conf: (none - stock mode)\n'
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
build) do_build ;;
|
||||
enable) do_enable ;;
|
||||
disable) do_disable ;;
|
||||
restore) do_restore ;;
|
||||
status) do_status ;;
|
||||
*)
|
||||
echo "BC-250 40 CU Re-enablement Tool (Fedora)"
|
||||
echo ""
|
||||
echo "Usage: sudo $0 <command>"
|
||||
echo ""
|
||||
echo " build Patch, compile, install patched amdgpu (~5 min)"
|
||||
echo " enable Activate 40 CU mode and reboot"
|
||||
echo " disable Return to stock 24 CU and reboot"
|
||||
echo " status Show current CU state"
|
||||
echo " restore Restore original amdgpu module"
|
||||
echo ""
|
||||
echo "Quick start:"
|
||||
echo " sudo $0 build && sudo $0 enable"
|
||||
echo ""
|
||||
echo "Dependencies: dnf install kernel-devel-$(uname -r) gcc make zstd curl"
|
||||
;;
|
||||
esac
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env bash
|
||||
# bc250-enable-40cu.sh — Build and install a patched amdgpu for 40 CU on BC-250
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./bc250-enable-40cu.sh build # patch + compile + install
|
||||
# sudo ./bc250-enable-40cu.sh enable # set 40 CU mode and reboot
|
||||
# sudo ./bc250-enable-40cu.sh disable # return to stock 24 CU and reboot
|
||||
# sudo ./bc250-enable-40cu.sh status # show current CU state
|
||||
# sudo ./bc250-enable-40cu.sh restore # restore original amdgpu module
|
||||
#
|
||||
# Requirements: kernel headers, gcc, make, zstd. Must run as root on BC-250.
|
||||
# Tested on: Debian Forky kernel 6.19.14+deb14-amd64
|
||||
#
|
||||
# Authors: duggasco, Claude | License: GPL-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KVER="$(uname -r)"
|
||||
MODDIR="/lib/modules/${KVER}"
|
||||
MODPATH="${MODDIR}/kernel/drivers/gpu/drm/amd/amdgpu/amdgpu.ko"
|
||||
MODSRC=""
|
||||
BUILDDIR="/tmp/bc250-40cu-build"
|
||||
CONF40="/etc/modprobe.d/bc250-40cu.conf"
|
||||
BACKUP_SUFFIX=".bc250-backup-$(date +%Y%m%d)"
|
||||
BC250_PCI_ID="13fe"
|
||||
|
||||
info() { printf '\033[0;32m[+]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[0;33m[!]\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[0;31m[E]\033[0m %s\n' "$*" >&2; }
|
||||
die() { err "$@"; exit 1; }
|
||||
|
||||
write_param_patch() {
|
||||
cat > "$1" << 'ENDPARAM'
|
||||
|
||||
/* BC-250 40 CU unlock: clears harvest mask + enables SPI dispatch to all WGPs */
|
||||
static int bc250_cc_write_mode;
|
||||
module_param(bc250_cc_write_mode, int, 0444);
|
||||
MODULE_PARM_DESC(bc250_cc_write_mode,
|
||||
"BC-250: 0=off 1=probe-SE0SH0 2=clear-SE0SH0 3=clear-all-SAs 4=probe-all-SAs");
|
||||
#define BC250_PCI_DEVICE_ID 0x13FE
|
||||
|
||||
ENDPARAM
|
||||
}
|
||||
|
||||
write_cc_patch() {
|
||||
cat > "$1" << 'ENDCC'
|
||||
|
||||
/* BC-250: unlock harvested CUs — CC (enumeration) + SPI (dispatch) + RLC (power) */
|
||||
if (bc250_cc_write_mode > 0 && adev->pdev->device == BC250_PCI_DEVICE_ID) {
|
||||
int bc_se, bc_sh;
|
||||
for (bc_se = 0; bc_se < adev->gfx.config.max_shader_engines; bc_se++) {
|
||||
for (bc_sh = 0; bc_sh < adev->gfx.config.max_sh_per_se; bc_sh++) {
|
||||
u32 bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after;
|
||||
if (bc250_cc_write_mode == 2 && (bc_se > 0 || bc_sh > 0))
|
||||
continue;
|
||||
gfx_v10_0_select_se_sh(adev, bc_se, bc_sh, 0xffffffff, 0);
|
||||
bc_cc_orig = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, 0);
|
||||
bc_cc_after = RREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG);
|
||||
bc_spi_orig = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, 0x1f);
|
||||
bc_spi_after = RREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK);
|
||||
WREG32_SOC15(GC, 0, mmRLC_PG_ALWAYS_ON_WGP_MASK, 0x1f);
|
||||
if (bc250_cc_write_mode == 1 || bc250_cc_write_mode == 4) {
|
||||
WREG32_SOC15(GC, 0, mmCC_GC_SHADER_ARRAY_CONFIG, bc_cc_orig);
|
||||
WREG32_SOC15(GC, 0, mmSPI_PG_ENABLE_STATIC_WGP_MASK, bc_spi_orig);
|
||||
dev_info(adev->dev,
|
||||
"bc250-40cu-probe: se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x (restored)",
|
||||
bc_se, bc_sh, bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
} else {
|
||||
dev_info(adev->dev,
|
||||
"bc250-40cu-enable: mode=%d se=%d sh=%d CC=0x%08x->0x%08x SPI=0x%08x->0x%08x",
|
||||
bc250_cc_write_mode, bc_se, bc_sh,
|
||||
bc_cc_orig, bc_cc_after, bc_spi_orig, bc_spi_after);
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx_v10_0_select_se_sh(adev, 0xffffffff, 0xffffffff, 0xffffffff, 0);
|
||||
}
|
||||
|
||||
ENDCC
|
||||
}
|
||||
|
||||
check_bc250() {
|
||||
if ! lspci -nn 2>/dev/null | grep -qi "${BC250_PCI_ID}"; then
|
||||
warn "No BC-250 (PCI ID 13fe) detected. This patch is BC-250 specific."
|
||||
printf "Continue anyway? [y/N] "
|
||||
read -r ans
|
||||
case "$ans" in y|Y) ;; *) exit 1 ;; esac
|
||||
fi
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
local missing=""
|
||||
command -v gcc >/dev/null 2>&1 || missing="${missing} gcc"
|
||||
command -v make >/dev/null 2>&1 || missing="${missing} make"
|
||||
command -v zstd >/dev/null 2>&1 || missing="${missing} zstd"
|
||||
if [ ! -d "${MODDIR}/build" ]; then
|
||||
missing="${missing} linux-headers-${KVER}"
|
||||
fi
|
||||
if [ -n "$missing" ]; then
|
||||
die "Missing dependencies:${missing}"
|
||||
fi
|
||||
}
|
||||
|
||||
find_source() {
|
||||
local d
|
||||
for d in \
|
||||
"/usr/src/linux-source-${KVER%%-*}" \
|
||||
"/usr/src/linux-source-${KVER%%+*}" \
|
||||
"/usr/src/linux-${KVER}" \
|
||||
"/usr/src/linux"; do
|
||||
if [ -f "$d/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
local srcpkg=""
|
||||
local p
|
||||
for p in \
|
||||
"/usr/src/linux-source-${KVER%%-*}.tar.xz" \
|
||||
"/usr/src/linux-source-${KVER%%+*}.tar.xz"; do
|
||||
[ -f "$p" ] && srcpkg="$p" && break
|
||||
done
|
||||
if [ -z "$srcpkg" ]; then
|
||||
srcpkg="$(find /usr/src -maxdepth 4 -name 'linux-source-*.tar.xz' 2>/dev/null | head -1)"
|
||||
fi
|
||||
|
||||
if [ -n "$srcpkg" ]; then
|
||||
info "Extracting kernel source from ${srcpkg}..."
|
||||
mkdir -p "${BUILDDIR}/src"
|
||||
tar xf "$srcpkg" -C "${BUILDDIR}/src" --strip-components=1 \
|
||||
'*/drivers/gpu/drm/amd/amdgpu/' 2>/dev/null || true
|
||||
if [ -f "${BUILDDIR}/src/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="${BUILDDIR}/src"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Kernel source not found locally. Trying apt..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get install -y "linux-source-${KVER%%-*}" 2>/dev/null || true
|
||||
srcpkg="/usr/src/linux-source-${KVER%%-*}.tar.xz"
|
||||
if [ -f "$srcpkg" ]; then
|
||||
mkdir -p "${BUILDDIR}/src"
|
||||
tar xf "$srcpkg" -C "${BUILDDIR}/src" --strip-components=1 \
|
||||
'*/drivers/gpu/drm/amd/amdgpu/' 2>/dev/null || true
|
||||
if [ -f "${BUILDDIR}/src/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c" ]; then
|
||||
MODSRC="${BUILDDIR}/src"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
die "Cannot find kernel source for ${KVER}. Install: apt install linux-source-${KVER%%-*}"
|
||||
}
|
||||
|
||||
patch_source() {
|
||||
local gfx="${MODSRC}/drivers/gpu/drm/amd/amdgpu/gfx_v10_0.c"
|
||||
[ -f "$gfx" ] || die "gfx_v10_0.c not found at ${gfx}"
|
||||
|
||||
if grep -q 'bc250_cc_write_mode' "$gfx"; then
|
||||
info "Source already patched."
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Patching gfx_v10_0.c..."
|
||||
cp "$gfx" "${gfx}.orig"
|
||||
|
||||
# Step 1: insert module parameter before '#include "amdgpu.h"'
|
||||
if ! grep -q '#include "amdgpu.h"' "$gfx"; then
|
||||
die "Cannot find anchor: #include amdgpu.h"
|
||||
fi
|
||||
|
||||
local param_file
|
||||
param_file="$(mktemp)"
|
||||
write_param_patch "$param_file"
|
||||
sed -i "/#include \"amdgpu.h\"/r ${param_file}" "$gfx"
|
||||
rm -f "$param_file"
|
||||
|
||||
# Step 2: insert CC write block in gfx_v10_0_get_cu_info after mutex_lock
|
||||
local cc_file
|
||||
cc_file="$(mktemp)"
|
||||
write_cc_patch "$cc_file"
|
||||
|
||||
awk -v insertfile="$cc_file" '
|
||||
/static.*gfx_v10_0_get_cu_info/ { in_cu_info = 1 }
|
||||
in_cu_info && /mutex_lock/ && !inserted {
|
||||
print
|
||||
while ((getline line < insertfile) > 0) print line
|
||||
close(insertfile)
|
||||
inserted = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$gfx" > "${gfx}.new"
|
||||
|
||||
if grep -q 'bc250-cc-clear' "${gfx}.new"; then
|
||||
mv "${gfx}.new" "$gfx"
|
||||
rm -f "$cc_file"
|
||||
info "Patch applied successfully."
|
||||
else
|
||||
rm -f "${gfx}.new" "$cc_file"
|
||||
mv "${gfx}.orig" "$gfx"
|
||||
die "Failed to insert CC write block. Kernel source layout may differ."
|
||||
fi
|
||||
}
|
||||
|
||||
build_module() {
|
||||
local amdgpu_dir="${MODSRC}/drivers/gpu/drm/amd/amdgpu"
|
||||
[ -d "$amdgpu_dir" ] || die "amdgpu source directory not found"
|
||||
|
||||
info "Building amdgpu module for kernel ${KVER} (2-5 min)..."
|
||||
make -C "${MODDIR}/build" M="$amdgpu_dir" -j"$(nproc)" modules 2>&1 | tail -5
|
||||
|
||||
local built="${amdgpu_dir}/amdgpu.ko"
|
||||
[ -f "$built" ] || die "Build failed - amdgpu.ko not produced"
|
||||
|
||||
if ! strings "$built" | grep -q 'bc250_cc_write_mode'; then
|
||||
die "Built module missing bc250_cc_write_mode - patch failed"
|
||||
fi
|
||||
|
||||
info "Build successful: ${built} ($(du -h "$built" | cut -f1))"
|
||||
echo "$built"
|
||||
}
|
||||
|
||||
install_module() {
|
||||
local built="$1"
|
||||
local target="${MODPATH}"
|
||||
|
||||
if [ -f "${target}.zst" ]; then
|
||||
target="${target}.zst"
|
||||
elif [ ! -f "$target" ]; then
|
||||
target="${target}.zst"
|
||||
fi
|
||||
|
||||
if [ -f "$target" ] && [ ! -f "${target}${BACKUP_SUFFIX}" ]; then
|
||||
info "Backing up original to ${target}${BACKUP_SUFFIX}"
|
||||
cp "$target" "${target}${BACKUP_SUFFIX}"
|
||||
fi
|
||||
|
||||
if [ "${target%.zst}" != "$target" ]; then
|
||||
info "Compressing and installing module..."
|
||||
zstd -f "$built" -o "$target"
|
||||
else
|
||||
cp "$built" "$target"
|
||||
fi
|
||||
|
||||
depmod -a "$KVER"
|
||||
info "Module installed at ${target}"
|
||||
}
|
||||
|
||||
do_build() {
|
||||
check_bc250
|
||||
check_deps
|
||||
find_source
|
||||
patch_source
|
||||
local built
|
||||
built="$(build_module)"
|
||||
install_module "$built"
|
||||
echo ""
|
||||
info "Done! Patched amdgpu module installed."
|
||||
info "Next: sudo $0 enable"
|
||||
}
|
||||
|
||||
do_enable() {
|
||||
printf '# BC-250 40 CU re-enablement\noptions amdgpu bc250_cc_write_mode=3\n' > "$CONF40"
|
||||
info "40 CU mode configured in ${CONF40}"
|
||||
if ! modinfo amdgpu 2>/dev/null | grep -q 'bc250_cc_write_mode'; then
|
||||
warn "Patched module not detected. Run: sudo $0 build"
|
||||
rm -f "$CONF40"
|
||||
exit 1
|
||||
fi
|
||||
info "Rebooting..."
|
||||
sleep 2
|
||||
reboot
|
||||
}
|
||||
|
||||
do_disable() {
|
||||
rm -f "$CONF40"
|
||||
info "40 CU config removed. Rebooting to stock 24 CU..."
|
||||
sleep 2
|
||||
reboot
|
||||
}
|
||||
|
||||
do_restore() {
|
||||
local target="${MODPATH}"
|
||||
if [ -f "${target}.zst" ]; then target="${target}.zst"; fi
|
||||
local backup
|
||||
backup="$(ls -1 "${target}.bc250-backup-"* 2>/dev/null | head -1)"
|
||||
[ -n "$backup" ] || die "No backup found"
|
||||
cp "$backup" "$target"
|
||||
rm -f "$CONF40"
|
||||
depmod -a "$KVER"
|
||||
info "Original module restored. Reboot to apply."
|
||||
}
|
||||
|
||||
do_status() {
|
||||
printf '\033[1m=== BC-250 CU Status ===\033[0m\n\n'
|
||||
|
||||
if lspci -nn 2>/dev/null | grep -qi "${BC250_PCI_ID}"; then
|
||||
printf ' PCI device: \033[0;32mBC-250 detected\033[0m\n'
|
||||
else
|
||||
printf ' PCI device: \033[0;31mBC-250 not found\033[0m\n'
|
||||
fi
|
||||
|
||||
if modinfo amdgpu 2>/dev/null | grep -q 'bc250_cc_write_mode'; then
|
||||
printf ' amdgpu module: \033[0;32mpatched\033[0m\n'
|
||||
else
|
||||
printf ' amdgpu module: \033[0;33mstock (unpatched)\033[0m\n'
|
||||
fi
|
||||
|
||||
local mode
|
||||
mode="$(cat /sys/module/amdgpu/parameters/bc250_cc_write_mode 2>/dev/null || echo 'N/A')"
|
||||
printf ' write_mode: %s\n' "$mode"
|
||||
|
||||
local cu_line
|
||||
cu_line="$(dmesg 2>/dev/null | grep 'active_cu_number' | tail -1)"
|
||||
if [ -n "$cu_line" ]; then
|
||||
local cus
|
||||
cus="$(echo "$cu_line" | grep -o 'active_cu_number [0-9]*' | awk '{print $2}')"
|
||||
if [ "$cus" = "40" ]; then
|
||||
printf ' active CUs: \033[0;32m\033[1m40\033[0m (full die)\n'
|
||||
elif [ "$cus" = "24" ]; then
|
||||
printf ' active CUs: \033[0;33m24\033[0m (stock)\n'
|
||||
else
|
||||
printf ' active CUs: %s\n' "$cus"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$CONF40" ]; then
|
||||
printf ' modprobe conf: \033[0;32m%s (40 CU enabled)\033[0m\n' "$CONF40"
|
||||
else
|
||||
printf ' modprobe conf: (none - stock mode)\n'
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
build) do_build ;;
|
||||
enable) do_enable ;;
|
||||
disable) do_disable ;;
|
||||
restore) do_restore ;;
|
||||
status) do_status ;;
|
||||
*)
|
||||
echo "BC-250 40 CU Re-enablement Tool"
|
||||
echo ""
|
||||
echo "Usage: sudo $0 <command>"
|
||||
echo ""
|
||||
echo " build Patch, compile, install patched amdgpu (~5 min)"
|
||||
echo " enable Activate 40 CU mode and reboot"
|
||||
echo " disable Return to stock 24 CU and reboot"
|
||||
echo " status Show current CU state"
|
||||
echo " restore Restore original amdgpu module"
|
||||
echo ""
|
||||
echo "Quick start:"
|
||||
echo " sudo $0 build && sudo $0 enable"
|
||||
;;
|
||||
esac
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# cu_map.sh — Read and display CU bitmap from DRM ioctl via libdrm.
|
||||
#
|
||||
# Optional health overlay:
|
||||
# ./cu_map.sh --health /var/lib/bc250-cu-health-test/results.tsv
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HEALTH="${BC250_CU_HEALTH_RESULTS:-/var/lib/bc250-cu-health-test/results.tsv}"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--health)
|
||||
HEALTH="${2:?missing value for --health}"
|
||||
shift 2
|
||||
;;
|
||||
--no-health)
|
||||
HEALTH=""
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
sed -n '1,10p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown argument: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BC250_CU_HEALTH_RESULTS="$HEALTH" python3 << 'PYEOF'
|
||||
import ctypes, struct, os, sys
|
||||
|
||||
libdrm = ctypes.CDLL("libdrm_amdgpu.so.1")
|
||||
fd = os.open("/dev/dri/renderD128", os.O_RDWR)
|
||||
dev = ctypes.c_void_p()
|
||||
maj, min_ = ctypes.c_uint32(), ctypes.c_uint32()
|
||||
libdrm.amdgpu_device_initialize(fd, ctypes.byref(maj), ctypes.byref(min_), ctypes.byref(dev))
|
||||
|
||||
buf = (ctypes.c_uint8 * 1024)()
|
||||
libdrm.amdgpu_query_info(dev, 0x16, 1024, ctypes.byref(buf))
|
||||
raw = bytes(buf)
|
||||
|
||||
num_se = struct.unpack_from('<I', raw, 20)[0]
|
||||
num_sh = struct.unpack_from('<I', raw, 24)[0]
|
||||
cu_active = struct.unpack_from('<I', raw, 48)[0]
|
||||
|
||||
total = 0
|
||||
rows = []
|
||||
patterns = []
|
||||
for se in range(num_se):
|
||||
for sh in range(num_sh):
|
||||
bm = struct.unpack_from('<I', raw, 56 + (se * 4 + sh) * 4)[0]
|
||||
n = bin(bm).count('1')
|
||||
total += n
|
||||
bar = ''.join('■' if bm & (1 << i) else '□' for i in range(10))
|
||||
# check if disabled CUs are contiguous (all packed at one end)
|
||||
disabled = [i for i in range(10) if not (bm & (1 << i))]
|
||||
if len(disabled) == 0:
|
||||
pattern = "full"
|
||||
elif disabled == list(range(disabled[0], disabled[0] + len(disabled))):
|
||||
pattern = "contiguous"
|
||||
else:
|
||||
pattern = "scattered"
|
||||
rows.append(f"SE{se} SH{sh}: {bar}")
|
||||
patterns.append(pattern)
|
||||
|
||||
possible = num_se * num_sh * 10
|
||||
harvested = possible - total
|
||||
print()
|
||||
health_path = os.environ.get("BC250_CU_HEALTH_RESULTS", "")
|
||||
health = {}
|
||||
if health_path and os.path.exists(health_path):
|
||||
with open(health_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
_idx, se, sh, wgp, status = parts[:5]
|
||||
try:
|
||||
health[(int(se), int(sh), int(wgp))] = status
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
print("BC-250 CU Map" + (" + Health" if health else ""))
|
||||
for r in rows:
|
||||
print(r)
|
||||
print(f"{total}/{possible} CUs active, {harvested} harvested")
|
||||
|
||||
if health:
|
||||
usable = 0
|
||||
failed = 0
|
||||
print()
|
||||
print("BC-250 CU Map + Health")
|
||||
for se in range(num_se):
|
||||
for sh in range(num_sh):
|
||||
glyphs = []
|
||||
for cu in range(10):
|
||||
wgp = cu // 2
|
||||
status = health.get((se, sh, wgp))
|
||||
if status == "FAIL":
|
||||
glyphs.append("✗")
|
||||
elif cu < 6:
|
||||
glyphs.append("■")
|
||||
elif status == "PASS":
|
||||
glyphs.append("✓")
|
||||
else:
|
||||
glyphs.append("?")
|
||||
for wgp in range(5):
|
||||
status = health.get((se, sh, wgp))
|
||||
if status == "FAIL":
|
||||
failed += 2
|
||||
elif wgp < 3 or status == "PASS":
|
||||
usable += 2
|
||||
print(f"SE{se} SH{sh}: {''.join(glyphs)}")
|
||||
print(f"{usable}/{possible} CUs usable ({failed} defective, masked)")
|
||||
elif health_path:
|
||||
print()
|
||||
print(f"Health overlay: no results file found at {health_path}")
|
||||
|
||||
libdrm.amdgpu_device_deinitialize(dev)
|
||||
os.close(fd)
|
||||
PYEOF
|
||||
+1
@@ -0,0 +1 @@
|
||||
abfeb1fed87c7b0f29370b6ec2911a8fae1f34e9
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
# BC-250 CU Live Manager
|
||||
|
||||
> **Interactive WGP and CU dispatch control for AMD BC-250 (`gfx1013`) using UMR.**
|
||||
>
|
||||
> Test factory, full, and custom WGP layouts from a terminal UI, then optionally save a chosen layout so it is restored on boot.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Bash" src="https://img.shields.io/badge/Shell-Bash-4EAA25?style=for-the-badge&logo=gnubash&logoColor=white">
|
||||
<img alt="Hardware" src="https://img.shields.io/badge/Hardware-AMD%20BC--250-ED1C24?style=for-the-badge&logo=amd&logoColor=white">
|
||||
<img alt="UMR" src="https://img.shields.io/badge/Register%20Access-UMR-555555?style=for-the-badge">
|
||||
<img alt="Systemd" src="https://img.shields.io/badge/Boot%20Restore-systemd-0088CC?style=for-the-badge&logo=linux&logoColor=white">
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## What this tool does
|
||||
|
||||
The AMD BC-250 normally boots with a **factory driver topology of 24 CUs**. This tool reads that driver topology, shows it in a live dashboard, and lets you route additional WGPs through UMR at runtime.
|
||||
|
||||
| Layout | Meaning | WGP count | CU count |
|
||||
| --- | --- | ---: | ---: |
|
||||
| **Factory WGPs** | WGPs enabled by the driver when AMDGPU boots | 12 WGPs | **24 CUs** |
|
||||
| **Full dispatch** | All WGPs handled by this tool are routed | 20 WGPs | **40 CUs** |
|
||||
| **Custom table** | Your manually selected WGP routing table | User selected | User selected |
|
||||
|
||||
Each **WGP contains 2 CUs**. The factory layout is therefore 12 WGPs, and full dispatch is 20 WGPs.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Live changes are temporary unless you save the table and install the boot service. If you only apply a live table, the driver topology is restored after reboot.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
Download the script:
|
||||
|
||||
```bash
|
||||
curl -L -o bc250-cu-live-manager.sh https://raw.githubusercontent.com/WinnieLV/bc250-cu-live-manager/refs/heads/main/bc250-cu-live-manager.sh
|
||||
chmod +x bc250-cu-live-manager.sh
|
||||
```
|
||||
|
||||
Start the interactive UI:
|
||||
|
||||
```bash
|
||||
sudo ./bc250-cu-live-manager.sh
|
||||
```
|
||||
|
||||
Running the script without a command opens the menu.
|
||||
|
||||
If `umr` is missing, the UI warns you and asks once whether it should try to install it.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard and editor
|
||||
|
||||
### Main dashboard
|
||||
|
||||

|
||||
|
||||
### WGP table editor
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Understanding the WGP table
|
||||
|
||||
The table is split into four shader-engine and shader-array rows:
|
||||
|
||||
| Row | Meaning |
|
||||
| --- | --- |
|
||||
| `SE0.SH0` | Shader Engine 0, Shader Array 0 |
|
||||
| `SE0.SH1` | Shader Engine 0, Shader Array 1 |
|
||||
| `SE1.SH0` | Shader Engine 1, Shader Array 0 |
|
||||
| `SE1.SH1` | Shader Engine 1, Shader Array 1 |
|
||||
|
||||
Each row has five WGP positions:
|
||||
|
||||
| WGP | CU pair |
|
||||
| ---: | --- |
|
||||
| `WGP0` | `CU0-CU1` |
|
||||
| `WGP1` | `CU2-CU3` |
|
||||
| `WGP2` | `CU4-CU5` |
|
||||
| `WGP3` | `CU6-CU7` |
|
||||
| `WGP4` | `CU8-CU9` |
|
||||
|
||||
### Legend
|
||||
|
||||
| Marker | Meaning | Notes |
|
||||
| --- | --- | --- |
|
||||
| `D+` | Driver topology WGP, currently routed | Factory-enabled at driver boot. These form the 24 CU layout. |
|
||||
| `S+` | SPI-routed WGP only | Enabled by the live routing table, not part of the driver boot topology. |
|
||||
| `D!` | Driver topology WGP, not routed | Unsafe or blocked state. The script warns and refuses unsafe live disables. |
|
||||
| `--` | Off | Not currently routed. |
|
||||
|
||||
`D+` entries are the factory WGPs reported by the driver when it booted. They are the reference point for the factory 24 CU configuration.
|
||||
|
||||
---
|
||||
|
||||
## Menu actions
|
||||
|
||||
The UI is designed so that write operations are reviewable. Pressing an action key does not silently write registers. Live write actions show a safety disclaimer and an apply confirmation unless `--yes` is used.
|
||||
|
||||
| Key | Menu label | Clear name | What happens |
|
||||
| --- | --- | --- | --- |
|
||||
| `e` | Edit WGP table | Custom WGP editor | Opens the WGP table editor. Toggle unlocked WGPs, then press `Enter` or `a` to review and apply the target table. |
|
||||
| `f` | Full dispatch | Full 40 CU dispatch | Builds a target table where all 20 WGPs are routed. The table is written only after you accept the safety prompt and confirm the change. |
|
||||
| `t` | Driver dispatch | Restore factory WGPs | Builds a target table from the driver boot topology. This returns routing to the factory 24 CU layout after confirmation. |
|
||||
| `w` | Write table | Save current table | Saves the current live table to `/etc/bc250-cu-live-manager.conf`. This prepares it for boot restore, but does not install the service by itself. |
|
||||
| `i` | Install service | Enable boot restore | Installs and enables the systemd service that reapplies the saved table on boot. It uses the table saved by `Write table`. |
|
||||
| `u` | Uninstall service | Remove boot restore | Disables and removes the boot service and saved config. |
|
||||
| `q` | Quit | Exit | Leaves the current live state as it is until reboot or until another action changes it. |
|
||||
|
||||
> [!NOTE]
|
||||
> Full dispatch and Restore factory WGPs are live actions, but they are not applied just by pressing `f` or `t`. The script first shows the target table, then asks for confirmation. Only after confirmation are the registers updated.
|
||||
|
||||
---
|
||||
|
||||
## Editor controls
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
| Arrow keys | Move around the table |
|
||||
| `h` `j` `k` `l` | Move using Vim-style keys |
|
||||
| `Space` | Toggle the selected unlocked WGP |
|
||||
| `Enter` or `a` | Review and apply the selected table |
|
||||
| `q` | Cancel and return to the menu |
|
||||
|
||||
Driver-active WGPs cannot be disabled live. The editor keeps those entries locked to avoid unsafe live disable paths.
|
||||
|
||||
---
|
||||
|
||||
## Common workflows
|
||||
|
||||
### Try full 40 CU dispatch until reboot
|
||||
|
||||
```text
|
||||
Open UI -> press f -> type accept -> review target table -> confirm y
|
||||
```
|
||||
|
||||
This routes all 20 WGPs live. Nothing is saved for reboot unless you also use `Write table` and install the service.
|
||||
|
||||
### Make full 40 CU dispatch survive reboot
|
||||
|
||||
```text
|
||||
Open UI -> press f -> type accept -> confirm y -> press w -> confirm y -> press i -> confirm y
|
||||
```
|
||||
|
||||
This applies the 40 CU table, saves that live table, then installs the systemd service so the saved table is replayed on the next boot.
|
||||
|
||||
### Restore the factory 24 CU layout
|
||||
|
||||
```text
|
||||
Open UI -> press t -> type accept -> review target table -> confirm y
|
||||
```
|
||||
|
||||
This restores the WGP routing table to the driver boot topology, which is the factory 24 CU layout.
|
||||
|
||||
To keep the factory layout on future boots, either uninstall the boot service with `u`, or press `w` after restoring so the factory table becomes the saved boot table.
|
||||
|
||||
### Build and save a custom WGP layout
|
||||
|
||||
```text
|
||||
Open UI -> press e -> toggle WGPs -> Enter -> type accept -> confirm y -> press w -> confirm y -> press i -> confirm y
|
||||
```
|
||||
|
||||
This applies a custom table live, saves it, and enables automatic restore on boot.
|
||||
|
||||
---
|
||||
|
||||
## Temporary vs persistent changes
|
||||
|
||||
| Action | Changes live table now | Survives reboot by itself | Purpose |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Apply from editor | Yes, after confirmation | No | Test a custom table live |
|
||||
| Full dispatch | Yes, after confirmation | No | Test or apply 40 CUs live |
|
||||
| Restore factory WGPs | Yes, after confirmation | No | Return to the 24 CU driver topology live |
|
||||
| Write table | No | Not by itself | Save the current live table to config |
|
||||
| Install service | No | Yes, if a table is saved | Reapply the saved table at boot |
|
||||
| Apply service | Yes, after confirmation when run manually | No | Apply the saved boot table immediately |
|
||||
|
||||
For persistence, both steps matter:
|
||||
|
||||
```text
|
||||
1. Write table
|
||||
2. Install service
|
||||
```
|
||||
|
||||
Installing the service without a saved table does not preserve the current live state. Writing a table without the service saves the config, but nothing reapplies it automatically after reboot.
|
||||
|
||||
---
|
||||
|
||||
## Write-table indicator
|
||||
|
||||
The action menu always shows:
|
||||
|
||||
```text
|
||||
[w] Write table
|
||||
```
|
||||
|
||||
When the saved boot table is missing or does not match the current live table, it becomes:
|
||||
|
||||
```text
|
||||
[w] Write table *
|
||||
```
|
||||
|
||||
The `*` means the current live table is not saved as the boot table.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Hardware
|
||||
|
||||
- AMD BC-250
|
||||
- PCI ID `13fe`
|
||||
|
||||
### Software
|
||||
|
||||
- `bash`
|
||||
- `umr`
|
||||
- `python3`
|
||||
- `libdrm_amdgpu.so.1`
|
||||
- `systemd` for boot restore
|
||||
- Root privileges for register access
|
||||
|
||||
---
|
||||
|
||||
## Installing UMR
|
||||
|
||||
The built-in installer supports these package managers:
|
||||
|
||||
| System | Package tool |
|
||||
| --- | --- |
|
||||
| Arch / CachyOS | `pacman` / `paru` |
|
||||
| Fedora | `dnf` |
|
||||
| Bazzite and immutable Fedora systems | `rpm-ostree` |
|
||||
|
||||
### Arch, CachyOS, Fedora
|
||||
|
||||
```bash
|
||||
sudo ./bc250-cu-live-manager.sh install-umr
|
||||
sudo ./bc250-cu-live-manager.sh
|
||||
```
|
||||
|
||||
### Bazzite / rpm-ostree
|
||||
|
||||
```bash
|
||||
sudo ./bc250-cu-live-manager.sh install-umr
|
||||
sudo reboot
|
||||
sudo ./bc250-cu-live-manager.sh
|
||||
```
|
||||
|
||||
Notes for immutable systems:
|
||||
|
||||
- `rpm-ostree install` changes the host image and requires a reboot.
|
||||
- If `/usr/local/bin` is not writable, service installation falls back to `/var/usrlocal/bin`.
|
||||
|
||||
---
|
||||
|
||||
## Boot restore
|
||||
|
||||
Boot restore is optional. Use it only when you want a selected WGP table to return after reboot.
|
||||
|
||||
The saved config lives here:
|
||||
|
||||
```text
|
||||
/etc/bc250-cu-live-manager.conf
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```ini
|
||||
BC250_WGP_MASKS=0x1f,0x1f,0x1f,0x1f
|
||||
UMR_ASIC=cyan_skillfish.gfx1013
|
||||
UMR_INSTANCE=1
|
||||
UMR=/usr/bin/umr
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
| --- | --- |
|
||||
| `BC250_WGP_MASKS` | Saved WGP masks in `SE0.SH0,SE0.SH1,SE1.SH0,SE1.SH1` order |
|
||||
| `UMR_ASIC` | UMR ASIC selector |
|
||||
| `UMR_INSTANCE` | UMR DRI instance |
|
||||
| `UMR` | Path to the UMR binary |
|
||||
|
||||
The systemd unit loads this file as an `EnvironmentFile`. On boot, the service runs the saved table through `apply-service` using `--yes`, so it can restore the table without an interactive prompt.
|
||||
|
||||
---
|
||||
|
||||
## CLI reference
|
||||
|
||||
The UI is recommended, but the same operations can be scripted.
|
||||
|
||||
### Status
|
||||
|
||||
```bash
|
||||
sudo ./bc250-cu-live-manager.sh status
|
||||
```
|
||||
|
||||
### Live dispatch presets
|
||||
|
||||
```bash
|
||||
# Enable all supported WGPs: 40 CUs after confirmation
|
||||
sudo ./bc250-cu-live-manager.sh enable all
|
||||
|
||||
# Restore the driver boot topology: factory 24 CUs after confirmation
|
||||
sudo ./bc250-cu-live-manager.sh stock-dispatch
|
||||
```
|
||||
|
||||
### Target a single WGP or CU pair
|
||||
|
||||
```bash
|
||||
sudo ./bc250-cu-live-manager.sh enable-wgp 1.0.4
|
||||
sudo ./bc250-cu-live-manager.sh disable-wgp 1.0.4
|
||||
|
||||
sudo ./bc250-cu-live-manager.sh enable-cu 1.0.8
|
||||
sudo ./bc250-cu-live-manager.sh disable-cu 1.0.8
|
||||
```
|
||||
|
||||
`enable-cu` and `disable-cu` still operate at WGP granularity. For example, CU `8` maps to WGP `4`, which controls CU `8` and CU `9` together.
|
||||
|
||||
### Boot restore commands
|
||||
|
||||
```bash
|
||||
# Save the current live table
|
||||
sudo ./bc250-cu-live-manager.sh write-service-table
|
||||
|
||||
# Install and enable boot restore
|
||||
sudo ./bc250-cu-live-manager.sh install-service
|
||||
|
||||
# Apply the saved table now
|
||||
sudo ./bc250-cu-live-manager.sh apply-service
|
||||
|
||||
# Remove boot restore
|
||||
sudo ./bc250-cu-live-manager.sh uninstall-service
|
||||
```
|
||||
|
||||
### Useful flags
|
||||
|
||||
| Flag | Use case |
|
||||
| --- | --- |
|
||||
| `--yes` | Skip interactive prompts for trusted automation |
|
||||
| `--dry-run` | Print intended UMR writes without applying them |
|
||||
| `--force` | Override the BC-250 PCI detection guard |
|
||||
| `--umr-instance <n>` | Force a specific UMR DRI instance |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
sudo ./bc250-cu-live-manager.sh --yes enable all
|
||||
sudo ./bc250-cu-live-manager.sh --dry-run enable all
|
||||
sudo ./bc250-cu-live-manager.sh --umr-instance 1 status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What the script changes
|
||||
|
||||
The tool reads and writes these BC-250 dispatch-related registers through UMR:
|
||||
|
||||
- `mmCC_GC_SHADER_ARRAY_CONFIG`
|
||||
- `mmSPI_PG_ENABLE_STATIC_WGP_MASK`
|
||||
- `mmRLC_PG_ALWAYS_ON_WGP_MASK`
|
||||
|
||||
Defaults:
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| ASIC selector | `cyan_skillfish.gfx1013` |
|
||||
| Register naming | `mm*` |
|
||||
|
||||
Apply operations clear the BC-250 CC harvest mask before writing SPI masks, following the known working live unlock sequence.
|
||||
|
||||
---
|
||||
|
||||
## Kernel patch
|
||||
|
||||
No kernel patch is required for this workflow.
|
||||
|
||||
This script writes live dispatch registers directly through UMR. Kernel patch material is still useful for research and alternate workflows, but it is not required for this tool.
|
||||
|
||||
---
|
||||
|
||||
## Safety behavior
|
||||
|
||||
- Write actions show a safety disclaimer unless `--yes` is used.
|
||||
- Interactive dispatch actions show the current table, the target table, and the proposed WGP changes before applying.
|
||||
- Type `accept` for the safety disclaimer, then confirm the dispatch plan with `y`.
|
||||
- Live disable paths are blocked when they would disable driver-active WGPs.
|
||||
- Driver-active WGPs are treated as locked in the editor.
|
||||
- Boot persistence only happens after a table is saved and the service is installed.
|
||||
|
||||
---
|
||||
|
||||
## Credits and references
|
||||
|
||||
- Kernel patch and research base:
|
||||
https://github.com/duggasco/bc250-40cu-unlock
|
||||
- Live unlock test and demo script:
|
||||
https://github.com/gennro/bc250-toolkit/blob/main/CachyOS-BC250-CU-Unlock.sh
|
||||
+1331
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Reference in New Issue
Block a user