Skip to content
Fuse
Esc
navigateopen⌘Jpreview
On this page

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_profile is 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 own kind and parent_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, where mig_profiles is 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 scalar gpu_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_kind as 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_kind as a case-insensitive substring of each device’s model, so a100 matches NVIDIA A100-SXM4-40GB. When a device reports no model at all, and only then, the host’s scalar gpu_kind stands in for it as a case-insensitive equality check. A host that reports neither is not ruled out.
  • Legacy scalar hosts compare gpu_kind against the host’s scalar kind as an exact, case-sensitive string, which a100 against NVIDIA A100-SXM4-40GB would 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_seen is 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.host is a 404 (host ... is not registered), raised at the API boundary before a VM row exists.
  • A pin to a real but ineligible host is a 503 naming the host and the gate (host "build-3" is cordoned, not active).
  • A label selector that matches nothing is a 503 reporting 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.

  • 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.

Was this page helpful?