AI Infra Interviews logo
← Coding for Infra / 07
mediumOpenAIAnyscale

Implement a scheduler that admits jobs by priority and preempts lower-priority work when it must. What are the rules?

Implement priority admission and completion, preserve a preempted job’s sequence, and distinguish a sufficient greedy victim set from an optimal one.

Updated Sep 2026 · Learn how AI infrastructure works through explanations, worked examples and diagrams, then practise applying it to interview questions.

TL;DR: Order waiting jobs by priority and submission sequence. Start the head when it fits; otherwise consider only lower-priority running jobs for preemption. Choose victims according to a stated policy, and keep their original queue sequence when they return. A greedy victim list is not necessarily a minimum-cost or minimum-count set, and strict priority alone does not guarantee progress for every job.

How to approach it

Define the priority convention, resource model and preemption semantics before coding. Implement admission, completion and input validation, then trace an equal-priority arrival and a higher-priority arrival. Explain the fairness and physical-release behavior that a production scheduler would need beyond this in-memory model.

A strong answer

An eight-GPU pool runs two four-GPU jobs. A higher-priority four-GPU job arrives. Reclaiming one lower-priority job is sufficient; evicting both would add disruption without helping this placement. The scheduler needs to calculate the deficit and stop selecting victims when its policy has enough capacity.

GPU Job Scheduler Design explains admission and placement. This exercise uses one interchangeable resource count and models preemption as immediate. It does not send termination commands or manage real GPU ownership.

State the policy before choosing the data structure

Lower priority numbers mean more important jobs. Equal priorities use submission order. A waiting job can preempt only a strictly lower-priority running job. Among eligible victims, choose the lowest importance first and then the latest original submission. That tie-break is deterministic; it is not a measurement of runtime or lost progress.

import heapq
import itertools

class Scheduler:
    """Single-threaded count model; preemption releases capacity instantly."""
    def __init__(self, capacity):
        if type(capacity) is not int or capacity <= 0:
            raise ValueError("capacity must be a positive integer")
        self.capacity = capacity
        self.waiting = []  # (priority, submission sequence, ID, size)
        self.running = {}  # ID -> the same tuple
        self.active = set()
        self.sequence = itertools.count()

    def submit(self, job_id, priority, size):
        if not isinstance(job_id, str) or not job_id or job_id in self.active:
            raise ValueError("job ID must be nonempty and unique among active jobs")
        if type(priority) is not int:
            raise ValueError("priority must be an integer")
        if type(size) is not int or not 1 <= size <= self.capacity:
            raise ValueError("job size cannot fit this pool")
        job = (priority, next(self.sequence), job_id, size)
        self.active.add(job_id)
        heapq.heappush(self.waiting, job)
        return self._schedule()

    def finish(self, job_id):
        del self.running[job_id]  # KeyError if the job is not running
        self.active.remove(job_id)
        return [("finish", job_id)] + self._schedule()

    def _schedule(self):
        events = []
        while self.waiting:
            head = self.waiting[0]
            free = self.capacity - sum(j[3] for j in self.running.values())
            if head[3] <= free:
                heapq.heappop(self.waiting)
                self.running[head[2]] = head
                events.append(("start", head[2]))
                continue
            candidates = [j for j in self.running.values() if j[0] > head[0]]
            candidates.sort(key=lambda j: (-j[0], -j[1]))
            chosen, available = [], free
            for job in candidates:
                chosen.append(job)
                available += job[3]
                if available >= head[3]:
                    break
            if available < head[3]:
                break  # no mutation when this head cannot be admitted
            for job in chosen:
                del self.running[job[2]]
                heapq.heappush(self.waiting, job)  # original sequence survives
                events.append(("preempt", job[2]))
        return events

The heap uses Python's standard priority-queue ordering pattern. Validation rejects oversized jobs before they can permanently block this pool's queue. Completion frees capacity and reruns admission; without that path, waiting jobs would have no ordinary way to resume.

Trace ownership and order

s = Scheduler(8)
assert s.submit("low1", 5, 4) == [("start", "low1")]
assert s.submit("low2", 5, 4) == [("start", "low2")]
assert s.submit("high", 1, 4) == [("preempt", "low2"), ("start", "high")]
assert s.submit("low3", 5, 4) == []
assert s.finish("high") == [("finish", "high"), ("start", "low2")]
assert {j[2] for j in s.waiting} == {"low3"}
assert sum(j[3] for j in s.running.values()) == 8

low2 returns ahead of low3 because its original submission sequence remains smaller. This preserves their local tie order; it does not guarantee that either will run under an endless stream of higher-priority work.

Preemption preserves the original queue order

The toy eight-GPU pool models release as immediate.

  1. low1 and low2 each use four GPUs

    Both have priority 5; low2 was submitted later.

  2. A priority-1 job needs four GPUs

    The declared policy preempts low2 and starts high.

  3. high finishes; low2 resumes before low3

    The original sequence survives preemption, but higher-priority arrivals can still delay it.

A production system must verify old workers stopped before reusing their devices.

The selected victims are sufficient under this greedy order, not mathematically minimal:

Free = 0; incoming job needs 8 GPUs
Eligible victims in policy order = 3, 3, 8 GPUs
Greedy selection = 3 + 3 + 8 = 14 GPUs across three victims
Minimum-count alternative = the single 8-GPU victim
sanity: stopping as soon as the ordered prefix fits does not optimize over all subsets.

That may be an intentional preference for evicting lower-importance work before higher-importance work. If lost progress matters, define a cost model and compare candidate sets accordingly. Submission recency alone says little about checkpoint age, useful progress or restart cost.

Make the model's limits explicit

The loop stops when its head cannot fit. That can leave devices idle even when a smaller queued job could run. Backfill is an alternative, but protecting the head's expected start may require reservations and runtime assumptions. Multi-Tenancy, Quotas and Fair Share explains broader policy choices.

Aging can increase a waiting job's priority. For example, 5 - waited_hours reaches priority 1 after four hours. That equality does not promise a four-hour wait bound: capacity, running-job duration, tie-breaking and arrivals still matter. If effective priorities change over time, the queue must be updated or evaluated accordingly; this code implements no aging.

A real preemption also has a release sequence: request checkpoint or stop, confirm the old workers cannot use the devices, then admit the replacement. Requeueing a record is not confirmation of physical release. Use attempt identity and ownership controls so delayed messages from old workers cannot affect a new attempt. Waiting for a near-complete job can be better than reclaiming it, depending on urgency and lost work.

For complexity, candidate sorting costs up to O(R log R) for R running jobs per selection. Several admissions and requeues can occur in one call. The heap does not make the whole scheduling operation O(log N).

What interviewers probe next

  • “Does this choose the fewest victims?” No; it chooses a sufficient prefix under the declared priority policy.
  • “Can low-priority jobs starve?” Yes; progress guarantees require additional policy and feasibility assumptions.
  • “When may a real replacement start?” After the required placement and verified release of prior ownership.

Common mistakes

  • Calling submission sequence the time a job last started.
  • Treating a greedy prefix as an optimal victim set.
  • Claiming aging alone gives a fixed maximum wait.
  • Reusing devices when only the scheduler record says a victim stopped.

Key takeaways

  • In the eight-GPU trace, one four-GPU victim is sufficient and keeps its original queue position.
  • The 3, 3, 8 example shows why a sufficient victim prefix need not be minimal.
  • Keep ordering, fairness and physical resource ownership as explicit parts of the design.
You can read the first 10 answers per topic without an account. Signing in increases that to 20, includes lessons marked Plus, and saves your progress so you can return to topics you want to review.no card · Google sign-in · nothing to cancel
HOW DID IT GO?
0
READING SIGNED OUT

Signing in doubles your free answers, from 10 to 20 per topic, and the site starts remembering you: mastery per topic, bookmarks, and a next-focus recommendation. Free, no card.

Sign in free
UP NEXT ON YOUR JOURNEY
FEDITOR'S NOTE

Look for valid input handling, strict priority semantics, runnable state transitions and honest limits around fairness and physical release.

DISCUSSION · 0

No comments yet — be the first to share your approach.