# Flavors (/docs/guides/vps/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
flavor 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 instance. 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 [#define-a-flavor]

The pool's owner creates flavors with [flavors.create](/docs/reference/api/flavors#create), decided as
`compute:flavors:manage` on the pool:

```ts
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 }
```

<TypeTable
  type="{
  tenantId: { type: 'string', description: &#x22;The pool owner's tenant. Other tenants get NOT_FOUND.&#x22;, required: true },
  poolId: { type: 'string', description: 'A pool of class vps-host or provider. Any other class is INVALID_STATE.', required: true },
  name: { type: 'string', description: '1 to 63 lower-case letters, digits, dots, underscores or hyphens, starting with a letter or digit. Unique in the pool.', required: true },
  vcpu: { type: 'number', description: 'Whole vCPUs, 1 to 1024.', required: true },
  memoryGiB: { type: 'number', description: 'GiB of memory, 1 to 16384.', required: true },
  diskGiB: { type: 'number', description: 'GiB of disk, 1 to 65536.', required: true },
  gpus: { type: 'number', description: 'Whole GPUs passed through to the VM, 0 to 16.', default: '0' },
  offerId: { type: 'string', description: &#x22;Provider pools: the provider offer this flavor creates, 1 to 128 characters. Required there; hosted pools ignore it.&#x22;, },
  placement: { type: &#x22;{ numaAffine?: boolean; pinned?: boolean; hugepages?: '2Mi' | '1Gi' }&#x22;, description: 'Hosted pools only (INVALID_INPUT on a provider pool): where the guest lives on its host. Sandbox templates on the flavor inherit it. See placement below.' },
}"
/>

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:

```bash
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](#update-and-delete)).

You can also declare flavors in a [FleetConfig](/docs/guides/fleet-config) document, next to the pools they belong
to, and let `fleet apply` call `flavors.*` for you:

```yaml title="fleet.yaml"
flavors:
  - { pool: vps-fra, name: g1.small, vcpu: 2, memoryGiB: 4, diskGiB: 40 }
```

## List flavors [#list-flavors]

[flavors.list](/docs/reference/api/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 [#flavors-on-hosted-pools]

In a `vps-host` pool, capacity places the flavor on one host of the pool as an open-ended
allocation (it lasts until the instance is deleted) with:

* **scalar claims** `vcpu`, `memoryGiB` and `diskGiB`, the flavor's numbers;
* **devices** `{ class: 'gpu', count: gpus }`: whole GPU atoms, 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](/docs/guides/capacity/placement) and [quotas and usage](/docs/guides/capacity/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](/docs/guides/iam/billing-and-usage).

### Placement [#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](/docs/guides/capacity/reservations#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](/docs/daemon/delivery#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 [`cpu` unit](/docs/daemon/host-config#cpu).
* **Only where bcd applies it.** An instance whose flavor has a `placement` is placed only on hosts labelled
  `bc.dev/cap.instance.placement` (82f4f51), which a bcd that applies `InstanceSpec.placement` advertises: 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.memoryGiB` is 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](/docs/daemon/delivery#memory-ballooning).

## Flavors on provider pools [#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:

```ts
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' },
);
```

```bash
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 cx22
```

Provider instances have no allocation and are not metered: the provider bills you. See
[providers](/docs/guides/vps/providers).

## The shape an instance keeps [#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](/docs/reference/api/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:

1. charges the growth to quota (`QUOTA_EXCEEDED` if it does not fit);
2. 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 guarded
   `allocated + Δ <= floor(allocatable × ratio)` update, shrinkage returns room;
3. on a conflict, refunds the growth and fails with `CAPACITY_UNAVAILABLE`, `details.resource` naming what ran out;
4. 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 [#update-and-delete]

[flavors.update](/docs/reference/api/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](/docs/reference/api/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 [#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](/docs/extending/steps-and-profiles)). `vps()` does not read
profiles in this release: instances always use the pool's flavor rows described here.
