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.
Which build
Real hardware needs a host build: one without the fakehw tag, which reads /proc, /sys and nvidia-smi.
| Build | Tags | Used by |
|---|---|---|
| Release | none (daemon/.goreleaser.yaml: real NVML and real hardware) | The install script's pinned binaries |
| Host, cgo-free | nonvml | daemon/Containerfile (bcd as a DaemonSet), tests/e2e/bcd-host-real.test.ts |
| Simulated | nonvml,fakehw | Unit 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/bcdBring-up checklist
- 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.
- Operating system. systemd as init, cgroup v2 (unified) mounted at
/sys/fs/cgroup, an NTP client, andnouveaublacklisted on a GPU host. Install the NVIDIA driver yourself, or leave it to thenvidia-driverunit. - 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). - Preflight. Write
/etc/bcd/config.toml(the install script does) and runsudo bcd doctor --config /etc/bcd/config.toml. Fix everyfailline. The install script runs doctor too, but only prints its warning. - Pick the privilege mode. Root (the install script's default) or
--unprivilegedwithbcd helper. Runningbcd nodeas an ordinary user without the helper works for trying bcd out, but takes no GPU holds (below). - 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-macon the CLI), the MAC of its default-route NIC (see binding a token to one machine). - Check the node.
nodes.getlists its conditions.GpuModelUnknownkeeps a GPU node from scheduling until the control plane's model table knows the card (see GPU naming);GpuCountMismatchmeans the pool expected another count or model. - Watch it run.
bcd statusshows 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.
| Flag | Does |
|---|---|
| (none) | A summary per section, then the findings |
--json | The whole inventory as JSON, and no findings |
--capture DIR | Copies 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 |
--redact | With --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:
| Check | Says |
|---|---|
dmi | warn: 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, numa | fail when /proc/cpuinfo is unreadable; warn without /sys/devices/system/node (NUMA-pinned placements cannot be honoured) |
memory | fail when /proc/meminfo is unreadable; warn when EDAC reports uncorrectable errors; otherwise the total and the huge page pools |
dimms | warn when only some DIMMs are ECC, or (as root) when the SMBIOS table lists no memory devices |
disks | warn with no physical disk under /sys/block, or a disk reporting a critical warning, at least 90% wear or media errors |
bmc | warn when the host has an IPMI interface but the BMC did not answer through /dev/ipmi0 (load ipmi_devintf) |
nics | warn with no physical NIC: no MAC identity, no PXE MAC for metal |
gpus | ok: the GPU count and driver version |
cgroup | fail without cgroup v2: hold slices and instance limits need it (systemd.unified_cgroup_hierarchy=1) |
kvm | warn without /dev/kvm |
iommu | warn 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):
| Check | ok | Otherwise |
|---|---|---|
systemd | systemd is init (/run/systemd/system) | fail: hold slices, the helper's units and instance units are systemd units |
delegation | The 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) |
vfio | On a GPU host with an IOMMU: vfio-pci loaded | warn: passthrough loads it on first use |
iommu-groups | Each 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) |
clock | The kernel says an NTP client keeps the clock synchronized (adjtimex, read-only), with its estimated error | fail when unsynchronized: certificates, tickets and signed intents compare times; warn when it cannot be read |
| The findings | Every 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 root | bcd node as bcd, beside a root bcd helper | bcd node as an ordinary user, no [helper] | |
|---|---|---|---|
| Set up by | The install script's default | daemon install-script --unprivileged (running bcd node unprivileged) | Running the binary by hand, as the real-host e2e does |
| GPU holds | Writes /etc/cdi and hold slices itself | Through the helper's file.write and slice.apply | Refused at once with node-unprivileged (unprivileged nodes) |
Hello capabilities on a GPU host | cdi, devices.limits, binding.kubernetes | The same | None of the three, so reservations with host resources or a Kubernetes binding are never placed there |
| Host config units | Applied directly | Applied by the helper's host.unit, from signed specs (under bcd helper) | |
| Product UUID and serials | Read from sysfs | Asked 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-band | Read directly | Asked 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 installed | Not part of hw.read: smartctl needs root | Not read: smartctl needs root |
| GPU passthrough persistence | Set by bcd around its own virsh and VFIO calls | Set by the helper around libvirt.start, libvirt.restore and libvirt.destroy | |
doctor | Also checks the helper's directories, its binding and signed-intent pins when [helper] is set | Run it as root to get those | Warns 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:
| Area | On WSL 2 | Effect on bcd |
|---|---|---|
| GPU | Paravirtual, through /dev/dxg; no /dev/nvidia*, no NVIDIA kernel module, no PCI function, no /proc/driver/nvidia | Inventory 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 |
| DMI | Usually no product UUID or serials | No trusted hardware key: bind the join token to the MAC (expect: { mac }) |
| TPM | None | The node key is a PKCS#8 file; --attestor tpm cannot be used |
| IOMMU | None | No passthrough; doctor's iommu warns |
| KVM | /dev/kvm when nested virtualization is on | VMs possible; doctor's kvm says why when it is missing |
| systemd | Only when enabled for the distribution | Doctor's systemd check fails without it, and hold slices cannot work |
| cgroups | v2, but hugetlb not delegated at the root at first | Doctor's delegation warns |
| Devices | Disks on Hyper-V's storvsc, NICs on VMBus | Disks are not SAS; NICs have no PCI address (fixed in e82d3a8) |
| Networking | The Windows host is the default route | A 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.
| What | Where | Result |
|---|---|---|
| Join, adoption and the real inventory | tests/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 node | The 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 metrics | The 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 doctor | The same suite | status --json answers with the node, connected, role node; doctor runs and reports |
nvidia-smi parsing | TestSnapshotLiveHost 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 only | TestRealGPUUnitsPlan 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 root | TestRealKernel in daemon/internal/hostconfig | A sysctl, the transparent huge page policy and 2 MiB pages set and put back (see host configuration) |
| Doctor's real-host and GPU checks | Run 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 withbcd.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. Neitherlibvirt-kvmnorcloud-hypervisorhas 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
smartctlhave not run as root on a real server. - A released binary through the install script. No release has been tagged (see installing bcd).
Better Compute is created by Sean Filimon
Last updated
config.toml referenceexperimental
Every key and section of bcd's /etc/bcd/config.toml with its type, default, validation and the modes that read it, plus the files bcd keeps under data_dir and elsewhere.
Host configurationexperimental
The config units bcd converges from NodeSpec.host, in order, with their settings, files, refusals and reboot cases, and how bcd helper applies them as root.