How jobs are scheduled
When you queue a batch job — a mesh export, a shader build, a study run — something has to decide whether this machine starts it now, later, or at all. This page explains what that decision is made of, because four parts of it are easy to guess wrong.
A request and a limit are different questions
Every job can declare two numbers for each resource it uses, and they are not two names for the same thing.
A request is what must be free before the job starts. It is reserved for the whole life of the job and returned when the job finishes. If you request two cores, two cores are set aside for you, and nothing else gets them until you are done.
A limit is the ceiling above which the job is considered to be misbehaving. Nothing is reserved for it. It is a number the system measures your job against while it runs.
The practical difference: raising a request makes your job harder to place, because more has to be free before it can start. Raising a limit does not — it changes only what counts as too much once the job is already running.
A limit is not enforced
This surprises people who have used container schedulers, so it is worth being plain about it.
The system schedules against your limit and measures your actual use. It does not stop a job that exceeds its limit. Nothing is killed, throttled or sandboxed.
The reason is portability. Real enforcement needs operating-system mechanisms — cgroups, job objects, rlimits — that differ on every platform this project runs on, and behave differently again on Android. Honest declarations plus a hard admission budget keep a machine usable, and they do it identically everywhere.
What you get instead is visibility. A job over its limit is marked in the job detail view, and the system records what each job type declared against what it actually used. A job type that consistently asks for four times what it needs wastes three quarters of a reservation on every single run — and that is invisible without the record. The system reports the drift. A person fixes it by editing a number.
A service class is derived, never chosen
You do not pick a priority class. It follows from what you declared:
| You declared | Class | What it means |
|---|---|---|
| Every resource has a request, and each request equals its limit | Guaranteed | Placed only where the full amount is free, and never deferred once admitted |
| Some request, and at least one below its limit | Burstable | Placed on the request, may use more up to the limit |
| Nothing at all | BestEffort | Placed wherever there is room, deferred first when the machine is busy |
There is no setting for this and no override. The class is a consequence, so the way to change it is to change what you declare.
The job detail shows all three together — what the job asked for, its ceiling, and the class that follows:
Declaring nothing is not a mistake. Every batch job that exists in OctoMY today declares nothing, and BestEffort is the right class for them. A thumbnail render should yield to a two-hour solve.
Deferring is not starving
Under pressure the system admits Guaranteed work before Burstable, and Burstable before BestEffort. On its own, that would mean a steady stream of Guaranteed jobs keeps a BestEffort job waiting forever.
So a waiting job's effective priority rises with how long it has waited, up to a ceiling. An old BestEffort job outranks a fresh Guaranteed one. It never outranks everything forever. And a job whose wait passes a threshold is surfaced in the interface, because starvation that is merely slow is still starvation.
A resource is named by the job, not by the system
The scheduler does not know what a core is.
octomy.io/cpu-milli and octomy.io/memory-bytes ship as conventions, and no code path treats them specially. If your job type measures its capacity in solver licences, camera streams or hashes per second, you declare that name and the scheduler honours it from the first run:
example.com/su2-processes 16
banter.io/hashes-per-second 500000
Three rules come with this, and each exists because of a specific failure:
- Names carry a domain.
cfd-coresis rejected;example.com/cfd-coresis accepted. Without the domain, the first two consumers to pick the same obvious word silently share one counter that means two different things to them. - Quantities are whole numbers. This is why the convention is
cpu-millirather thancpu— you express half a core as500, not0.5. A scheduler comparing floating-point capacities has a class of rounding bug that integers simply do not have. - Capacity is never overcommitted. If a node offers two of something and two jobs each hold one, a third job asking for one does not schedule there. The system does not gamble that a job will use less than it asked for.
What a node offers
A machine does not have to offer all of itself. It states a mode, and the mode scales what it advertises:
| Mode | For |
|---|---|
Interactive |
Someone is using this machine right now |
Background |
An idle desktop that still belongs to a person |
Drone |
A machine that exists to run jobs |
Off |
Accepts nothing |
Two things make this more useful than it first looks.
The mode is a table, not a number. Interactive gives up half the cores and a quarter of the memory, and offers no GPU at all — a single percentage could not say that.
And a node answers separately for its own work and for other nodes' work. A workstation can run its own studies flat out while lending nothing to anybody, which is one setting rather than a special case, and is what most people actually want.
Changing the mode never disturbs a job that is already running. It changes what is admitted next.
Further reading
- Declaring job resources — the naming rules and the shipped conventions
- Share a machine with the fleet — setting modes, and what each one gives up
- Find out why a job is not running — reading the three kinds of
Pending