Idle-Fill Scheduling#
A quota is a ceiling on what a team may claim, not a rule that capacity should sit empty when nobody wants it. Without idle-fill, a job that has hit its QOS group node quota waits even when nodes are idle, and those node-hours are simply lost — the job is not waiting for a resource, it is waiting for a number.
Idle-fill lets such a job run anyway, on capacity no job with a quota claim wants. A run placed this way is borrowed: it consumes no quota while it runs, and it is reclaimed when a job that does hold a claim needs the nodes.
The feature is off by default. Enable it with [scheduler] idle_fill_enabled.
Important
Enabling idle-fill narrows a documented preemption guarantee. Reclaim
ignores preempt_mode, so on a partition with preempt_mode = "off" a
borrowed job can still be evicted. Read What the guarantee becomes before
enabling it on a cluster whose users rely on that promise.
Configuration#
[scheduler]
idle_fill_enabled = true # default false
idle_fill_exempt_secs = 60 # default 60
idle_fill_max_borrow_factor = 2.0 # default 0.0, unbounded
idle_fill_max_cluster_fraction = 0.25 # default 0.0, unbounded
idle_fill_enabledThe master switch. With it off, nothing in this page applies and no job is ever stamped as borrowed.
idle_fill_exempt_secsHow long a borrowed job runs before it may be reclaimed. This is the only guard between a borrowed job and reclaim, so it is deliberately short — order of a minute — and it is capped. The window doubles on each successive eviction of the same job, up to one hour, so a job that keeps losing its node backs off rather than being lent and evicted indefinitely.
idle_fill_max_borrow_factorA ceiling on what one QOS may hold on loan, as a multiple of that QOS’s own group node cap.
2.0against a cap ofnode=4allows 8 borrowed nodes.idle_fill_max_cluster_fractionA ceiling as a share of the registered nodes. Not redundant with the factor: a multiple of a large quota can still swallow the cluster.
Both default to 0.0, meaning unbounded, so an already tuned cluster behaves as
before. When both are set the tighter wins, and fractions floor, so a ceiling never
overshoots. Each is consulted only where a new loan is admitted, which makes them
one-directional — lowering a ceiling refuses the next borrow and never evicts a run
that is already borrowing.
Without either, borrowing is bounded only by idle capacity, because borrowed nodes sit outside the quota aggregates by design.
There is deliberately no reuse of preempt_exempt_time here. That knob is
unbounded, and a user can raise their own by submitting to several partitions,
since the most protective matched partition wins. A cluster-wide
preempt_exempt_time = 3600 would protect every borrowed job from reclaim for an
hour, leaving a job with a real claim waiting for capacity that was lent away.
Which jobs may borrow#
A pending job is eligible only when its QOS group node cap is the sole limit it breaches. Every other limit is still enforced: other group TRES dimensions, per-job and per-user caps, max running jobs, max submit jobs, and the group wall budget.
This is stricter than it may first appear, and the strictness is the point. A job over both its node cap and its GPU cap must not be lent nodes, because doing so would let it escape a cap on a genuinely contended resource. Eligibility is therefore decided by re-running the full limit check with the node cap lifted, and treating the job as eligible only if it then passes — not by reading the reason code the job reports, which names only the first breach found.
Four kinds of job are never lent capacity:
Jobs with no effective time limit. A borrowed job with no bound would make its node appear busy indefinitely, and other jobs would plan around a slot years out. Set
default_time_limit_minutes, a partitionmax_time, or submit with-tif you want these jobs to be eligible.Heterogeneous job components, because a component placed on borrowed capacity can strand its own siblings.
Jobs requesting a burst buffer, whose staging state cannot be evaluated speculatively.
Jobs requesting licenses, which cannot be re-checked without risking a license an in-quota job claimed in the same scheduling pass.
Where borrowed jobs are placed#
Borrow candidates are offered only the capacity left after every in-quota job has taken what it can start on and reserved what it is waiting for. They are considered last, below every job with a claim, so a borrowed job can never take a node an in-quota job would have used, and it cannot displace a future slot the scheduler is holding.
A candidate that cannot start immediately simply does not start; it holds no future reservation. Under very deep queues, borrow candidates are the first thing dropped, so idle-fill degrades to off rather than crowding anyone out.
Quota accounting#
A borrowed run is held outside every quota aggregate, at both the association and the QOS gate: node and other TRES totals, occupied-node counts, running and submitted job counts, and array concurrency.
This is not cosmetic. If a borrowed job counted toward its own team’s quota, it would block that team’s next legitimate job at an admission gate — and a job blocked there is removed from the pending list before the scheduler ever sees it, so it could never trigger reclaim. The lent capacity could then never be recovered, which is the exact failure the feature exists to prevent.
One consequence to plan for: because borrowed nodes leave the aggregates, neither the QOS group node cap nor the association node cap bounds how much a scope may borrow. Only the amount of genuinely idle capacity does.
Note
A QOS group node cap counts distinct occupied nodes, not jobs. Two non-exclusive jobs that pack onto one node consume one node of quota, so a cap may never be reached at all by jobs that share nodes. Expect idle-fill to engage mainly for exclusive or whole-node work.
Reclaim#
When a job holding a quota claim cannot be placed, Spur looks for borrowed jobs whose eviction would let it run.
The set of jobs to evict must provably close the shortfall, or nothing is evicted. A node counts as recovered only when every job on it is reclaimable, so the outcome is all-or-nothing: the waiting job gets a full allocation, or the cluster is left untouched. A partial eviction would destroy work without helping anyone, and a job that can never be placed anywhere — asking for more GPUs than any node has, say — can never assemble such a set, and so evicts nothing, ever.
Reclaimed jobs are requeued, not cancelled. The job returns to the queue with
its spec intact and starts again when capacity allows, so the user loses the
partial run, not the work. --no-requeue does not prevent this; suspension is
not used, because a suspended job keeps its node allocation and would never
release the capacity.
The fate is always requeue, whatever PreemptMode the job’s QOS or partition
sets. A QOS with preemptmode=cancel still has its borrowed runs requeued, not
cancelled. The two other modes cannot serve reclaim: suspend never releases the
allocation, which is the entire point of reclaiming, and cancel destroys work the
job had no claim to lose when requeue preserves it at no cost to the reclaimer.
Reclaim also does not charge the lost run to fairshare. Charging it would lower the borrower’s priority for work it was not allowed to finish, which would make it the preferred next victim — the more capacity it lost, the more it would go on to lose.
Reclaim is attempted before preemption, since recovering lent capacity is always preferable to evicting a job that holds a claim to it.
Priority inside the opportunistic tier#
Borrowed runs and jobs in a QOS marked idlefillpreemptable=yes form one
opportunistic tier. Within it, QOS priority decides what happens, under two rules.
A reclaim takes the cheapest opportunistic run first. A job with a quota claim evicts the lowest-priority opportunistic run available — typically a burst job — and climbs to a dearer borrowed run only if that was not enough. Only as many nodes as the claim needs are taken.
An opportunistic job may only displace work cheaper than itself. A borrowed job
that needs nodes can reclaim from an idlefillpreemptable QOS of strictly lower
priority, so a high-priority idle-fill job outranks a low-priority burst job. The
reverse is refused: a burst job never displaces a dearer borrowed run.
The comparison is the QOS’s priority, not the job’s computed priority. A job’s
priority folds in an age factor and climbs while it waits, so a long-queued burst job
would otherwise outrank a freshly borrowed run and invert both rules. Standing within
the tier is a property of the QOS, which is stable.
This is what makes the burst pattern and idle-fill coexist while burst is phased out: a burst QOS sits at a large negative priority by construction, so it sorts below stamped idle-fill work without any special case.
What the guarantee becomes#
The partition setting preempt_mode = "off" documents that running jobs are
never kicked out. With idle-fill enabled that promise narrows to:
Jobs with a quota claim are never kicked out.
A reclaim driven by a job with a quota claim consults none of preempt_mode, the
priority gap, the QOS allow list, or preempt_exempt_time. It is not preemption:
preemption arbitrates between two jobs that both hold a claim, whereas a borrowed
job holds none. Priority does decide which borrowed run is taken, and an
opportunistic job reclaiming from another is bounded by its own QOS priority — see
Priority inside the opportunistic tier.
The alternative — refusing to lend capacity wherever reclaim is not permitted — was rejected because it would make idle-fill inert on a default-configured cluster. A borrowed job is defined by being reclaimable, so a setting that shielded it would not yield a safer job; it would yield capacity that was lent out and can never be recovered.
Two things bound the change. The master switch is off by default, so no existing
cluster behaves differently until an operator enables it. And the guarantee weakens
only for jobs that never had a claim to the capacity: for every job running inside
its quota, preempt_mode = "off" still means exactly what it says.
Relationship to burst QOS#
Spur already supports opportunistic borrowing through the burst-QOS pattern (see
Accounting, Accounts, Users, and QOS), where a low-priority QOS is made preemptable by other QOS via
preempt_type = qos_priority and allow-lists.
The two differ in who decides. The burst pattern is opt-in per job and requires the submitter to choose a burst QOS, and an operator to maintain the allow-lists. Idle-fill is automatic and derived from the quota a team already has: any job whose only obstacle is its group node cap is considered, with nothing to opt into.
A per-QOS flag, idle_fill_preemptable, marks a QOS whose jobs are always
reclaimable even while inside quota, so an existing burst QOS can be migrated onto
idle-fill’s reclaim path. Jobs in such a QOS still count fully toward every quota,
because they are running inside their own.
Set it with sacctmgr:
$ sacctmgr -i modify qos burst set idlefillpreemptable=yes
$ sacctmgr show qos format=Name,IdleFillPreemptable
Name IdleFillPreemptable
burst yes
normal no
The flag accepts yes/no, true/false, and 1/0. It defaults to
no on every QOS, including those created before this release, so enabling
idle-fill does not make any existing workload reclaimable until you say so.
Observing borrowed runs#
squeue reports it with the %W field:
$ squeue -o "%i %j %u %t %N %W"
JOBID NAME USER ST NODELIST BORROWED
18 alice-legit ifalice R node-3 no
19 bob-borrow ifbob R node-4 yes
An over-quota job that has not been lent capacity keeps reporting its real
reason, QOSGrpNodeLimit, rather than a placement reason — being over quota is
the truth about why it is waiting.
sacct --format=Borrowed records whether a completed run was borrowed, and a
reclaimed run is
reported as PREEMPTED with PreemptMode=Requeue and the job that reclaimed
it. See Monitoring & Controlling Jobs.