Turning an ex-mining AMD BC-250 board into a working Vulkan inference node
for llama.cpp, on Ubuntu.
These boards are cheap, plentiful on the secondhand market, and genuinely capable — but almost nothing about them works out of the box, and most of the failure modes are silent. You end up with a system that runs, reports no errors, and is quietly a fraction as fast as it should be.
This guide is the write-up of getting several of them working. Every step explains why, because the "why" is what you need when your board behaves slightly differently from mine.
Scope. Host setup only: firmware, cooling, drivers, memory, clocks, and a working
llama.cppbuild. What you serve on top of it is your business.
Two BC-250 boards in a 3D-printed chassis, each with a status display, behind a 2.5G switch. This is what the guide below produces.
- What you are working with
- The four things that will waste your time
- Using the scripts
- 1. BIOS
- 2. Cooling
- 3. Drivers and permissions
- 4. Sensors
- 5. Memory and GTT
- 6. Building llama.cpp
- 7. The clock governor
- 8. Unlocking all 40 CUs
- 9. Thermal tuning
- 10. Running more than one board
- 11. Benchmarks — two-board layer split
- 12. Running it as a service
- Troubleshooting
- The build
- Credits
The BC-250 is a board pulled from ASRock's BC-250 blockchain/mining systems, built around a semi-custom AMD APU whose driver-level codename is Cyan Skillfish. It is a compute part with a display-less, headless personality — which is a large part of why generic AMD GPU guides do not apply cleanly.
| PCI ID | 1002:13fe |
| GPU target | gfx1013, driven by RADV — Mesa's Vulkan driver |
| Compute units | 24 enabled from the factory; 40 physically present |
| Memory | 16 GB GDDR6, shared between CPU and GPU |
| ROCm support | None. Vulkan is the only usable acceleration path. |
| Tested on | Ubuntu Server 26.04 |
Three consequences worth internalising before you start:
No ROCm means Vulkan or nothing. llama.cpp with -DGGML_VULKAN=ON is the
practical path. Anything in a tutorial that says rocminfo or hipcc does not
apply to this board.
Shared memory is not VRAM. The model lives in system RAM, reached by the GPU through GTT. How much of it the GPU may address is a kernel setting, not a hardware limit — and the default silently caps you at roughly half. Section 5.
It was designed to be loud and disposable. The stock cooler assumes a mining chassis with screaming fans and nobody nearby. In a normal room, on sustained inference load, it thermally throttles. Section 2 is not optional.
Read these now. Each one costs hours if you meet it without warning, because none of them produces an error message.
1. Vulkan silently falls back to CPU. If your user is not in the render
group in the current login session, vulkaninfo shows only llvmpipe — a
software rasterizer. Everything builds, everything runs, inference is orders
of magnitude slower, and nothing anywhere says why. Group membership needs a
new login session; reconnect your SSH.
2. The DRM card index changes between reboots. Your GPU is card0 today
and card1 tomorrow. Every guide that tells you to cat /sys/class/drm/card0/device/pp_dpm_sclk is setting you up to read the wrong
device or get "No such file". Always resolve by PCI device ID:
for c in /sys/class/drm/card*/device; do
[ "$(cat $c/device 2>/dev/null)" = "0x13fe" ] && echo "$c"
done3. A kernel upgrade reverts the 40 CU unlock. The patched module is built against the running kernel. After an upgrade you are quietly back to 24 CUs, with no warning. Re-run the build after upgrades, or pin the kernel.
4. Short benchmarks lie about thermals. The heatsink has enough mass to absorb a two-minute test. The real sustained ceiling only appears after a 10–15 minute heat soak. Tune on short runs and you will ship a board that throttles in production.
This repo contains the whole procedure as numbered, idempotent scripts. They are safe to re-run, none of them reboots your machine on its own, and each one refuses to proceed if its preconditions are not met.
git clone https://github.com/wdonega/bc250-llm-setup.git
cd bc250-llm-setup| Script | What it does | Run as | Interrupts |
|---|---|---|---|
00-preflight.sh |
Read-only: hardware, IOMMU, idle temps | user | — |
10-gpu-drivers.sh |
Mesa/RADV + render/video groups |
sudo |
reconnect |
20-sensors.sh |
lm-sensors + the Nuvoton chip |
sudo |
— |
30-ram-gtt.sh |
Raise TTM page limits via GRUB | sudo |
reboot |
40-llama-cpp.sh |
Build with Vulkan + RPC backends | user (not sudo) | — |
50-governor.sh |
Clock/voltage governor + curve | sudo |
— |
60-unlock-40cu.sh |
Patch amdgpu to expose all 40 CUs | sudo |
reboot |
71-configure-worker.sh |
RPC worker as a systemd service | sudo |
— |
72-configure-head.sh |
API server as a systemd service | sudo |
— |
90-validate.sh |
Read-only: verify the whole stack | user | — |
The 7x pair is only for a two-board setup, and they run on different
machines: 71 on the worker, 72 on the head. A single board needs neither.
_lib.sh holds shared helpers and is sourced by the others.
Set ASSUME_YES=1 for unattended runs — but not on 60-unlock-40cu.sh,
which asks you to eyeball the CU map of your specific board first.
You do not need the scripts. Every section below contains the manual commands. The scripts exist to add the checks that are easy to forget by hand.
BIOS ──► cooling ──► drivers ──► sensors ──► GTT ──► llama.cpp ──► governor ──► 40 CU ──► retune
(1) (2) (3) (4) (5) (6) (7) (8) (9)
▲ ▲ ▲ ▲
└── reconnect └── reboot reboot ┘ │
│
benchmark baseline ─────────────────────────────────────────► compare
- Physical work first. Nothing downstream is measurable on a board that throttles at idle.
llama.cppbefore the CU unlock. You want a benchmarked baseline before you change the silicon's configuration, so that a later problem has one candidate cause instead of two.- Thermal tuning last, and again after the unlock. It is the only step that must be re-measured on every individual board.
Two settings, before you boot anything. Do both in one visit and save a reboot.
- Disable IOMMU. A stability requirement on these boards, not a tuning preference.
- Set dedicated VRAM to the minimum (1 GB). Everything the model needs comes through GTT from shared memory anyway; a large carve-out just takes RAM away from it.
To confirm IOMMU is really off once you are booted:
ls /sys/kernel/iommu_groups/ | wc -l # 0 means disabledDo this before anything else, with the board powered off. On sustained inference load the stock cooling is not adequate outside a mining chassis.
There is a trap here worth describing, because it inverts the usual diagnostic. On one board the dried-out factory paste was masking the problem: the heatsink felt cool to the touch, which looks like good news. It was cool because the heat was never reaching it. After repasting, the heat arrived — and the fin stack turned out to be too dense to shed it. Fixing the paste is what exposes the airflow problem. Expect to do both.
- Remove the heatsink — 4 screws in an X pattern around the die.
- Repaste. Factory paste on a refurbished mining board is reliably dried out. This is not optional maintenance.
- Thin out the central/upper fins. They are press-fit: pull them one at
a time, near the base. There is a purpose-made printed tool for exactly
this — BC-250 Scooper
— which grips near the base and spreads the load better than pliers do.
Needle-nose pliers work too, they are just easier to slip with.
⚠️ Never with a Dremel, saw, or anything that produces swarf. Aluminium filings on a populated board are a short circuit.⚠️ Never with the heatsink still mounted on the board.- If a neighbouring fin bends, straighten it carefully before continuing.
- Reassemble and mount 2x 120 mm fans blowing down onto the heatsink.
Target: ~52 °C at idle. If you are sitting at 70 °C+ doing nothing:
- Are the fans actually spinning?
- Is the airflow pointed at the heatsink, not across it?
- Are they on a 12 V rail? On 5 V they spin, look fine, and move far too little air. This is the most common miss.
There is no fan control. On these boards the fans are wired straight to the PSU — no PWM, no software curve, nothing to configure. The governor in section 7 controls clocks, not airflow. Your only thermal levers are physical.
sudo apt update
sudo apt install -y mesa-vulkan-drivers libvulkan1 libvulkan-dev \
vulkan-tools mesa-utils pciutils
sudo usermod -aG render,video "$USER"Or: sudo ./10-gpu-drivers.sh
exit # then ssh back in
id # 'render' must appear in the effective groupsThis is gotcha #1. newgrp render
works in principle but only affects the subshell it spawns, which is exactly
the kind of half-applied state that produces a confusing result an hour later.
Reconnecting is predictable.
vulkaninfo --summaryYou need to see:
deviceName = AMD BC-250 (RADV GFX1013)
driverName = radv
llvmpipeappearing alongside it is normal and harmless.llvmpipeas the only device means the group is not effective. That is a permissions problem, not a driver problem — do not go reinstalling Mesa.DISPLAY/DisplayPlanePropertieswarnings are headless-server noise. Ignore them.
You cannot tune what you cannot measure, and on this board the thermal ceiling is the real limit — not the frequency cap.
sudo apt install -y lm-sensors
sudo sensors-detect --auto
# The board's Nuvoton chip needs force=true: the driver refuses to bind
# because it does not recognise this firmware's customer ID. The readings
# it produces once forced are correct.
sudo modprobe nct6683 force=true
echo 'options nct6683 force=true' | sudo tee /etc/modprobe.d/bc250-sensors.conf
echo 'nct6683' | sudo tee /etc/modules-load.d/99-bc250-sensors.conf
sensorsOr: sudo ./20-sensors.sh
The reading that matters for everything downstream is the amdgpu one.
With a 1 GB BIOS carve-out, the model lives in system RAM and reaches the GPU through GTT. The kernel's TTM allocator caps how many pages it will hand out, and the default is roughly half your RAM — a silent ceiling on model size, well below what the board can actually hold.
Add to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub:
ttm.pages_limit=3670016 ttm.page_pool_size=3670016
3670016 × 4 KiB = 14 GiB. Then:
sudo update-grub
sudo rebootOr: sudo ./30-ram-gtt.sh — which parses the existing line instead of
overwriting it, backs it up, and refuses to touch a file with duplicate
GRUB_CMDLINE_LINUX_DEFAULT entries.
⚠️ Do not hand-roll this withsed 's|GRUB_CMDLINE_LINUX_DEFAULT=""|...|'. It matches only an empty value. On a host that already has kernel parameters it silently changes nothing and exits successfully, and you reboot believing it worked.
Verify after the reboot:
cat /proc/cmdline | tr ' ' '\n' | grep ttm
cat /sys/class/drm/card*/device/mem_info_gtt_total # ~14 GiBAfter the 1 GB BIOS carve-out, free -m on these boards reports about
14693 MB total — and the limit above is 14336 MiB. That looks like it
leaves only ~350 MB for the OS, which would be absurd.
It works because ttm.pages_limit is a ceiling, not a reservation. Nothing
is allocated up front; the parameter only removes the kernel's artificial cap
on how much system RAM the GPU is allowed to map. What actually constrains
you is the real allocation — model weights plus KV cache — competing with the
OS for the same pool.
So do not size your model against this number. Size it against measured free
RAM, which is the last column of
section 11. The short version from
those runs: below roughly 1.5 GB free a config runs but is not operable,
because a full context or one extra process puts llama-server in front of the
OOM killer.
sudo apt install -y build-essential cmake git libcurl4-openssl-dev \
glslc libvulkan-dev spirv-headers spirv-tools
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build -DGGML_VULKAN=ON -DGGML_RPC=ON
cmake --build build --config Release -j"$(nproc)"Or: ./40-llama-cpp.sh — as your normal user, not with sudo, which would
build into /root.
Check vulkaninfo shows RADV before you start the build. Without the render
group effective you will spend ten minutes compiling a perfectly good binary
that then only ever finds llvmpipe.
Why -DGGML_RPC=ON even with one board: it builds ggml-rpc-server, so
this machine can act as a backend for another board and split one model by
layer (section 10).
Turning it on later means a full rebuild — it costs nothing now.
./build/bin/llama-bench -hf Qwen/Qwen2.5-0.5B-Instruct-GGUF:Q4_K_M -ngl 999Write this down. It is what you compare against after the CU unlock and after every governor change. Without it you are guessing.
The stock amdgpu driver has no usable DPM for this APU — it parks the clock
and leaves it there. cyan-skillfish-governor
talks to the SMU directly and gives you a frequency range, a voltage curve, and
a thermal ceiling.
VER=0.4.12
wget "https://github.com/filippor/cyan-skillfish-governor/releases/download/v${VER}/cyan-skillfish-governor-smu_${VER}-1_amd64.deb"
sudo dpkg -i "cyan-skillfish-governor-smu_${VER}-1_amd64.deb" || sudo apt --fix-broken install -y
sudo systemctl enable --now cyan-skillfish-governor-smu.serviceThen install a config at /etc/cyan-skillfish-governor-smu/config.toml. The
governor-config.toml in this repo is a working starting point:
[frequency-range]
min = 1000 # MHz
max = 1900 # MHz
[temperature]
throttling = 88 # clock down above this
throttling_recovery = 78 # clock back up below thissudo systemctl restart cyan-skillfish-governor-smuOr: sudo ./50-governor.sh — which verifies it is really a BC-250 before
setting any voltages, and backs up your existing config.
⚠️ This sets voltages. Take the[[safe-points]]curve from a known-good config rather than inventing values. Undervolting at the top of the curve produces silent output corruption, not a clean crash — you get a model that works and is subtly wrong.
Two things to understand about the numbers:
maxonly governs short bursts. Under sustained load the thermal ceiling decides. A well-cooled 24 CU board settles around 1700 MHz on its own. Raisingmaxdoes not raise sustained clocks.- The 10 °C gap is hysteresis. Narrow it and the governor oscillates between throttled and unthrottled instead of settling.
24 of the 40 CUs are fused off in the driver's view. They can be re-enabled. Tooling: bc250-40cu-unlock.
sudo ./scripts/cu_map.sh| Pattern | Meaning |
|---|---|
Contiguous — ■■■■■■□□□□ in all four shader arrays |
Normal factory harvest. Enabling all 40 is safe. |
Scattered — ■■□□■■□□■■ |
May be genuinely defective silicon. Do not blanket-enable. Run the per-WGP health test from the unlock repo and mask selectively. |
Enabling defective CUs does not get you free performance. It gets you hangs and corrupt output.
sudo apt install -y gcc make zstd binutils pciutils \
"linux-headers-$(uname -r)" "linux-source-$(uname -r | cut -d- -f1)"
git clone https://github.com/wdonega/bc250-40cu-unlock.git
cd bc250-40cu-unlock
sudo ./scripts/bc250-enable-40cu.sh build # 5-15 minutes
modinfo amdgpu | grep -i bc250 # the parm MUST appear here
sudo ./scripts/bc250-enable-40cu.sh enable # writes modprobe.d + initramfs
sudo rebootOr: sudo ./60-unlock-40cu.sh (--map-only to just inspect).
If modinfo shows no bc250 parameter, stop. The build did not take, and
rebooting will simply give you 24 CUs again while you believe you have 40.
sudo dmesg | grep active_cu_number # active_cu_number 40
sudo dmesg | grep bc250-40cu # CC 0xfff80000->0xffe00000, SPI 0x07->0x1f
RADV_DEBUG=info vulkaninfo --summary 2>&1 | grep num_cu # num_cu = 40The patched module is built against the running kernel. After
apt upgradepulls a new one, you are silently back to 24 CUs. Re-run thebuildstep, or pin your kernel.90-validate.shin this repo catches it.
Mandatory after the unlock. 40 CUs draw roughly 125 W where 24 drew 95 W at the same clock. Whatever ceiling you found before no longer holds.
Monitor in one terminal — resolving the card by device ID, per gotcha #2:
CARD=$(for c in /sys/class/drm/card*/device; do \
[ "$(cat $c/device 2>/dev/null)" = "0x13fe" ] && echo "$c"; done)
watch -n 1 "sensors | grep -A1 amdgpu; echo; cat $CARD/pp_dpm_sclk"Sustained load in another:
cd ~/llama.cpp
./build/bin/llama-bench -hf Qwen/Qwen2.5-0.5B-Instruct-GGUF:Q4_K_M \
-ngl 999 -r 50 -p 2048 -n 512Watch for 10–15 minutes. What you are looking for is the clock settling at a value and staying there, rather than sawtoothing between throttled and unthrottled. Sawtoothing means your hysteresis gap is too narrow.
A commonly cited community figure for sustained 40 CU operation is 1500 MHz, but treat that as a sanity check on your own measurement, not a target to configure. Cooling quality varies enormously between boards.
Adjust [frequency-range] and [temperature], restart the governor, measure
again.
Two modes, not mutually exclusive:
Independent nodes — best for throughput. Each board runs its own
llama-server; put them behind whatever OpenAI-compatible router you already
use and let it load-balance.
Layer split — for a model larger than one board's memory. The boards act as one node, each holding part of the layers.
# on the worker
./build/bin/ggml-rpc-server --host 0.0.0.0 --port 50052 --cache
# on the head node
./build/bin/llama-server -hf <model> --rpc <worker-host>:50052 -ngl 999The binary is ggml-rpc-server, not rpc-server — older guides and
earlier llama.cpp revisions use the shorter name and it no longer exists.
Check --help if the flags below do not match your build.
--cache enables the worker's local tensor cache. Without it the head re-sends
its share of the model over the network on every restart, which turns a
service restart into minutes of transfer.
llama.cpp's RPC protocol has no authentication and no input validation. Anything that can reach port 50052 can execute code on the worker, and the command above binds it to
0.0.0.0on purpose — the head has to reach it. Keep it on an internal LAN, firewalled from everything else. Never route it to the internet.71-configure-worker.shin section 12 installs it withIPAddressAllowrestricted to RFC1918 ranges as a backstop, but that is defence in depth, not a substitute for not exposing the port.
This is what -DGGML_RPC=ON in section 6 was for.
Is the split worth the trouble? Measured on these boards, yes, and not for the reason you would expect: splitting was faster than a single node (16.18 vs 14.65 t/s), because one board can only hold a smaller quant. The network is not the bottleneck. Numbers, memory distribution per board and the full comparison are in section 11.
Measured 2026-09-14 with llama.cpp (Vulkan + RPC), model
unsloth/Qwen3.8-27B-GGUF, both boards with the 40 CU unlock applied and the
governor at max 1900 MHz / 88 °C.
| Head node | Worker node | |
|---|---|---|
| Role | llama-server |
ggml-rpc-server -c |
| RAM | 14693 MB | 14694 MB |
| GTT | 14 GiB (ttm.pages_limit=3670016) |
14 GiB |
Interconnect: 2.5 GbE.
The same Java code-review prompt on every run — 875 tokens in (833 on the
Q4/32k run), a fixed 3000 tokens out, -ngl 999 --parallel 1 --no-mmproj, and
KV cache at q8_0 except where noted.
Two generation figures are reported, because they differ and the gap is the
interesting part. Average is eval time over the full 3000 tokens.
First 100 is the instantaneous rate at token 100 — it always starts higher
and decays as the context fills.
| Config | Prompt eval | Gen (avg) | Gen (first 100) | Total | Free RAM on head |
|---|---|---|---|---|---|
| Q4_K_M, 32k ctx (split) | 72.6 t/s | 16.18 t/s | 17.75 t/s | 3m17s | 4.3 GB |
| Q4_K_M, 64k ctx (split) | 75.8 t/s | 15.55 t/s | 17.42 t/s | 3m24s | 3.2 GB |
| Q5_K_XL, 32k ctx (split) | 72.6 t/s | 13.41 t/s | 15.03 t/s | 3m56s | 2.0 GB |
| Q5_K_XL, 64k ctx, KV q8 (split) | 71.5 t/s | 13.23 t/s | 15.05 t/s | 3m59s | 0.9 GB |
| Q5_K_XL, 64k ctx, KV q4 (split) | 71.0 t/s | 13.80 t/s | 15.25 t/s | 3m50s | 1.4 GB |
| Q3_K_XL, 8k ctx (single node) | 80.9 t/s | 14.65 t/s | 18.96 t/s | 3m35s | 0.9 GB |
GTT used across the split:
| Config | Head | Worker | Total |
|---|---|---|---|
| Q4_K_M, 32k | 8.9 GiB | 7.2 GiB | 16.1 GiB |
| Q5_K_XL, 32k | 11.2 GiB | ~8.9 GiB | ~20 GiB |
The head always holds more: on top of its share of layers it carries the KV cache and the context buffers.
Quantisation dominates; context size barely registers. Doubling 32k → 64k cost 3.9 % on Q4 and 1.3 % on Q5. Going Q4 → Q5 cost 17 %. That holds while the context is empty — the real decay shows up as it fills.
The network is not the bottleneck people expect. This was the open question when the build went up, and the answer is: at 2.5 GbE, with a 27B model split by layer, the RPC round-trip is not what limits you. The quant is.
Prompt eval scales with prompt size. Early runs with 66-token prompts managed 16–21 t/s. At 875 tokens it is 71–81 t/s. The per-round-trip overhead dilutes when there is more to process per trip, so short prompts make the split look far worse than it is.
KV cache at q4_0 is nearly free. 4 % faster (less data over the wire) and
~400 MB back. Less than hoped, because with an empty context the cache is not
populated yet — the saving grows in real use.
Splitting solved a memory problem without costing speed. Note what the single-node row is and is not. It is not apples-to-apples, and is not meant to be: Q3_K_XL at 8k is roughly the largest this model gets on one board, and that board ends up with 0.9 GB free. So the comparison it supports is the one that actually matters — split with the quant you want versus single node with the quant that fits. On that comparison the split wins on both axes: a better quant, 4.3 GB of headroom, and a higher average generation rate.
Single node still wins where the network is genuinely absent: best prompt eval (80.9 t/s) and the fastest first 100 tokens (18.96 t/s). It just cannot hold the configuration you want to run.
The two candidates worth running are Q4_K_M + 64k (fastest, 3.2 GB free) and Q5_K_XL + 32k (better quant, 2.0 GB free).
Anything under ~1.5 GB free on the head — Q5 + 64k, and the single-node Q3 —
runs, but is not operable: a full context or one extra process on the box
puts llama-server in front of the OOM killer.
Choosing between the two is not a benchmark question. It comes down to answer quality on real code, which none of this measures. The signal to watch for is API hallucination — a method or annotation that does not exist in the library.
Everything so far has been run by hand. To make it survive a reboot there are two systemd units, one per board.
# on the WORKER board, first
sudo ./71-configure-worker.sh
# on the HEAD board
sudo ./72-configure-head.shWorker first. The head retries every 15s so the reverse order converges too — it just wastes a few minutes in restart loops.
Both units are templates: __USER__ and __HOME__ are substituted at
install time from the account you sudo from, so nothing is pinned to my
username. Don't install the .service files by hand — the scripts render
them, check that the binaries exist, that the render/video groups exist,
and that the board really is a BC-250 before touching systemd.
| Setting | Why |
|---|---|
OOMScoreAdjust=-500 |
With ~3 GB free the server is the box's largest process and the OOM killer's natural first pick. This makes the kernel take something else. |
TimeoutStartSec=900 |
First load pushes half the tensors over the network — minutes, not seconds. The default timeout would kill it mid-load. |
Restart=always + RestartSec=15 |
Covers the worker not being up yet, with no cross-host dependency to declare. |
ProtectSystem=strict, ProtectHome=read-only |
Confines the process; ReadWritePaths opens only the two cache directories it actually writes. |
DeviceAllow=/dev/dri rw, SupplementaryGroups=render video |
Without both, the hardening above takes the GPU away and you are silently back on llvmpipe. |
--cache on the worker |
Local tensor cache, so the head doesn't re-send the model on every restart. |
Model, context size, KV cache type and worker address live in
/etc/default/llama-server:
sudoedit /etc/default/llama-server
sudo systemctl restart llama-server72-configure-head.sh installs that file once and never overwrites it, so
a re-run can't silently change what you're serving. Its comments carry the
measured combinations from
section 11 next to their memory
headroom — which is the number that decides whether a config is operable, not
the throughput.
llama.cpp's RPC protocol has no authentication and no input validation.
Anything that can reach port 50052 can execute code on the worker. The unit
sets IPAddressAllow to the RFC1918 ranges as a backstop, but treat that as
defence in depth, not as permission to route the port somewhere.
The API on :8080 binds to 0.0.0.0 so other machines can use it. Put it
behind whatever you normally use for that, and keep it off the internet.
If you cloned this repo, ./90-validate.sh checks everything below in one pass
and exits with the number of failures.
| Symptom | Likely cause |
|---|---|
vulkaninfo shows only llvmpipe |
render group not effective in this session — reconnect |
| Inference works but is absurdly slow | Same thing. Almost always this. |
pp_dpm_sclk: No such file |
Card index changed on reboot — resolve by device ID 0x13fe |
| Clock stuck low, never recovers | Thermal throttle; throttling_recovery gap too narrow |
| Clock sawtooths under load | Hysteresis gap too narrow — widen it |
| 70 °C+ at idle | Paste, heatsink contact, or airflow. Check the fans are on 12 V |
| Governor will not exceed ~1500 MHz | [frequency-range] max is below the next DPM step |
num_cu back to 24 after an upgrade |
Kernel upgrade reverted the unlock — rebuild |
| GTT far smaller than 14 GiB | ttm.* missing from /proc/cmdline, or no reboot yet |
| Random hangs / corrupt output after unlock | Possibly defective CUs — re-check the harvest map |
| Instability under load, no thermal cause | IOMMU still enabled in the BIOS |
llama-server restart-loops every 15s |
Worker not up — run 71-configure-worker.sh on the other board |
Service dies with 203/EXEC |
Built without -DGGML_RPC=ON, so there is no ggml-rpc-server binary |
| Service runs but inference is slow | Hardening took the GPU — check DeviceAllow / SupplementaryGroups survived your edits |
llama-server OOM-killed on long runs |
Under ~1.5 GB free. Drop the quant or the context |
| Item | Used here | Notes |
|---|---|---|
| Boards | 2x AMD BC-250 | Section 10 covers running them together |
| PSU | MSI 650 W (ATX) | See below |
| Switch | KeepLiNK managed 2.5G — 8x 2.5 GbE + 1x 10G SFP+ | See below |
| Fans | 2x 120 mm per board | Wired straight to the PSU — section 2 |
On the PSU. 650 W is comfortable headroom, not a requirement: two boards draw roughly 125 W each with all 40 CUs active (95 W at 24), so the compute load is around 250 W. Size for the boards you plan to add, and remember the fans hang off the same supply — there is no PWM header, they run straight off a 12 V rail.
On the switch. In the layer-split mode of section 10 activations cross the network between boards on every token, so it is fair to assume the link matters. Section 11 measures it, and the answer is that at 2.5 GbE it is not the bottleneck for a 27B model — the quantisation is. Worth knowing before you spend money on faster networking to fix a problem you do not have. The 10G SFP+ port here is the uplink to the rest of the network, not something the boards themselves need.
The chassis in the photos is 3D printed. models.3mf in this
repo is the full project as sliced — 14 plates, Creality Print — containing
every part used:
| Part | Purpose |
|---|---|
10InchRackGenerator.stl, Rack Edge, Top Front Edge, Left Handle, Right Front Foot |
The 5U 10-inch rack frame |
atx top support, laterais maiores, borda |
Board support and side panels |
honeycomb_plate.stl, honeycomb_plate_fanmount.stl |
Honeycomb side panel and its fan mount |
capa fonte |
PSU cover |
io-shield.stl |
I/O shield |
tampa lcd |
Covers for the two status displays |
These are other people's designs. Follow the links — each remains under its author's own license and terms.
| Model | Author's page | Used |
|---|---|---|
| Mini Lab RAX 5 — 5U server rack | MakerWorld | Adapted — the rack was modified to fit this build |
| BC-250 to ATX case adapter (board mount + front panel) | Printables | Unmodified |
| 2HE 10-inch rack mount for mini-ITX / ATX PSU (hangs the mount in the rack) | MakerWorld | Unmodified |
| BC-250 Scooper (tool for removing heatsink fins) | Printables | Unmodified — see section 2 |
The scooper is a tool, not a chassis part — it is not in models.3mf, print
it separately from the link above.
This guide stands on other people's work:
- filippor/cyan-skillfish-governor — the SMU governor that makes these boards usable at all.
- ggml-org/llama.cpp — the Vulkan backend that makes them useful.
- bc250-40cu-unlock — the CU unlock tooling.
- The wider BC-250 community, whose scattered forum posts and Discord messages are where most of this was originally worked out.
Everything in this guide was run on real hardware. But boards vary — different refurbishers, different thermal history, different silicon quality. Specifically:
- The thermal numbers are from my boards. Yours will differ. Measure.
- The voltage curve is a working starting point, not a universal safe setting.
- The CU unlock modifies a kernel module and re-enables silicon the vendor disabled. It has been stable here. It is still your hardware and your risk.
Corrections and additions from other people's boards are very welcome — that is how the numbers in here get better.

