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

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:
2026-07-06 18:54:30 -04:00
co-authored by Claude Fable 5
commit 4e452acc7d
66 changed files with 13574 additions and 0 deletions
@@ -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 "================================================================="
@@ -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"
@@ -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
@@ -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)"
@@ -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
@@ -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
@@ -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
@@ -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