better/compute

Running bcd on real hardware

A bring-up checklist for bcd on a real machine, what bcd hardware and bcd doctor check before a join, what changes between root, bcd helper and an unprivileged node, WSL 2 specifics, and exactly what has and has not run on real hardware.

Most of bcd is tested against fakes: a fake HGX node (fakehw), a fake host for the config units, fake virsh, qemu-img and nvidia-smi. Round 6 (2026-10-04, bbfa7f4 onwards) ran it on a real machine for the first time and fixed what that found. This page is for the operator bringing bcd up on a new server or workstation: the order to do things in, the two local commands that tell you what bcd sees (bcd hardware, bcd doctor), how the privilege mode changes what bcd can do, what is special about WSL 2, and an exact list of what has and has not run on real hardware. Installing the binary and the systemd units is on installing bcd; the GPU side in depth is on GPUs on a node.

One real machine so far

Everything below that ran on real hardware ran on one machine: a workstation running WSL 2 (Ubuntu) on an AMD Threadripper PRO 9995WX (192 threads) with one GeForce RTX 5090 (driver 616.56, CUDA 13.4), as an ordinary user, without root, an IOMMU, DMI identity or a TPM. No bare-metal server and no data-center GPU has run bcd yet.

Which build

Real hardware needs a host build: one without the fakehw tag, which reads /proc, /sys and nvidia-smi.

BuildTagsUsed by
Releasenone (daemon/.goreleaser.yaml: real NVML and real hardware)The install script's pinned binaries
Host, cgo-freenonvmldaemon/Containerfile (bcd as a DaemonSet), tests/e2e/bcd-host-real.test.ts
Simulatednonvml,fakehwUnit tests, the other e2e suites, bcd sim

Both host builds read GPUs through nvidia-smi; the nonvml build only makes doctor's nvml check warn (see the NVML backend). bcd version prints the build tags. bcd hardware says which backend it runs (host or fakehw) in its first line, and --capture refuses a fakehw build.

cd daemon
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags nonvml -o bin/bcd ./cmd/bcd

Bring-up checklist

  1. Firmware. Turn on VT-x or AMD-V for VMs, and VT-d or AMD-Vi for GPU passthrough. Under Secure Boot the NVIDIA kernel module must be signed by an enrolled key.
  2. Operating system. systemd as init, cgroup v2 (unified) mounted at /sys/fs/cgroup, an NTP client, and nouveau blacklisted on a GPU host. Install the NVIDIA driver yourself, or leave it to the nvidia-driver unit.
  3. Look before joining. Copy a host build of bcd onto the machine and run sudo bcd hardware. Read the findings: a missing product UUID, placeholder serials or no NICs change how the machine can join (below).
  4. Preflight. Write /etc/bcd/config.toml (the install script does) and run sudo bcd doctor --config /etc/bcd/config.toml. Fix every fail line. The install script runs doctor too, but only prints its warning.
  5. Pick the privilege mode. Root (the install script's default) or --unprivileged with bcd helper. Running bcd node as an ordinary user without the helper works for trying bcd out, but takes no GPU holds (below).
  6. Join. A machine whose DMI gives no product UUID or serials (WSL, many VMs, boards with unprogrammed SMBIOS) has no trusted , and JoinService refuses it unless the join token is bound to an identifier it does have: create the token with expect: { mac } (--expect-mac on the CLI), the MAC of its default-route NIC (see binding a token to one machine).
  7. Check the node. nodes.get lists its conditions. GpuModelUnknown keeps a GPU node from scheduling until the control plane's model table knows the card (see GPU naming); GpuCountMismatch means the pool expected another count or model.
  8. Watch it run. bcd status shows the session, the generations, each section and one line per GPU (see bcd status).

bcd hardware

bcd hardware (e82d3a8) prints what this binary sees of the machine, without joining or talking to anything: the inventory bcd would report and a list of bring-up findings. Run it as root: the product UUID, serials, DIMMs and NVMe health are root-only, and an ordinary user reads them empty.

FlagDoes
(none)A summary per section, then the findings
--jsonThe whole inventory as JSON, and no findings
--capture DIRCopies exactly the procfs and sysfs files the collector read into DIR, so inventory.Collector{Root: DIR} replays the same machine (identical section hashes). A path ending in .tar.gz writes a reproducible archive instead, since sysfs symlinks do not survive a Windows checkout. Host builds only
--redactWith --capture: replaces serials, UUIDs, MACs, WWNs, GPU UUIDs and the hostname with stable stand-ins of the same shape (UUID path components too), so a fixture tells nothing about the machine. Without --capture it is a usage error (exit 2)

The summary has one line per system, CPU (with its NUMA nodes and isolated CPUs), memory (with each DIMM), disk, NIC, GPU, BMC and OS:

bcd <version> (host backend)

System    <vendor> <product> (board <vendor> <board>), BIOS <vendor> <version>, UEFI (Secure Boot), TPM 2.0
CPU       <model> (<vendor> x86_64): 2 sockets, 96 cores, 192 threads
  node0   cpus 0-47,96-143, 503.8 GiB
Memory    1007.6 GiB
  DIMM    <slot>       64.0 GiB DDR5 4800 MT/s ECC <manufacturer> <part>
Disk      nvme0n1    3.5 TiB   nvme <model> (serial <serial>, wwn <wwn>)
NIC       eth0       <mac> mlx5_core 0000:18:00.0 100000 Mb/s fw <version>
GPU 0     NVIDIA H100 80GB HBM3 00000000:1B:00.0 GPU-<uuid>
BMC       <vendor> <mac> <ip> (IPMI, Redfish) fw <version>
OS      ubuntu 24.04, kernel <release>, cgroup v2, runtimes containerd,libvirt

Findings
  ok    dmi       <vendor> <product>, UUID <uuid>
  ...

The GPUs come from the same collection bcd node makes (procfs, then nvidia-smi; see where GPU facts come from), except that bcd hardware keeps no memory of GPUs, so a GPU passed through to a VM at that moment is not listed. A capture copies procfs and sysfs only; nvidia-smi's answers are not part of it (GPU fixtures are captured separately, under daemon/internal/nvml/testdata).

Findings

inventory.HostFindings reviews the collection. Every finding has a status (ok, warn, fail), and bcd hardware exits 1 when one fails:

CheckSays
dmiwarn: no DMI at all (a container, or a VM without SMBIOS; the node is identified by MACs only); the product UUID and serials read empty because bcd is not root; no product UUID; a placeholder product UUID (00000000-…, ffffffff-…, 03000200-0400-0500-0006-000700080009 and similar), so the hardware key falls back to serials and MACs and a reclaim needs expect or approval; a placeholder system serial
cpu, numafail when /proc/cpuinfo is unreadable; warn without /sys/devices/system/node (NUMA-pinned placements cannot be honoured)
memoryfail when /proc/meminfo is unreadable; warn when EDAC reports uncorrectable errors; otherwise the total and the huge page pools
dimmswarn when only some DIMMs are ECC, or (as root) when the SMBIOS table lists no memory devices
diskswarn with no physical disk under /sys/block, or a disk reporting a critical warning, at least 90% wear or media errors
bmcwarn when the host has an IPMI interface but the BMC did not answer through /dev/ipmi0 (load ipmi_devintf)
nicswarn with no physical NIC: no MAC identity, no PXE MAC for metal
gpusok: the GPU count and driver version
cgroupfail without cgroup v2: hold slices and instance limits need it (systemd.unified_cgroup_hierarchy=1)
kvmwarn without /dev/kvm
iommuwarn for GPUs without an IOMMU: no passthrough

Round 6 fixed what the first real machine showed (e82d3a8): disks behind Hyper-V's storvsc, iSCSI or a RAID controller's virtual drives are no longer called SAS; SCSI and SATA serials come from the kernel's vpd_pg80; a NIC on VMBus has no PCI address; SR-IOV virtual functions and a BMC's Redfish USB host interface are not the machine's NICs; and arm64 topology comes from sysfs, with core names from the MIDR part (Grace reads Neoverse-V2). Later commits added DIMMs from SMBIOS (82fef0e), NIC firmware (9b83ece), NVMe health (362435e), SATA and SAS health through smartctl (97089e9) and the BMC (9ddb5ea).

bcd doctor as preflight

bcd doctor --config FILE checks the configuration, nvidia-smi, the inventory, the host and the node's identity, one line per check, and exits 1 when any check fails. The checks it always had are listed on the overview, the GPU checks on GPUs on a node. Since e2a0cf0 a host build ends with a real-host preflight (doctor_host.go):

CheckokOtherwise
systemdsystemd is init (/run/systemd/system)fail: hold slices, the helper's units and instance units are systemd units
delegationThe cgroup root delegates cpu, cpuset, memory, io, pids and hugetlb (cgroup.subtree_control)warn naming the controllers the kernel offers but the root does not delegate; systemd enables one once a unit asks for it, so this usually settles once bcd's slices exist
kvm(only reported when missing)warn with the cause: the CPU has vmx or svm but the module is not loaded or is blacklisted; a VM without nested virtualization; or no VT-x/AMD-V flags (enable them in the firmware)
vfioOn a GPU host with an IOMMU: vfio-pci loadedwarn: passthrough loads it on first use
iommu-groupsEach GPU in an IOMMU group of its own (with its other functions and PCI bridges)warn (6cf5122) naming each GPU that shares its group with another device, such as a NIC or an NVMe drive behind a PCIe switch or a root port without ACS: bcd helper refuses to pass such a GPU through, so move it to a CPU-attached slot or enable ACS. Also warn for GPUs with no IOMMU group, and for an inventory with GPUs but no NVIDIA function on the PCI bus (a paravirtual GPU)
clockThe kernel says an NTP client keeps the clock synchronized (adjtimex, read-only), with its estimated errorfail when unsynchronized: certificates, tickets and signed intents compare times; warn when it cannot be read
The findingsEvery finding of bcd hardware that is not ok, except kvm, iommu and cgroup, which other checks report

The GPU checks of f84f1f9 (gpu-bus, nouveau, persistence, fabric-manager, gpu-host) and da60bbd (gpu-binding) run in the same pass. On the round-6 workstation doctor reported nvml ok (616.56), the gpu line ok, gpu-host warning about WSL, systemd ok, the clock synchronized, hugetlb not yet delegated and no DMI.

Root, bcd helper or unprivileged

bcd runs in one of three ways, and they differ in what reaches the control plane:

bcd node as rootbcd node as bcd, beside a root bcd helperbcd node as an ordinary user, no [helper]
Set up byThe install script's defaultdaemon install-script --unprivileged (running bcd node unprivileged)Running the binary by hand, as the real-host e2e does
GPU holdsWrites /etc/cdi and hold slices itselfThrough the helper's file.write and slice.applyRefused at once with node-unprivileged (unprivileged nodes)
Hello capabilities on a GPU hostcdi, devices.limits, binding.kubernetesThe sameNone of the three, so reservations with host resources or a Kubernetes binding are never placed there
Host config unitsApplied directlyApplied by the helper's host.unit, from signed specs (under bcd helper)
Product UUID and serialsRead from sysfsAsked from the helper's dmi.read (a fixed list of /sys/class/dmi/id files, 128 bytes each, no arguments)Read empty: the hardware key leans on MACs
DIMMs (SMBIOS type 17), NVMe health, the BMC in-bandRead directlyAsked from the helper's hw.read (parsed values only, at most 64 entries each, 30 seconds overall)DIMMs from EDAC only; no NVMe health; the BMC's interfaces from sysfs only
SATA and SAS health (smartctl)Read with smartmontools installedNot part of hw.read: smartctl needs rootNot read: smartctl needs root
GPU passthrough persistenceSet by bcd around its own virsh and VFIO callsSet by the helper around libvirt.start, libvirt.restore and libvirt.destroy
doctorAlso checks the helper's directories, its binding and signed-intent pins when [helper] is setRun it as root to get thoseWarns gpu-binding on a GPU host

dmi.read and hw.read (29ed48d, 32beca7, 45bc17f) are read-only and take no arguments: the helper enumerates the entries and controllers itself and never lets the node name a path. What they return is inventory only, never an attestation input.

WSL 2

WSL 2 is a convenient place to try bcd on a GPU workstation, and it is where round 6 ran. It is a Hyper-V VM with a Linux kernel, and differs from a server in ways bcd notices:

AreaOn WSL 2Effect on bcd
GPUParavirtual, through /dev/dxg; no /dev/nvidia*, no NVIDIA kernel module, no PCI function, no /proc/driver/nvidiaInventory and samples from nvidia-smi only; no passthrough, gpu.reset, MIG, DCGM or XIDs; the nvidia-driver unit never installs a Linux driver. See WSL 2 paravirtual GPUs
DMIUsually no product UUID or serialsNo trusted hardware key: bind the join token to the MAC (expect: { mac })
TPMNoneThe node key is a PKCS#8 file; --attestor tpm cannot be used
IOMMUNoneNo passthrough; doctor's iommu warns
KVM/dev/kvm when nested virtualization is onVMs possible; doctor's kvm says why when it is missing
systemdOnly when enabled for the distributionDoctor's systemd check fails without it, and hold slices cannot work
cgroupsv2, but hugetlb not delegated at the root at firstDoctor's delegation warns
DevicesDisks on Hyper-V's storvsc, NICs on VMBusDisks are not SAS; NICs have no PCI address (fixed in e82d3a8)
NetworkingThe Windows host is the default routeA control plane on Windows is reached at the default-route address, with gateway_server_name naming the certificate's host

testdata/hosts/wsl2-threadripper-9995wx.tar.gz in daemon/internal/inventory is that machine, captured with bcd hardware --capture … --redact: the first real-host fixture, replayed by the inventory tests.

What has run on real hardware

Every item here ran on the round-6 workstation, as an ordinary user unless it says otherwise.

WhatWhereResult
Join, adoption and the real inventorytests/e2e/bcd-host-real.test.ts with BCD_E2E_REAL=1 (92e3385)A dev stack on Windows with external TLS; a nonvml host build in WSL joins over TLS 1.3 and mTLS with a token bound to the default-route MAC, is adopted, and its inventory matches the kernel: uname -r, the CPU count and memory within 5%. One GPU in inventory through nvidia-smi
An unprivileged GPU nodeThe same suite (da60bbd)No bc.dev/cap.devices.limits label, and a 1-GPU reservation never reaches binding or active there. Before the GPU model table's version 2 (223022b) the RTX 5090 was GpuModelUnknown and the reservation step skipped (8e9602c)
OTLP GPU metricsThe same suite (898a23c)NodeSpec.telemetry pointed at a collector in the test; the export carries every GPU nvidia-smi lists, bcd.gpu.memory.total equal to nvidia-smi's framebuffer, and a power limit. 4 of 4 tests passed
bcd status and bcd doctorThe same suitestatus --json answers with the node, connected, role node; doctor runs and reports
nvidia-smi parsingTestSnapshotLiveHost in daemon/internal/nvml with BCD_REAL_GPU=1 (0915ab7, 9a6fee9)Every GPU yields UUID, name, architecture, VRAM, compute capability and bus id; light samples agree with full ones
GPU host units, planned onlyTestRealGPUUnitsPlan in daemon/internal/hostconfig with BCD_REAL_GPU=1 (2e71a60)nvidia-driver with the host's branch and gpu-settings with the current power limit plan nothing; branch 470 is refused terminally; container-toolkit plans the toolkit install, runtime configuration and /etc/cdi/nvidia.json. Nothing is applied
Kernel settings, as rootTestRealKernel in daemon/internal/hostconfigA sysctl, the transparent huge page policy and 2 MiB pages set and put back (see host configuration)
Doctor's real-host and GPU checksRun by hand (e2a0cf0, f84f1f9)As listed above
# Linux, or Windows with WSL (BCD_WSL_DISTRO, default Ubuntu); builds bcd with -tags nonvml first
BCD_E2E_REAL=1 pnpm vitest run tests/e2e/bcd-host-real.test.ts
# The real-GPU Go tests (scripts/daemon-test.mjs forwards BCD_* into WSL)
BCD_REAL_GPU=1 node scripts/daemon-test.mjs --run TestSnapshotLiveHost ./internal/nvml/
BCD_REAL_GPU=1 node scripts/daemon-test.mjs --run TestRealGPUUnitsPlan ./internal/hostconfig/

The suite asserts only what holds on any Linux host and logs the rest (GPU count, KVM), so it runs on a laptop, in WSL or on a GPU server; its GPU tests skip without nvidia-smi.

What has not

  • A GPU hold on real hardware. bcd ran unprivileged without [helper], so no CDI spec was written to a real /etc/cdi, no hold slice was created on a real systemd, and the reservation test took its unprivileged branch.
  • A container from a CDI name. The workstation has no nvidia-ctk, so no vendor spec was generated and no container was started with bcd.dev/gpu=<id>.
  • Passthrough. No IOMMU, so no GPU was bound to vfio-pci, and the persistence-mode sequence around it (f25abbe, 5bb9f22) and the passed-through GPU memory (c5c52c4) are tested against fakes only. Neither libvirt-kvm nor cloud-hypervisor has run a real VM.
  • MIG, NVLink, NVSwitch, Fabric Manager, MPS. The only GPU is a GeForce card without them.
  • Data-center GPUs, ECC and row remapping, XIDs, gpu.reset, DCGM. None on a real board.
  • Applying host units. Driver installs, MIG layouts, boot arguments, 1 GiB pages, power limits and Kubernetes joins are planned against the real driver at most, never applied.
  • The TPM. No TPM on the workstation; every TPM test uses the software TPM.
  • Root-only readers. The NVMe and IPMI ioctl paths, DIMMs from SMBIOS and smartctl have not run as root on a real server.
  • A released binary through the install script. No release has been tagged (see installing bcd).

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page