@better-compute/inventory
v0.3.0
Published
Hardware inventory for better-compute: section hashing, diffs, GPU model mapping, labels and health conditions.
Maintainers
Readme
@better-compute/inventory
Hardware inventory for better-compute: GPU naming, inventory sections, snapshots and diffs, devices and atoms, labels, node conditions and GPU health, plus the always-on inventoryModule.
Install
npm install @better-compute/inventoryIt is also included in the umbrella package as better-compute/inventory:
npm install better-computeUsage
import { normalizeGpuProduct } from '@better-compute/inventory';
normalizeGpuProduct('NVIDIA H100 80GB HBM3'); // { gpuProduct: 'H100-SXM5-80GB', gpuModel: 'H100' }
normalizeGpuProduct('NVIDIA GH200 480GB'); // { gpuProduct: 'GH200-480GB', gpuModel: 'other' }inventoryModule is the built-in module the server registers before any injected module, so a control plane built with betterCompute() already has it. The package depends on @better-compute/core and @better-compute/protocol.
Inventory reports come from bcd, the Go node daemon, through the gateway. bcd is built and tested against simulated and fake hardware but is not released as a binary yet, and nothing here has run against real GPU hardware.
What it covers
- GPU naming, section verification, diffs, devices and atoms, labels, the hardware key, and the conditions.
- The Inventory reconciler.
inventory.get,inventory.history(snapshots newest first, paged by snapshot id) andinventory.diff(any two snapshots by id or hash; by default the latest against the one before).- Health:
xidPoliciesandrecordXid(48 quarantines the GPU; 79 and 95 make the node NoSchedule for a reboot; 63 and 64 cover row remaps). Inventory findings (a failed or pending row remap, uncorrectable ECC, a degraded NVLink) are applied on every inventory. Device quarantines live indevice_health: the atom leaves placement and the node does not. Remediation runs in provision (healthwork). - Fractional atoms:
gpu.slotatoms<uuid>/s<k>fortimeslice/mpspools (frompolicy.sharing.replicas); MIGgpu.migatoms;partitionPendingfreezes a GPU during a MIG mode change;gpuAtomFamilyfeeds the capacity atom rule. - The hygiene sweep (
inventory.hygiene, a periodic singleton ininventoryModule, every 5 minutes):DriverMismatch(a converged node whose GPU driver branch differs from itsnvidia-driverunit),CertExpiring(the newest identity certificate has under a tenth of its life left, at least an hour: renewal is failing) andDaemonOutdated(an older protocol, orminDaemonVersion). Each is NoSchedule plus anode.condition.changedevent, cleared with its cause. - Host pressure from
StatusReport.pressure(Linux PSI and CPU steal):CpuPressure,MemoryPressure,DiskPressurewith hysteresis, NoSchedule onvps-hostpools and alert-only elsewhere (recordHostPressure; the gateway'sxidStatusHandlercalls it). - Image caches: a node's
imagessection keepsnode_image_cachein step for theimage-cachedscorer (imageCacheNodes). - Atom health edges wake the node's started reservations (
wakeStartedReservations, work kindcapacity.reservations), so capacity's Degraded conditions follow at once.
From Hello to devices
Report. In the Hello transaction, and on every heartbeat that carries hashes, the gateway calls
recordInventoryHashes(tx, work, nodeId, hashes). It stores the hashes innode_inventoryand returns the sections to pull (Welcome.request_sections): those whose stored bytes have a different hash.recordInventoryHashesManydoes a whole batch of reports in one statement (the gateway's group commit), one result slot per report.Pull. For each
InventorySectionframe the gateway callsingestInventorySection(tx, work, nodeId, section). The section is checked against its own SHA-256 over the received bytes (INVALID_INPUTotherwise) and never re-encoded. It is stored innode_inventory_sections, and work kindinventoryis queued for the node.ingestInventorySections(tx, work, entries)stores a batch of many nodes' sections in one statement, with one result slot per entry.Apply.
inventoryReconcilerrunsapplyInventory(sql, nodeId, { events })in one transaction with the node row locked. Once every reported section's bytes are present and the digest differs from the last applied one, it:- appends an
inventory_snapshotsrow (decoded sections as JSON, bigints as strings); - plans devices with
planDevices. GPUs becomegpuatoms, and a MIG GPU becomes a non-bookable parent ofgpu.migatoms. A vanished device that still has live bookings is kept withhealthy = falseand deleted once its bookings end. A device id another node owns is left alone and raisesDuplicateIdentity; - replaces the
inventory-sourced labels (gpu.product,gpu.model,gpu.count,cuda.driver.major,mig.capable,fabric.domain, …); - writes
GpuCountMismatch,GpuModelUnknownandDuplicateIdentityintonode_conditions, and mirrors the true ones intonodes.no_schedule_reasonsas bare type names (keeping every entry it does not own); - sets
hardware_key. It is trusted with two or more independent identifiers, and a clash on the unique index is caught in a savepoint and becomesDuplicateIdentity; - publishes
node.inventory.changedplus onenode.<change>event per diff entry.
A
tenant-controllednode (a dedicated-root lease: the lessee has root) is advisory. Its set is appended as a snapshot and announced asnode.inventory.changedwithadvisory: true, and nothing else changes: devices, health, labels, conditions, the image cache and the hardware key keep the platform's last view, andrecordXidandrecordHostPressureignore its reports. Otherwise a GPU the lessee reports missing or unhealthy would leave the lease's bill. The applied digest is stored asadvisory:<digest>. When metal ends the lease it callsendAdvisoryInventory, so the next report applies the same set under platform trust.- appends an
Pure building blocks: verifySection, sectionsToRequest, mergeSections, inventoryDigest, devicesFromGpus, gpuLinks (undirected GPU pairs for placement), planDevices, inventoryLabels, inventoryConditions, mergeNoScheduleReasons, diffInventory, hardwareKey / hardwareKeyFromClaims (join claims hash exactly like inventory).
GPU naming
normalizeGpuProduct(nvmlName)returns{ gpuProduct, gpuModel }.- Before lookup the name is canonicalized: upper case, punctuation collapsed to
-, and a leadingNVIDIAdropped (older drivers omit it). gpuProducthas the form<MODEL>-<FORM>-<MEMORY>GB.gpuModelis the family that pools, policies and meters use.- A name missing from the table keeps its canonical form as
gpuProductand maps togpuModel: 'other', which raisesGpuModelUnknown. - Product names normalize to themselves, so the function is idempotent.
- Before lookup the name is canonicalized: upper case, punctuation collapsed to
gpuModelsis the versioned, JSON-serializable mapping table. Adding or renaming a product bumpsgpuModels.version.gpuModelFamilies:H100,H200,B200,GB200,A100,L40S,L4,RTX-PRO-6000, and since version 2GH200,L40,A40,A30,A10,RTX-6000-ADA,RTX-A6000,RTX-5090,RTX-5080,RTX-4090.
| 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, L4 | L40S-PCIE-48GB, 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 |
Other exports
inventoryModuleis aComputeModule<'inventory', InventoryApi>holding theinventoryAPI group (get,history,diff), each authorized ascompute:nodes:readon the node.- Its methods validate
{ tenantId, nodeId }strictly. - It adds no catalog fragment, because its actions are already in compute's own catalog.
- Its methods validate
- Condition types:
nodeConditionTypesandisNodeConditionType: the node condition types.inventoryChangeTypes: the events an inventory diff emits.
- Protocol re-exports:
NodeInventoryand its section types (GpusSection,Gpu, …), re-exported from@better-compute/protocolas types only.
