Inventory and health
NodeInventory v1 and its section hashes, snapshots and diffs, GPU naming, devices and atoms, conditions, XID handling, remediation and acceptance.
Placement can only book what the control plane knows a node has, and should only book it on hardware that works. is what a node reports about its hardware; health is what the control plane concludes from it. Together they produce the node's and , the labels selectors match, and the that keep a broken node or GPU out of placement.
Inventory is owned by @better-compute/inventory, a built-in module that every server registers.
NodeInventory v1
The daemon reports its hardware as eight sections of the NodeInventory v1 schema
(bettercompute.agent.v1). Readings that change by the second (temperatures, power draw, clocks, utilization)
are never inventory; they are telemetry, and they never go to Postgres.
| Section | Source on the node | Holds |
|---|---|---|
system | DMI | vendor, product, serials (system, board, chassis), product UUID, BIOS, boot mode, Secure Boot, TPM |
cpu | /proc/cpuinfo, sysfs | vendor, model, architecture, sockets, cores, threads, flags, NUMA nodes |
memory | /proc/meminfo, EDAC, DMI | total bytes, huge page pools, DIMMs, EDAC counters |
disks | sysfs, udev, NVMe, SMART | name, model, serial, WWN, size, transport, SMART health |
nics | netlink, sysfs | physical NICs: permanent MAC, driver, firmware, speed, PCI address, NUMA node, SR-IOV VFs, RDMA, link, MTU |
gpus | NVML | driver, CUDA and NVML versions; per GPU: index, UUID, PCI bus id, raw NVML name, architecture, VRAM, serial, VBIOS, NUMA node, MIG state, NVLinks, topology row, fabric clique, ECC counters, row remapping |
bmc | in-band IPMI, Redfish host interface | IP, MAC, vendor, firmware, Redfish and IPMI support |
os | os-release, uname | distro and version, kernel, cgroup version, KVM, IOMMU, runtimes, hostname |
The daemon collects inventory when its boot changes, on hardware events and XIDs, after provisioning steps,
on a jittered 12-hour rescan, and when you run inventory.refresh.
How inventory travels
Inventory is large and rarely changes, so it is never re-sent whole. Each section travels, and is stored, by the hash of its bytes.
Hash on the node
The daemon canonicalizes each section (repeated fields sorted by a stable key), encodes it, and takes the SHA-256
of those exact bytes. Hello, every tenth heartbeat, and the first heartbeat after changed sections were sent
(427787a) carry the section hashes.
Pull what changed
The gateway records the hashes and asks, in Welcome.request_sections, only for sections whose hash differs from
the stored one. Unknown section names from a newer daemon are ignored.
Verify over the received bytes
Each InventorySection{ section, content, sha256 } is checked against its SHA-256 over the bytes as received
and decoded; it is never re-encoded to compare, so only the daemon's encoding has to be stable. A section over
1 MiB, a hash mismatch or bytes that do not decode are INVALID_INPUT.
Apply once complete
The Inventory controller runs when every reported section is present. The snapshot digest is the SHA-256 over
name=hash lines in section order; an unchanged digest does nothing. A changed one is applied in one transaction
with the node row locked.
Applying an inventory:
- appends a snapshot (
inventory_snapshots, idinv_…): snapshots are append-only; - plans the node's devices and atoms, and its inventory labels;
- sets the ;
- writes the inventory conditions and health findings, and mirrors the NoSchedule ones into
noScheduleReasons, keeping every entry it does not own; - publishes
node.inventory.changed(snapshotId,digest,sections,changes) and one event per change:node.gpu.missing,node.gpu.added,node.disk.replaced,node.driver.changed,node.mig.layout.changed.
Trust: tenant-controlled nodes are advisory
A node under a dedicated-root lease has trust: 'tenant-controlled':
the lessee has root, so it can report anything. Its reports are advisory (PLAN.md §10.1 Trust). For such a node:
applyInventoryappends the snapshot and publishesnode.inventory.changedwithadvisory: true(and no per-change events), and changes nothing else. Devices, device health, labels, conditions,noScheduleReasons, the image cache and the hardware key keep the platform's last view, and the applied snapshot stays the platform's last one. The result status isadvisory.- The applied digest is stored as
advisory:<digest>, so hash reports treat the set as applied and heartbeats do not queue it again. recordXidignores its XIDs (no condition, no quarantine, no NoSchedule), andrecordHostPressureignores its pressure, so a lessee cannot make avps-hostpool's node NoSchedule or raise conditions.- The pool's image cache query for the
image-cachedscorer skips tenant-controlled nodes.
Without this, a GPU the lessee reports missing or unhealthy (or a faked XID) would turn the device
healthy: false and leave the lease's bill. The inventory.get, inventory.history and inventory.diff
methods still read the appended snapshots, the lessee's included. When metal ends the lease,
metal.exitTenantRoot calls endAdvisoryInventory, which clears the advisory digest, so the next report applies
the set for real under platform trust even if it did not change.
Reading inventory
All three methods are decided as compute:nodes:read on the node, and answer NOT_FOUND for a node of another
tenant or one with no inventory yet.
| Method | Returns |
|---|---|
inventory.get | The latest snapshot: nodeId, hash, sections (decoded, 64-bit numbers as strings), collectedAt. |
inventory.history (v0.2) | Snapshots newest first, limit 1 to 100 (20 by default), paged by snapshot id. |
inventory.diff (v0.2) | The changes between two snapshots, named by id (inv_…) or hash; by default the latest against the one before. |
const latest = await client.inventory.get({ tenantId, nodeId });
console.log(latest.sections.gpus);
const { from, to, changes } = await client.inventory.diff({ tenantId, nodeId });
for (const change of changes) console.log(change.type, change.deviceId, change.before, change.after);better-compute api inventory.diff tenantId=$TENANT nodeId=node_… --format tableA diff lists, in this order, GPUs missing and added (by UUID), MIG layout changes (mode plus sorted instances), disks replaced (the same kernel name with another WWN or serial), and driver changes (NVIDIA driver or CUDA version). A section missing on either side yields no changes for it: a node that stopped reporting a section did not lose hardware.
GPU naming
NVML names are inconsistent across drivers and SKUs ("NVIDIA H100 80GB HBM3", "NVIDIA H100 PCIe"). Every GPU gets two normalized names:
gpuProduct, the exact product in<MODEL>-<FORM>-<MEMORY>GBform, such asH100-SXM5-80GB;gpuModel, its family, such asH100.
Pools, policies and meters use gpuModel: a pool holds exactly one. Selectors may use either. The mapping is the
versioned table gpuModels (the plan's gpu-models.json, version 2), and normalizeGpuProduct(name) applies
it: upper case, punctuation collapsed to -, a leading NVIDIA dropped.
| Family | Products |
|---|---|
H100 | H100-SXM5-80GB, H100-PCIE-80GB, H100-NVL-94GB |
H200 | H200-SXM5-141GB, H200-NVL-141GB |
B200 | B200-SXM-180GB |
GB200 | GB200-SUPERCHIP-186GB |
A100 | A100-SXM4-40GB, A100-SXM4-80GB, A100-PCIE-40GB, A100-PCIE-80GB |
L40S | L40S-PCIE-48GB |
L4 | L4-PCIE-24GB |
RTX-PRO-6000 | RTX-PRO-6000-SERVER-96GB, RTX-PRO-6000-WORKSTATION-96GB, RTX-PRO-6000-MAXQ-96GB |
GH200 | GH200-SUPERCHIP-96GB, GH200-SUPERCHIP-144GB |
L40, A40, A30, A10 | L40-PCIE-48GB, A40-PCIE-48GB, A30-PCIE-24GB, A10-PCIE-24GB |
RTX-6000-ADA, RTX-A6000 | RTX-6000-ADA-PCIE-48GB, RTX-A6000-PCIE-48GB |
RTX-5090, RTX-5080, RTX-4090 | RTX-5090-PCIE-32GB, RTX-5080-PCIE-16GB, RTX-4090-PCIE-24GB |
Version 2 added the parts real hosts reported that version 1 missed: data-center cards outside the flagship lines, and the workstation and GeForce cards that edge, lab and single-tenant hosts run.
A name the table does not know keeps its canonical form as gpuProduct, maps to gpuModel: 'other', and raises
GpuModelUnknown, which keeps the node from scheduling until the table knows it.
Devices and atoms
The gpus section becomes devices with stable ids, so a reboot or a re-inventory never renames them:
| Device | Id | Class | Bookable |
|---|---|---|---|
| A whole GPU | its UUID, GPU-… | gpu | yes, unless partitioned |
| A MIG instance | <gpuUuid>/gi<g>/ci<c> | gpu.mig | yes; its parent GPU is not |
| A time-slice or MPS slot | <gpuUuid>/s<k> | gpu.slot | yes; its parent GPU is not |
Only atoms are booked. Slots exist in timeslice and mps pools: 4 per GPU for time-slicing and 10 for MPS by
default (policy.sharing.replicas, 1 to 48, MPS's limit; a708aa6). A GPU whose MIG mode is about to change has every atom
unbookable, with partitionPending: true, until the change completes. See
fractional GPUs.
Device attributes are what selectors and scorers read: gpuProduct, gpuModel, vramGiB, index,
pciBusId, numaNode, architecture, computeCapability, driverVersion, migCapable, nvlinkPeers,
fabricCluster and fabricClique. A device that vanished while it still has live bookings is kept with
healthy: false and deleted once its bookings end. A device id another node already owns is left alone and
raises DuplicateIdentity.
Labels
Inventory also writes node labels with source inventory, the way NVIDIA's GPU Feature Discovery does:
gpu.present, gpu.count, gpu.product, gpu.model, gpu.vramGiB, cuda.driver.major,
nvidia.driver.version, mig.capable, fabric.domain, cpu.arch, cpu.vendor, cpu.model, os.distro,
os.kernel and kvm.available. A label whose GPUs disagree reads mixed. nodes.label cannot overwrite them.
Since badb8c2 the host's CPU and memory have labels too, for selectors and when: expressions such as
numa.nodes > 1:
| Label | Value |
|---|---|
cpu.count | Logical CPUs |
cpu.sockets | Sockets |
cpu.isolated | How many CPUs the kernel isolates (0 when none) |
numa.nodes | NUMA nodes |
memory.totalGiB | Installed memory in whole GiB |
hugepages.2Mi, hugepages.1Gi | Configured pages in the host's 2 MiB and 1 GiB pools (0 when the size exists but holds none) |
cpu.model is the reported model with trademark marks dropped and every other run of characters outside
A-Z a-z 0-9 . _ turned into - (AMD EPYC 9654 96-Core Processor reads AMD-EPYC-9654-96-Core-Processor);
a sandbox snapshot that includes memory restores only on hosts with an equal value (see
snapshots).
The gateway writes one more kind at every Hello: each driver the daemon advertises in Hello.drivers becomes
the label bc.dev/driver.<id>=<version> with source system (a missing version reads unknown; ids outside
[a-z0-9._-] are skipped), and driver labels the daemon no longer advertises are dropped, in one statement that
writes nothing when they did not change. Operators cannot overwrite them either. Placement can require them with
PlacementRequest.nodeLabels, where '*' accepts any version (see
placement). The vps module does this for every runtime: a hosted
instance is placed only on hosts carrying bc.dev/driver.<runtime>, bcd's built-in libvirt-kvm and
cloud-hypervisor included since 0fca413 (before, only plugin runtimes needed it).
Since 14ceb1e the same statement also writes one label per kind each advertised driver serves,
bc.dev/driver-kind.<id>.<kind>=1 (a site agent hosting bcx-ipmi carries bc.dev/driver-kind.ipmi.power,
.management and .inspect), and drops the ones a later Hello no longer lists. Kinds outside [a-z][a-z0-9-]
(1 to 32 characters) are skipped, and at most 32 are read per driver. A placement or selector can require one with
'*', as for driver labels. Metal reads them (a3ec8d0): before a verb starts a workflow, it keeps only the operations
of the kinds the site's agents report for the node's BMC driver, and refuses the verb with UNSUPPORTED (reason
bmc-driver-unsupported) when a step needs one they lack. The labels also make a driver metal's own table does not
know checkable. See what metal checks before a workflow.
The hardware key
The hardware key identifies the machine rather than its operating system. It is the SHA-256 over the normalized
product UUID, serial (system, else board, else chassis), sorted permanent MACs and sorted GPU UUIDs, after
dropping firmware placeholders such as To Be Filled By O.E.M.. It is trusted when at least two independent
identifiers survive, and trusted keys are unique across nodes. JoinService computes it from the join claims and
the Inventory controller from inventory, with the same function, so both agree. A second node with the same
trusted key (a cloned disk image, a moved board) gets DuplicateIdentity and keeps its key untrusted until an
operator resolves it.
A copied node certificate shows up at the gateway instead: two machines with the same certificate keep taking the
node's session from each other, each second Hello publishing security.node.duplicate-session. More than 3 such
alternations within 10 minutes revoke the certificate (security.node.quarantined, reason duplicate-identity), and
the real machine must re-join with a new token. See duplicate identity.
Conditions
A condition is { type, status, reason?, message?, since }, where status is true, false or unknown and
since moves only when the status changes. nodes.get lists them. A condition with a NoSchedule policy also
puts its type into noScheduleReasons, in the same transaction.
| Condition | Raised by | Effect |
|---|---|---|
GpuCountMismatch (v0.1) | Inventory: a GPU count other than the pool's policy.expect.gpuCount, or a family other than the pool's gpuModel | NoSchedule. unknown with reason NotReported when the node sends no gpus section. |
GpuModelUnknown (v0.1) | Inventory: a GPU name missing from gpuModels | NoSchedule |
DuplicateIdentity | Inventory: the trusted hardware key or a device id belongs to another node | NoSchedule |
AcceptanceFailed | NodeLifecycle, see below | NoSchedule while true |
NodeUnreachable | Liveness, see operating nodes | the unreachable reason |
Xid48 (v0.2) | an XID 48 (double-bit ECC) | the GPU is quarantined |
Xid79, Xid95 (v0.2) | an XID 79 (fell off the bus) or 95 (uncontained ECC) | NoSchedule; a reboot once no allocation is live |
RowRemapPending (v0.2) | XID 63, or inventory's row-remap state | NoSchedule; a GPU reset once no allocation is live |
RowRemapFailed (v0.2) | XID 64, or inventory's row-remap state | the GPU is quarantined for replacement |
EccUncorrectable, NvlinkDegraded (v0.2) | inventory: volatile uncorrectable ECC errors, or NVLinks down on a GPU that has NVLink peers | the GPU is quarantined |
DriverMismatch, CertExpiring, DaemonOutdated | the hygiene sweep, see below | NoSchedule, plus node.condition.changed |
CpuPressure, MemoryPressure, DiskPressure (v0.3) | host pressure in status reports, see below | NoSchedule on vps-host pools; an alert elsewhere |
FabricManagerDown (v1.1) | bcd's fabric-manager unit: an NVSwitch system without Fabric Manager running, so CUDA cannot initialize multi-GPU jobs (c7613e1) | NoSchedule while true |
MPSDown (v1.1) | bcd's gpu-sharing unit: an mps pool's MPS control daemon is not running, so every client fails (c7613e1) | NoSchedule while true |
RebootRequired (v1.1) | bcd's host units: a setting takes effect only after a reboot; the reason names the units (0aea5c0), see below | Informational |
HostManagedExternally (v1.1) | the host section's state on a node with [host] units = false (c7613e1) | Informational |
ForeignGpuExposure (v1.1) | bcd on a Kubernetes worker: something besides bcd hands out GPUs (another nvidia.com/* device plugin, another GPU DRA driver in the node's ResourceSlices, or a container toolkit that accepts NVIDIA_VISIBLE_DEVICES from unprivileged pods, including one the container runtime runs whose configuration is missing or unreadable), or, on a DaemonSet node, no container runtime configuration is readable, so bcd offers none (9f860ad, f88bcdb; see GPU isolation) | NoSchedule while true (ebd9806), and the node's active reservations are Degraded with reason ForeignGpuExposure (4570dfe) |
PodResourcesUnavailable (v1.1) | bcd on a Kubernetes worker: it holds devices for Kubernetes and the kubelet's pod-resources socket is missing or failing, so released devices cannot be reclaimed from pods (9f860ad) | NoSchedule while true (ebd9806), for the whole node |
KubernetesReleased (v1.1) | bcd: the spec dropped k8s-join, and bcd cordoned and drained the Node (9f860ad) | Informational |
KubernetesTaintsDrift (v1.1) | bcd's k8s-join unit: the Node's taints differ from the pool's, which the kubelet may not change; the message is the kubectl taint command a cluster admin runs (6ef198b) | Informational |
KubernetesReleaseStuck (v1.1) | bcd: a released Kubernetes allocation still held after 5 minutes (by a pod, a prepared DRA claim or a pod process, or with the kubelet unreachable), so its release is not reported yet (cd5fc82, 8bcbe92) | NoSchedule while true (ebd9806) |
KubernetesReleasesUnreadable (v1.1) | bcd: its pending Kubernetes releases file (releasing.json) could not be read, so every GPU of the node stays claimed until no pod may hold a bcd GPU of an allocation the node does not know, or an operator removes the marker <data_dir>/releasing.unreadable (a18f423, b01130f; see an unreadable releases file); reason ReleasesFileUnreadable | NoSchedule while true (4884a65) |
The gateway's daemonNodeConditions (b91dcff, 88219ea) lists the Kubernetes conditions, so they are written as node
conditions. Since ebd9806 (security review 2) three of them, and since 4884a65 a fourth, are NoSchedule while true on
a node that is not tenant-controlled (on a tenant-controlled node they stay advisory, as below), because each means
the node cannot hand out GPUs safely:
ForeignGpuExposure: another path (the GPU operator's device plugin, another GPU DRA driver, orNVIDIA_VISIBLE_DEVICESfrom unprivileged pods) reaches the node's GPUs, or bcd cannot rule one out, so a hold booked there would not be isolated. A change of it also wakes the node's started reservations, which then showDegraded/ForeignGpuExposureat once (4570dfe; see reservation conditions).PodResourcesUnavailable: bcd cannot tell which pods hold which devices. Placement has no NoSchedule for one binding only, so the whole node takes no work until the socket answers.KubernetesReleaseStuck: until ebd9806 it rode bcd'sStatusReport.conditionsand thenode.statusevent only. Now it is recorded, and while a released allocation's devices are still held by a pod the node takes no more work, since the GPUs may still be in use; an operator looks first.KubernetesReleasesUnreadable: bcd could not read its pending releases, so every GPU of the node stays claimed on the node itself (no hold, VM,gpu.resetordiag.dcgmgets one). Until 4884a65 the gateway dropped it, so placement kept booking those GPUs and each new hold failed on the node withoverlap. Now it is recorded and the node takes no work until the claim clears.
KubernetesReleased and KubernetesTaintsDrift stay informational. See
Kubernetes workers.
FabricManagerDown, MPSDown, RebootRequired, HostManagedExternally and the Kubernetes conditions above come
from bcd's host configuration and its Kubernetes side. The gateway's
xidStatusHandler makes each the node's condition exactly as the last status report says, true while reported and
false once it is not, and writes and publishes node.condition.changed only on a change. On a tenant-controlled
node they are advisory: recorded, never a NoSchedule reason.
RebootRequired. Some host settings wait for a reboot: 1 GiB huge pages reserved on the kernel command line, CPUs
isolated with the cpu unit, the iommu unit, an ECC mode set with gpu-settings, a driver swap while the GPUs are
in use, and a MIG mode reset that failed. bcd never reboots on its own, and nothing in the control plane reboots on
this condition: reboot the node yourself with node.reboot, within the pool's
disruption budget. The node keeps running and taking work meanwhile.
The condition table in @better-compute/inventory (nodeConditionTypes) also defines PcieReplayHigh and
ClockSkew. No controller raises those on nodes yet.
Node hygiene
Some faults no single report shows. The inventory module's inventory.hygiene reconciler, a periodic singleton,
sweeps every node that is not deleted or retired every 5 minutes, 500 nodes per transaction:
| Condition | Raised when |
|---|---|
DriverMismatch | The node converged (observed_generation >= generation) on a spec whose nvidia-driver host unit wants branch B, while its GPUs report a driver of another branch. |
CertExpiring | The newest unrevoked certificate of the node's identity generation (ramdisk certificates aside) has less than a tenth of its lifetime left, and at least an hour, so renewal is failing. |
DaemonOutdated | The session's proto_max is below the control plane's protocol version, or bcd runs below a configured minDaemonVersion. |
Each raised condition adds a NoSchedule reason and publishes node.condition.changed (condition, status,
reason, noSchedule); both go when the cause does. Only changes are written. The built-in reconciler uses the
defaults; hygieneReconciler(options) and runHygiene(sql, options) take every, minDaemonVersion,
protocolMax, certExpiringFraction, certExpiringMinSeconds and batch.
Host pressure
Since protocol v0.3, StatusReport.pressure carries the host's Linux pressure stall information ("some" avg10
from /proc/pressure/*, percent of wall time) and CPU steal (percent of the last report interval); it is absent
when the kernel exposes no PSI. recordHostPressure(tx, nodeId, report) turns it into conditions:
| Condition | Raised at | Cleared below |
|---|---|---|
CpuPressure | CPU PSI 50%, or steal 20% | half of each |
MemoryPressure | memory PSI 20% | 10% |
DiskPressure | I/O PSI 40% | 20% |
Between the two levels a condition keeps its status, so a host hovering at a threshold does not flap. On nodes of
vps-host pools the conditions are NoSchedule; elsewhere they only alert through node.condition.changed.
Nothing is written while nothing changes, and nothing at all for a tenant-controlled node (see
trust). The gateway runs it only when the host installs the status handler:
import { createGateway, xidStatusHandler } from 'better-compute/gateway';
import { hostPressureStatusHandler } from 'better-compute/inventory';
createGateway({ reports: { status: [xidStatusHandler, hostPressureStatusHandler({ events })] } });hostPressureStatusHandler({ thresholds }) takes other raise thresholds (cpu, steal, memory, io). In
tests, FakeDaemon.setPressure() puts a HostPressure into every status report.
bcd reports real values (daemon/internal/health). Every 30 seconds it reads the some avg10 line of
/proc/pressure/cpu, memory and io, and computes CPU steal from the cpu line of /proc/stat as the share of
CPU time stolen since the previous sample (0 on the first). It puts the sample into its latest StatusReport and
sends that report again. A kernel without PSI yields no pressure at all, so no pressure condition is raised.
Health, quarantines and remediation
Not every fault should take a whole node away. A GPU with a double-bit ECC error is one bad atom on an eight-GPU machine, while a GPU that fell off the bus usually needs a reboot of the node.
- Device quarantine. A device-level fault writes a
device_healthrow (sourcexidorinventory) and marks the devicehealthy: false. The atom leaves placement; the rest of the node keeps working. An XID quarantine stays until it is lifted withclearDeviceHealth(tx, deviceId, reason?)(a function of@better-compute/inventory) or by a successful remediation; inventory quarantines follow each new inventory. - XID events. The daemon reports XIDs as
Xid<n>conditions on a GPU in its status reports. With the gateway'sxidStatusHandlerinstalled (createGateway({ reports: { ...gatewayReportHandlers, status: xidStatusHandler } })), each one goes throughrecordXid. XIDs without a policy (13, 31, 43, 45 and other application faults) are only recorded as events, and XIDs from atenant-controllednode change nothing. When an XID quarantines a GPU, the handler also wakes the node's started reservations at once (04f6bda), so a reservation holding that GPU turnsDegradedwith reasonAtomUnhealthy, naming the GPU, within a pass, rather than at capacity's periodic recheck or provision's health reconciler. Nothing is released, and the node keeps its hold; on a Kubernetes worker bcd's device plugins or DRA driver stop offering the GPU as healthy (6f5a99f).tests/e2e/kubernetes-bindings.test.tsruns this with an XID 48 on a Kubernetes-bound GPU. - XIDs in bcd. bcd follows
/dev/kmsgfrom its end and parses driver lines of the formNVRM: Xid (PCI:0000:18:00): 79, .... It normalizes the bus id to NVML's form (00000000:18:00.0), finds the GPU with that bus id in its inventory, and raises conditionXid79(statusTRUE,device_idthe GPU's UUID, the driver's text as the message), one per XID and GPU. The status report goes out at once, and the condition stays in every later report. An XID for a bus id the inventory does not know is only logged. bcd keeps these conditions for the rest of the boot: they are stored with the boot id indata_dir/faults.json(mode 0600), so a restart of bcd in the same boot reports them again even though it reads the kernel log from its end, while the first start after a reboot finds another boot id and drops them. The reboot that remediation runs therefore clears them, and so does agpu.resetthat leaves the GPU healthy, for that GPU (662fb55). An XID also makes bcd collect its inventory again about 2 seconds later, since the GPU's ECC and row-remap counters move with it. - Remediation. Node-level faults queue the
healthreconciler of the provision module. It waits until the node holds no live allocation, then runs anode.rebootworkflow (Xid79,Xid95) or a GPU reset (RowRemapPending; bcd'sgpu.resetrefuses a GPU that still has a live hold, see delivery). Runs share the pool's disruption budget (policy.disruptionBudget, 5% of the pool and at least one node, together with workflows and rollouts), and a circuit breaker pauses remediation for a pool when more than 10% of its nodes turned unhealthy within 10 minutes (pool.remediation.paused). A successful reboot clears the XID conditions and their quarantines; a GPU reset is confirmed by the next inventory. A failed or cancelled run, or two runs that did not clear the fault, put the node inmaintenancefor an operator (node.remediation.escalated). Progress is published asnode.remediation.startedandnode.remediation.succeeded.
Acceptance checks
A node that just joined has not proven its hardware works. Acceptance runs the pool's checks before the node becomes schedulable, and again whenever you ask.
{
"acceptance": [{ "kind": "diag.nvml" }],
"expect": { "gpuCount": 8 }
}The NodeLifecycle controller runs them for a node in adopting once it is connected:
- It takes the pool's
policy.acceptancelist (each kind once). A site agent, a node without a pool, or an empty list passes at once (reasonNoChecks). - It enqueues one command per check, with idempotency key
acceptance:<nodeId>:<attempt>:<kind>and a 10-minutenot_after, setsAcceptanceFailedtounknown(reasonRunning), and publishesnode.acceptance.startedwith the attempt, command ids and checks. diag.nvmlchecks the GPUs inventory found againstpolicy.expect.gpuCount(else against the number found). Other kinds run with their default payload in this release; a kind that is not registered fails the attempt withUnsupportedCheck. On bcd (662fb55),diag.nvmlfailsnvml.initwhen the driver's management interface (nvidia-smi) did not answer, and per GPU failsecc.uncorrectable,row-remap.pending,row-remap.failure,pcie.link(a link narrower than its maximum width) andxid.recent(an XID this boot), each with its reason; bcd advertises thenvmlcapability inHelloonly when that data is there.- When every command settled: all succeeded (and
diag.nvmlreportedpassed) makes the nodeadopted, setsAcceptanceFailedtofalse(reasonPassed), refreshes its IAM projection, asks the NodeSpec controller for generation 1 and publishesnode.adopted. OtherwiseAcceptanceFailedturnstruewith the reason (Expired,diag.nvml:<check>,<kind>:failed, …), the node staysadoptingwith theAcceptanceFailedNoSchedule reason, andnode.acceptance.failedcarries the reason and command id.
A failure is sticky: the controller does not retry on its own. Fix the cause, then call
nodes.adopt (compute:nodes:provision), which starts a new attempt. On an
adopted node, nodes.adopt moves it back to adopting, so it leaves placement until the checks pass again.
diag.nvml is the v0.1 check. The diag.dcgm kind comes with the metal module, whose commissioning profile
runs it at level 2 (and the dedicated-root release at level 1); as a pool acceptance check it runs with its
default payload until check arguments (with) are mapped per kind. From level 2, bcd refuses it gpu-busy for a
GPU a hold or a VM is using (539c4ce).
On the node itself, bcd doctor prints one gpu line per GPU with its load, and warns on a pending row remap or MIG
mode change and fails on failed remapping or uncorrectable ECC errors (f9d7af9; see
bcd doctor).
Better Compute is created by Sean Filimon
Last updated
Attestation
The attestors that admit machines (join-token, x509pop, provisioned, discovery, cloud-iid, tpm), what each proves, single-use evidence and two-round joins.
Operating nodes
Cordon, drain, taint and label nodes, understand maintenance, liveness and connectivity, revoke a node's identity, and delete nodes, with the nodes API.