Pools
Pools group the nodes of one site under one GPU model, sharing mode and policy, and offerings make a pool visible to other tenants with a quota.
A is the unit you schedule into and sell from. It lives in one , holds machines with one GPU model, shares its GPUs in one way, and carries the policy its nodes and reservations follow. Every reservation names exactly one pool, and every quota is per pool.
A pool has an owner tenant. For platform capacity that is the root tenant; a customer that brings its
own hardware owns the pools on its custom site. Other tenants see a pool only when its owner
offers it to them, with a quota.
What a pool is made of
Prop
Type
Classes
The class decides the role a machine gets when it joins the pool (see
joining), and only host nodes add schedulable capacity:
| Class | Node role | Used for |
|---|---|---|
metal-host | host | Bare-metal GPU servers running bcd, reserved as holds |
vps-host | host | Hypervisors that run VPS instances |
provider | host | Machines of a provider account, with agent: 'none' |
metal-dedicated | dedicated | Machines metal deploys whole; dedicated nodes add no reservable capacity |
One GPU model
A pool holds one gpuModel, the family name inventory derives from the NVML name (H100 for
H100-SXM5-80GB). Policies, IAM attributes and meters (gpu.h100.hours) all key on it, so a
pool that mixed models would bill and authorize wrongly. Selectors can still narrow within the model by
gpuProduct, vramGiB or any other attribute (placement).
Sharing modes
exclusive(the default): each GPU is one atom, booked whole.mig: GPUs in MIG mode expose their instances asgpu.migatoms.timesliceandmps: each GPU is split intogpu.slotatoms. These modes give no isolation between workloads, so these pools are single-tenant: they can be offered only to their owner.
Fractional GPUs covers the atoms and what a request must ask for in each mode.
Pool policy
policy is a JSON object. Unknown fields are stored as given; these are the fields the code reads:
Prop
Type
The placer reads the overcommit ratio stored on each node_resources row, not the policy. For vps-host pools the
vps module's vps.hosts work writes vcpu, memoryGiB (less the huge page pools) and diskGiB from each host's
inventory with the pool's ratio (default 1, clamped to 0.1 to 100). For every other pool, since 8a7f8eb, capacity's
capacity.hosts reconciler (every 30 seconds) writes vcpu (logical CPUs less hostReserve.vcpu, with the ratio)
and memoryGiB (installed memory less the huge page pools and hostReserve.memoryGiB, with the ratio), and for every
pool hugepages2Mi and hugepages1Gi (the configured pages, never overcommitted). tenant-controlled nodes get no
rows, a row never shrinks below what is allocated, and unchanged rows are not rewritten. GPU reservations book these
rows with their devices (see host resources).
The host policy fields (hugepages, kernelModules, cpu, iommu, gpu, kubernetes, and a sharing mode of
mps or timeslice) become config units in every host's NodeSpec, through provision (see
host configuration). Since 37dcd02 pools.create and
pools.update (when it changes the policy) run the modules' pool-policy validators before writing, and provision
registers one that checks these fields as nodes would get them, so a bad value is INVALID_INPUT at the API; a
validator's failure that is not a compute error reads policy: .... The server adds its own validator for
joinAttestors (1516728), which knows the attestors JoinService runs: join-token and every registered one.
admission.budgetCheck: 'meter' adds a Better IAM
billing.check to every reservation and instance admission in the pool, on top of the IAM policy described in
quotas and usage.
Create a pool
pools.create is authorized as compute:pools:create on the
tenant, and the site must belong to that tenant. Platform pools are therefore created by the root
tenant on its shared or dedicated sites, and a customer creates pools on its own custom site.
const pool = await compute.api.pools.create(rootCredential, {
tenantId: 'root',
siteId: 'site_01k…',
class: 'metal-host',
gpuModel: 'H100',
sharing: 'exclusive',
policy: {
acceptance: [{ kind: 'diag.nvml' }],
maxPending: '6h',
},
idempotencyKey: 'pool-fra1-h100',
});ownerQuotasets the owner's own quota rows. It defaults to{ gpu: 2147483647 }, effectively unlimited whole GPUs. Owners ofmig,timesliceormpspools must addgpu.migorgpu.slotrows themselves, because a missing row grants 0.- With
idempotencyKey, the pool id is derived from the tenant, the caller and the key, as for reservation drafts. A retry returns the same pool; the same key with a different site, class, sharing mode, agent or provider account isCONFLICT. Fleet config uses this to plan pools it has not created yet. class: 'provider'needsproviderAccountIdandagent: 'none'; other classes refuseproviderAccountId. Both areINVALID_INPUT.- A site the tenant does not own is
NOT_FOUND.
The pool starts with an empty rollup and emits pool.created. Machines join it with a join token (see
joining).
Offerings
An is a pool_offers row. It makes the pool exist in another
tenant's view and creates that tenant's quota rows in the same transaction.
await compute.api.pools.offer(rootCredential, {
tenantId: 'root', // the pool's owner, acting
poolId,
toTenantId: 'acme',
quota: { gpu: 32 },
priority: 10,
});pools.offeriscompute:pools:shareon the pool, and only the owner can call it (anyone else getsNOT_FOUND). Offering again updates the row and the quota.quotamaps resources to limits:gpu,gpu.mig,gpu.slot, or a scalar such asvcpu. A resource without a quota row grants 0.includeDescendantsandpriority(-1000 to 1000, default 0) are stored with the offer and round-trip through fleet config. In this release the capacity module matches the offered tenant exactly and the placer does not readpriority.- The offering validator refuses, with
INVALID_STATE, to offer atimesliceormpspool, or a pool that holdstenant-ownednodes, to anyone but its owner. Apolicy.singleTenantpool can be offered to one tenant besides its owner, and never withincludeDescendants. An archived pool cannot be offered either.
The offered tenant then sees the pool in pools.list and
pools.get, each pool with owned, archived and the tenant's own
quota rows:
{
"items": [
{
"id": "pool_01k…",
"ownerTenantId": "root",
"siteId": "site_01k…",
"class": "metal-host",
"gpuModel": "H100",
"sharing": "exclusive",
"agent": "bcd",
"policy": { "acceptance": [{ "kind": "diag.nvml" }], "maxPending": "6h" },
"version": 1,
"owned": false,
"archived": false,
"quotas": [{ "tenantId": "acme", "poolId": "pool_01k…", "resource": "gpu", "max": 32, "used": 4, "available": 28 }]
}
]
}pools.unoffer removes the offer and answers
{ removed: boolean }. It lowers the tenant's quotas on the pool to what is in use, so live
reservations keep their bookings and charge while nothing new fits. Pending reservations of that
tenant fail with PoolGone at their next retry. A pool cannot be unoffered from its owner.
Offering also writes the pool's row in the IAM
for the tenant, with the attributes gpuModel, class, sharing, region and owned, so tenant
policies can decide on them (see actions and resources).
Update and archive
pools.update (compute:pools:manage, owner only) takes a new
policy, a new gpuModel, or both. The policy is replaced as a whole, not merged, so send every field
you want to keep. The update refreshes the pool's authz row for the owner and every offered tenant and
emits pool.updated. Class, site, sharing mode and agent cannot change.
pools.archive (compute:pools:manage, owner only) retires a
pool without disturbing what runs on it:
- every offer is removed, each offered tenant's quotas drop to what is in use, and their authz rows are removed;
- the pool takes no new work: new reservations are
NOT_FOUND, and the placer answerspendingwiththe pool is archived; - placed and active reservations keep their bookings until they end, while pending ones fail with
PoolGoneat their next retry; - only the owner still sees the pool, with
archived: true.
Archiving is idempotent and emits pool.archived with the list of unoffered tenants.
When Better IAM deletes a tenant, compute's tenancy teardown (capacityTenantDeleted) archives the pools
it owns the same way (pool.archived with reason: 'tenant deleted', quotas of the offered tenants
zeroed), withdraws every offer made to it (pool.unoffered to the owner) and ends its reservations. Other
tenants' live bookings in an archived pool run to their end. See
tenancy.
The pools API
| Method | Action | Decided on | Phase |
|---|---|---|---|
pools.list | compute:pools:read | tenant | v0.1 |
pools.get | compute:pools:read | pool | v0.1 |
pools.create | compute:pools:create | tenant | v0.1 |
pools.capacity | compute:capacity:read | pool | v0.1 |
pools.offer | compute:pools:share | pool | v0.1 |
pools.unoffer | compute:pools:share | pool | v0.1 |
pools.update | compute:pools:manage | pool | v0.2 |
pools.archive | compute:pools:manage | pool | v0.2 |
All eight are served today. Events: pool.created, pool.offered, pool.unoffered, pool.updated,
pool.archived, and quota.updated from quotas.set. better-compute pools list --tenant acme
lists pools from the CLI.
Better Compute is created by Sean Filimon
Last updated