Flavors
Named instance shapes per VPS pool, and how each becomes scalar claims, GPU atoms and quota on a host, or a provider offer.
Tenants should not have to know how many vCPUs a host has or what a provider calls its server types. A
is a named shape, such as g1.small or gpu1.l40s, that the pool's owner defines
and tenants pick by name when they create an . The pool decides what the name
means: in a hosted pool it becomes claims on one host, in a provider pool it names the provider's offer.
Flavors belong to one pool. The same name in two pools can mean two different shapes.
Define a flavor
The pool's owner creates flavors with flavors.create, decided as
compute:flavors:manage on the pool:
await compute.api.flavors.create(
{ token },
{ tenantId: 'root', poolId: 'pool_01jz…', name: 'gpu1.l40s', vcpu: 8, memoryGiB: 32, diskGiB: 200, gpus: 1 },
);
// → { id: 'flv_…', poolId: 'pool_01jz…', name: 'gpu1.l40s', vcpu: 8, memoryGiB: 32, diskGiB: 200,
// gpus: 1, archived: false, version: 1 }Prop
Type
A second flavor with the same name in the pool is CONFLICT. A provider pool flavor without offerId is
INVALID_INPUT.
The CLI's flavors commands (d1e83ca) take the same fields as flags:
better-compute flavors create --tenant root --pool pool_01jz… --name gpu1.l40s \
--vcpu 8 --memory-gib 32 --disk-gib 200 --gpus 1
better-compute flavors list --tenant root --pool pool_01jz…
better-compute flavors update --tenant root --pool pool_01jz… --name gpu1.l40s --vcpu 8 --memory-gib 64 --disk-gib 200 --gpus 1
better-compute flavors delete --tenant root --pool pool_01jz… --name gpu1.l40s--offer is the provider offer of a provider pool's flavor. flavors update sends every flag again, as the method
expects (see update and delete).
You can also declare flavors in a FleetConfig document, next to the pools they belong
to, and let fleet apply call flavors.* for you:
flavors:
- { pool: vps-fra, name: g1.small, vcpu: 2, memoryGiB: 4, diskGiB: 40 }List flavors
flavors.list (compute:flavors:read) returns { items }, sorted by name, to
the pool's owner and to every tenant the pool is offered to. That is what an instance form loads when a pool is
chosen. Archived flavors are not listed.
Flavors on hosted pools
In a vps-host pool, capacity places the flavor on one host of the pool as an open-ended
(it lasts until the instance is deleted) with:
- scalar claims
vcpu,memoryGiBanddiskGiB, the flavor's numbers; - devices
{ class: 'gpu', count: gpus }: whole GPU , passed through with VFIO and booked like any other GPU booking.
A host fits when, for every scalar, allocated + claim <= floor(allocatable × ratio) on its node_resources row.
ratio is the overcommit: with allocatable 16 vCPU and ratio 2, a host takes 32 vCPUs of flavors. GPUs are never
overcommitted. A host with no row for a scalar the flavor asks for is not a candidate.
The vps.hosts work (every 30 seconds, vps({ hostSyncEvery })) derives those rows from each host's applied
inventory, for nodes of vps-host pools that are not tenant-controlled:
| Scalar | allocatable |
|---|---|
vcpu | logical CPUs (cpu.threads) minus the reserve |
memoryGiB | installed memory less the host's huge page pools, in whole GiB, minus the reserve (9d55b36) |
diskGiB | non-removable disks in whole GiB minus the reserve |
The reserve is what a host keeps for itself, vps({ hostReserve }), by default 2 vCPU, 8 GiB and 100 GiB. ratio is
the pool's policy.overcommit[resource] (default 1, clamped to 0.1 to 100). Rows are written only when they change,
and a shrinking host never drops allocatable below what is already allocated. Until a host has reported inventory
it has no rows and is not a candidate; doctor warns vps.hosts.capacity for adopted hosts in that state.
The same shape is what the instance counts against the tenant's quota in the pool: vcpu, memoryGiB, diskGiB,
and gpu when gpus is above 0. The placement charges it, and deleting the instance returns it. See
placement and quotas and usage.
Hosted instances are metered from placement until their allocation is released: vps.vcpu.hours at vcpu per hour,
vps.memory.gib.hours at memoryGiB per hour, and for GPU flavors gpu.<model>.hours at gpus per hour, where the
model is the pool's gpuModel in lower case. The vps.usage job writes the windows every 5 minutes; see
billing and usage.
Placement
A flavor's placement (9d55b36, migration vps/0018_placement) says where its guests live on their host:
| Field | Effect |
|---|---|
hugepages: '2Mi' | '1Gi' | Guest memory is backed by huge pages of that size, never ballooned. The instance books the host's hugepages1Gi (one per GiB) or hugepages2Mi (512 per GiB) row instead of memoryGiB; the capacity module keeps those rows from each host's configured pools (see host resources). Quota still charges it as memoryGiB |
numaAffine: true | vCPUs and memory on one NUMA node: the node of the instance's GPUs, or without GPUs the least-loaded node with room for the memory |
pinned: true | Each vCPU gets its own host CPU that no other pinned guest on the host has |
After capacity places the instance on a host, vps chooses the host placement from the host's inventory (its NUMA
nodes, its GPUs' NUMA nodes) and the other guests' placements, stores it in instances.host_placement, and sends it
to the host as InstanceSpec.placement (cpus, mems, hugepageSizeBytes), which bcd maps onto libvirt-kvm or
cloud-hypervisor (see instances). Placements on one host take turns on its vcpu
row, so the second sees the first's pinned CPUs. A host that cannot honour numaAffine or pinned still runs the
guest, floating, and the instance carries a PlacementDegraded condition naming why: NoNumaTopology,
NoNumaNodeFits, GpuNumaUnknown, NumaMemoryTooSmall or NotEnoughFreeCpus. Huge pages are never degraded:
they were booked, and bcd's own admission refuses a start the free pages cannot back.
- The host's own CPUs. Since b6b804e a guest's CPUs never include the host's reserved CPUs, the lowest
non-isolated,
policy.hostReserve.vcpu(default 2), whether pinned or floating on a NUMA node. - Isolated CPUs first. A pinned guest takes the CPUs the kernel isolates first (no host tasks or timer ticks
there), then the rest by id. Isolate them with the pool's
cpuunit. - Only where bcd applies it. An instance whose flavor has a
placementis placed only on hosts labelledbc.dev/cap.instance.placement(82f4f51), which a bcd that appliesInstanceSpec.placementadvertises: an older bcd would back a huge-page guest with memory placement never booked. - A balloon on overcommitted pools (073324a). A pool whose
policy.overcommit.memoryGiBis above 1 gives every guest a virtio balloon, so the host can reclaim the guests' free memory, except huge-page-backed and GPU (VFIO) guests, whose memory is pinned. An older bcd drops the field harmlessly, so no capability label is needed for it. Since 12e9ced bcd inflates those balloons only while the host is short of memory (under 10% available, or a memory stall of 10% or more), idle guests first, never below a guest's floor ([instances] balloon_floor_percent, default 50, or its use plus 10%), and deflates them once more than 20% is free again; host admission still charges each guest its full memory. See memory ballooning.
Flavors on provider pools
In a provider pool the flavor's offerId is what the reconciler passes to ProviderImpl.create. Its vcpu,
memoryGiB, diskGiB and gpus are what the tenant is charged against quota at admission, and what policies
see; nothing checks them against the offer. Copy them from the offer:
const { items } = await compute.api.providerAccounts.offers(
{ token },
{ tenantId: 'root', accountId: 'pacct_…', query: { kind: 'vps', minVcpu: 2 } },
);
// [{ id: 'cx22', kind: 'vps', region: 'fsn1', vcpu: 2, memoryGiB: 4, diskGiB: 40, pricePerHour: 0.0071, currency: 'EUR', available: true }, …]
await compute.api.flavors.create(
{ token },
{ tenantId: 'root', poolId: providerPool, name: 'cx22', vcpu: 2, memoryGiB: 4, diskGiB: 40, offerId: 'cx22' },
);better-compute provider-accounts offers pacct_01jz… --tenant root --kind vps
better-compute flavors create --tenant root --pool pool_01jz… --name cx22 --vcpu 2 --memory-gib 4 --disk-gib 40 --offer cx22Provider instances have no allocation and are not metered: the provider bills you. See providers.
The shape an instance keeps
An instance stores a copy of its flavor's shape when it is admitted. Changing or archiving the flavor later never changes a live instance: its claims, quota and policy attributes stay what they were. To move an instance to another shape, resize it.
instances.resize works on stopped hosted instances whose GPU count stays the same. It computes the difference per resource between the old shape and the new one, then:
- charges the growth to quota (
QUOTA_EXCEEDEDif it does not fit); - calls capacity's
resizeAllocation(tx, allocationId, { vcpu, memoryGiB, diskGiB }), which changes the allocation's scalar claims in place on its node, all or nothing: growth takes the same guardedallocated + Δ <= floor(allocatable × ratio)update, shrinkage returns room; - on a conflict, refunds the growth and fails with
CAPACITY_UNAVAILABLE,details.resourcenaming what ran out; - otherwise refunds the shrinkage and stores the new shape.
The allocation never moves to another host, and GPU atoms are never added or removed: a different GPU count is
UNSUPPORTED, and so is a flavor with a different placement and resizing a provider instance. Create a new
instance instead. A resize with the same placement chooses the host placement again for the new shape.
Update and delete
flavors.update takes the same input as create and replaces the shape. Every
field is required again except gpus, which falls back to 0 when you leave it out, and offerId, which keeps its
value when you leave it out. Updating an archived flavor also restores it. A missing flavor is NOT_FOUND.
flavors.delete returns { deleted, archived }:
- a flavor no instance ever used is deleted:
{ deleted: true, archived: false }; - a flavor any instance row references, including deleted instances, is archived instead:
{ deleted: false, archived: true }.
An archived flavor is hidden from flavors.list, and new instances and resizes that name it are NOT_FOUND.
Instances already using it keep running.
Flavors in profiles
A VPS profile written with defineProfile({ target: 'vps', runtime, flavors, network }) validates a map of flavor
names to shapes as well (see steps and profiles). vps() does not read
profiles in this release: instances always use the pool's flavor rows described here.
Better Compute is created by Sean Filimon
Last updated
Sandboxesbeta
Short-lived isolated VMs from per-tenant templates, with a time-to-live, warm pools, snapshots, exec over SSH certificates and one IAM decision per sandbox.
Providersbeta
Provider accounts, the defineProvider contract, the Hetzner Cloud and Proxmox VE providers, conformance cases, and secret references for credentials.