Scheduling and placement
How the orchestrator picks a host for an environment
By default you never name a host. You describe what the workload needs, and the orchestrator picks a host that can satisfy it. This page explains how that choice is made, and just as importantly, what it does not consider.
Constraining placement
A self-hosted operator who knows their boxes by name can constrain the choice
with a Fusefile placement block:
placement:
host: build-3 # exact host id
labels: # every pair must match the host's declared labels
disk: nvme
tier: build
fuse environment create --host build-3 --label disk=nvme is the imperative
equivalent.
Both fields are hard gates, not preferences. They narrow the candidate set
before scoring; they never override the gates below. A pinned host still has to
be active, run the right backend, and have room, and a request that matches no
host is rejected now rather than queued.
Labels come from the operator at registration
(fuse host register build-3 --label disk=nvme) and are never probed from the
host agent, the same trust model as --gpu-kind.
A pin is a constraint, not a reservation: two environments pinned to the same host draw down the same capacity, and the second fails if it no longer fits.
fuse host <id> is still not pinning. It sets a client-side scope that
filters what fuse environment list shows you, nothing more, and create says so
when you have one set:
note: create is orchestrator-scheduled; the active host "gpu-1" does not pin placement (pass --host gpu-1 to pin it)
Forks are an exception to all of this: a fork is already pinned to its source’s
host (the seed snapshot’s rootfs is host-local) and bypasses the scheduler, so it
does not inherit the source’s placement.
The filter chain
Every registered host is tested against these gates, in order. A host that fails any of them is out; there is no partial credit and no fallback tier.
| Gate | Rule |
|---|---|
| Host pin | If the spec names a host_id (placement.host), only that host is considered. An unknown id is a 404 before any VM is created; a real but ineligible host fails with the gate that rejected it. |
| State | The host must be active. Both cordoned and draining are excluded. |
| Region | If the spec names a region, the host’s region must match it exactly. An empty region disables the gate rather than preferring anything. |
| Arch | If the spec names a CPU architecture (amd64 or arm64; the uname spellings x86_64/aarch64 are accepted and normalized), the host’s arch must match it. An empty spec arch disables the gate. A host with no reported arch counts as amd64 — every host registered before the field existed was x86_64, so old records stay schedulable without a migration. |
| Backend | A spec asking for GPUs is only placed on a qemu host. |
| Labels | If the spec names labels (placement.labels), every pair must match the host’s declared labels. An empty selector disables the gate. |
| Capacity | The host must have room, see below. |
Region lives under resources, not placement, for historical reasons: it
predates the block and a second spelling of the same selector would be worse than
the split.
The backend gate is defensive. Registration already refuses GPU capacity on a Firecracker host, so this second check exists to catch a stale or hand-edited host record rather than to do routine work.
What “has room” means
Capacity is checked as capacity - allocated on four dimensions, always:
- CPUs, RAM, and storage must each cover the request.
- The host must have at least one free VM slot.
GPUs are only considered when the spec asks for them, and the check takes one of three forms depending on what the host reported:
- A MIG request (a
gpu_profileis set) draws from the MIG instance pool for that profile, and then applies its own kind gate. On a host that reports per-instance inventory (mig_instances), the gate is per-instance: each instance carries its ownkindandparent_gpu_uuid, so the request is matched to a free instance whose card is MIG-capable and a kind match, both of the same card, and the orchestrator binds that specific UUID to the VM. A host that reports no per-instance inventory falls back to the count-map path, wheremig_profilesis the scheduling unit and the kind gate is host-level (the host must report a MIG-capable device of the requested kind). The MIG pool and the whole-device pool are independent, so a slice never consumes a card. A host reporting no per-device inventory falls back to its scalargpu_kind, and one declaring no kind at all cannot be ruled out here. - A host reporting per-device inventory counts free devices by UUID and filters them by kind, which is what lets a host with mixed GPU models answer correctly.
- A legacy host reporting only a scalar count subtracts counts and compares
gpu_kindas an exact, case-sensitive string.
That last difference is worth internalizing. There are exactly two kind-matching rules in the scheduler:
- Per-device hosts (and MIG requests, which share the same matcher) match
gpu_kindas a case-insensitive substring of each device’s model, soa100matchesNVIDIA A100-SXM4-40GB. When a device reports no model at all, and only then, the host’s scalargpu_kindstands in for it as a case-insensitive equality check. A host that reports neither is not ruled out. - Legacy scalar hosts compare
gpu_kindagainst the host’s scalar kind as an exact, case-sensitive string, whicha100againstNVIDIA A100-SXM4-40GBwould not satisfy.
The same Fusefile can therefore place differently depending on how the host reports its inventory.
The host scalar is deliberately only a fallback, never an override. It is a
single label for the whole machine, so consulting it for a device that reported
its own model would make the match device-independent, and a MIG request needs
mig_capable and the kind match to hold of one card rather than of two
different ones.
Choosing among the survivors
Placement is not first-fit. Every host that clears the filters is scored:
score = free_cpus * 10000 + free_ram_mb
CPUs dominate, because they are the scarcest resource in practice, and free RAM breaks ties between hosts with equal CPUs. The fleet’s placement policy then picks from the scored set:
- Spread (the default) takes the highest score, the least-loaded host.
- Binpack takes the lowest score, packing work onto hosts that are already busy so others stay empty.
Hosts are held in a map, so the order they are considered in varies between runs. Placement is still deterministic, because every comparison breaks ties on the host ID rather than on arrival order. Two identical requests against an unchanged fleet land on the same host.
What the scheduler ignores
The list of what is not consulted is the more useful half of the model:
- Host liveness.
last_seenis recorded at registration and never refreshed, and no gate reads it. A host whose agent died an hour ago is still a placement candidate until someone cordons it. - Real utilization. Every number is the orchestrator’s own bookkeeping against what it has allocated. Nothing polls the host to ask what it is actually doing, so work started outside Fuse is invisible.
- Capacity drift. A host’s capacity is recorded when it registers and never re-probed.
- Network topology, IOMMU groups, image availability, and tenant quotas are not modeled at all.
When nothing fits
A request that clears no host fails with no host has sufficient capacity,
surfaced as a 503. Fuse does not queue the request or wait for room to appear,
a placement either succeeds now or is rejected now.
Placement failures are reported distinctly rather than as capacity shortfalls:
- An unknown
placement.hostis a404(host ... is not registered), raised at the API boundary before a VM row exists. - A pin to a real but ineligible host is a
503naming the host and the gate (host "build-3" is cordoned, not active). - A label selector that matches nothing is a
503reporting the selector and how many hosts were considered (disk=nvme,tier=build matched none of 4 schedulable hosts). - A pinned or labelled host that matches but has no room falls back to the normal capacity message: that really is a capacity problem.
A GPU request made when no GPU host is registered at all fails differently, with
gpu workloads require a registered gpu host, rather than reporting a capacity
shortfall on hosts that could never have served it. A placement request with no
hosts registered at all fails with placement requires registered hosts, because
the legacy single-provider path is not a scheduled host.
Read next
- Hosts for what capacity is probed versus declared.
- Providers for the backend split the GPU gate depends on.
- The reconcile loop for what happens to an environment after it is placed.