better/compute
Capacity

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:

ClassNode roleUsed for
metal-hosthostBare-metal GPU servers running bcd, reserved as holds
vps-hosthostHypervisors that run VPS instances
providerhostMachines of a provider account, with agent: 'none'
metal-dedicateddedicatedMachines 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 as gpu.mig atoms.
  • timeslice and mps: each GPU is split into gpu.slot atoms. 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.

create-pool.ts
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',
});
  • ownerQuota sets the owner's own quota rows. It defaults to { gpu: 2147483647 }, effectively unlimited whole GPUs. Owners of mig, timeslice or mps pools must add gpu.mig or gpu.slot rows 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 is CONFLICT. Fleet config uses this to plan pools it has not created yet.
  • class: 'provider' needs providerAccountId and agent: 'none'; other classes refuse providerAccountId. Both are INVALID_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.

offer.ts
await compute.api.pools.offer(rootCredential, {
  tenantId: 'root', // the pool's owner, acting
  poolId,
  toTenantId: 'acme',
  quota: { gpu: 32 },
  priority: 10,
});
  • pools.offer is compute:pools:share on the pool, and only the owner can call it (anyone else gets NOT_FOUND). Offering again updates the row and the quota.
  • quota maps resources to limits: gpu, gpu.mig, gpu.slot, or a scalar such as vcpu. A resource without a quota row grants 0.
  • includeDescendants and priority (-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 read priority.
  • The offering validator refuses, with INVALID_STATE, to offer a timeslice or mps pool, or a pool that holds tenant-owned nodes, to anyone but its owner. A policy.singleTenant pool can be offered to one tenant besides its owner, and never with includeDescendants. 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:

pools.list, as acme sees it
{
  "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 answers pending with the pool is archived;
  • placed and active reservations keep their bookings until they end, while pending ones fail with PoolGone at 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

MethodActionDecided onPhase
pools.listcompute:pools:readtenantv0.1
pools.getcompute:pools:readpoolv0.1
pools.createcompute:pools:createtenantv0.1
pools.capacitycompute:capacity:readpoolv0.1
pools.offercompute:pools:sharepoolv0.1
pools.unoffercompute:pools:sharepoolv0.1
pools.updatecompute:pools:managepoolv0.2
pools.archivecompute:pools:managepoolv0.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.

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page