183 lines
12 KiB
Markdown
183 lines
12 KiB
Markdown
# Brief: scheduler-preemption
|
|
|
|
## Problem
|
|
|
|
The XCE scheduler has no preemption mechanism. When a high-priority job cannot be scheduled because all resources are consumed by lower-priority running jobs, the scheduler has no way to free those resources. The job is simply pended, and it waits indefinitely — regardless of its priority — until running jobs naturally complete.
|
|
|
|
Three specific gaps exist:
|
|
|
|
1. **No preemption decision type** — the `xce_decision_op_t` enum has `DISPATCH` and `PEND` but no `PREEMPT`. The scheduler cannot communicate a preemption intent to the controller.
|
|
2. **No victim selection mechanism** — even if a preemption decision type existed, there is no logic to select which running jobs should be preempted. Cross-queue victim selection (a high-priority job in queue A preempting a low-priority job in queue B) is entirely absent.
|
|
3. **No SIGSTP pipeline** — the controller and worker have no path to deliver a stop signal to a running job and transition its state to `JOB_SUSPENDED`, freeing its resources for the preempting job.
|
|
|
|
Operators with mixed-priority workloads need preemption to ensure critical work proceeds without manual intervention — the operator should not have to manually kill low-priority jobs to make room.
|
|
|
|
## Current State
|
|
|
|
### What exists
|
|
- **Decision framework** (`decision.h`): `xce_decision_op_t` with `DISPATCH`, `PEND`, and `NONE` ops. Core/decision callback flows to driver/controller.
|
|
- **Queue structure** (`xce_sched_queue_t`): has `priority` field for inter-queue ordering within the picker, but no preemption-related fields.
|
|
- **HOOK_DETAIL_JOB_STOP** (enum value 65 in `cluster_def.h`): existing hook detail enum, reused for preemption stop notifications.
|
|
- **Controller→worker messaging**: existing message pipeline for dispatch/pend decisions; the same path can carry preemption actions to workers.
|
|
|
|
### What's missing
|
|
- `XCE_DECISION_PREEMPT` opcode and `xce_preempt_decision_t` payload in `decision.h`
|
|
- Victim selection policy: which running jobs get preempted for a given high-priority job
|
|
- Per-queue preemption configuration: which queues can preempt from which other queues, which queues are immune to preemption
|
|
- SIGSTP delivery pipeline: scheduler → controller → worker → process group
|
|
- Job state transition to `JOB_SUSPENDED` triggered by preemption
|
|
- Integration of preemption into the scheduling loop: when allocation fails, attempt preemption before pended
|
|
|
|
## Desired Outcome
|
|
|
|
A job preemption system with three layers:
|
|
|
|
**1. Decision type** — `XCE_DECISION_PREEMPT` carries victim jobids, signal type, and preempting job id through the core's decision callback → normal-mode driver → controller → worker pipeline, reusing the same callback infrastructure (`xce_core_decision_cb_t`) used by `DISPATCH` and `PEND`.
|
|
|
|
**2. Victim selection** — a `preempt_policy` plugin on each queue selects which running jobs to preempt. The default policy `"lowest_priority_first"` implements LSF-style cross-queue victim selection:
|
|
- Collect running jobs from the preempting job's `preemptible_queues` list
|
|
- Filter: victim priority < preempting job priority AND victim's queue has `preemptible == true`
|
|
- Rank by priority ascending (lowest priority first)
|
|
- Select the minimum subset whose freed resources satisfy the preempting job's resreq
|
|
|
|
**3. SIGSTP pipeline** — the controller receives `XCE_DECISION_PREEMPT`, delivers `HOOK_DETAIL_JOB_STOP` to each victim's worker, the worker sends SIGSTP to the job's process group, the controller transitions the job to `JOB_SUSPENDED`, and resources are released. The preempting job is pended and retried in the next scheduling round (async preemption).
|
|
|
|
### Queue Preemption Configuration
|
|
|
|
Each queue carries three new fields:
|
|
|
|
| Field | Type | Purpose |
|
|
|-------|------|---------|
|
|
| `preemptible_queues` | `char**` | List of queue names this queue CAN preempt from |
|
|
| `preemptible` | `bool` | Whether this queue's jobs CAN BE preempted by other queues |
|
|
| `preempt_policy` | `char*` | Victim-selection plugin name (NULL = use default) |
|
|
|
|
## Alternatives Considered
|
|
|
|
### Approach A: Kill-and-requeue only (rejected)
|
|
|
|
| Aspect | Detail |
|
|
|--------|--------|
|
|
| **Mechanism** | SIGKILL victims, requeue from scratch |
|
|
| **Pros** | Simple, no suspended state, no resume logic |
|
|
| **Cons** | Destroys progress, lose all intermediate computation. Requeued jobs restart from zero. Not viable for long-running HPC workloads where checkpointing is explicit. |
|
|
|
|
**Rejected because:** HPC jobs can run for hours or days. Killing a job that is 90% complete to make room for a higher-priority job wastes all accumulated work. SIGSTP + resume preserves progress when the preempted job is later resumed.
|
|
|
|
### Approach B: SIGSTP with suspend/resume (chosen)
|
|
|
|
| Aspect | Detail |
|
|
|--------|--------|
|
|
| **Mechanism** | SIGSTP to process group → job → `JOB_SUSPENDED` → resources freed → preempting job runs → preempted job optionally resumed later |
|
|
| **Victim selection** | Policy plugin per queue, default `"lowest_priority_first"` |
|
|
| **Cross-queue** | Per-queue `preemptible_queues` list enables cross-queue preemption |
|
|
| **Scope** | v1 is SIGSTP only; kill-and-requeue escalation deferred |
|
|
|
|
**Chosen because:** Preserves victim job progress. Enables later resume (outside this spec's scope). Aligns with LSF-style preemption semantics familiar to HPC operators. The async pattern (pend preempting job, retry next round after SIGSTP) keeps the scheduling loop simple.
|
|
|
|
## Approach
|
|
|
|
### Preemption Flow
|
|
|
|
```
|
|
scheduler controller worker
|
|
─────────── ────────── ──────
|
|
alloc fails for high-prio job J
|
|
|
|
preempt_policy.select_victims(J):
|
|
collect running jobs from
|
|
Q_aggressor.preemptible_queues
|
|
filter: victim.prio < J.prio
|
|
filter: victim.queue.preemptible
|
|
rank: lowest priority first
|
|
select minimum set satisfying J.resreq
|
|
|
|
→ [victim1, victim2]
|
|
|
|
XCE_DECISION_PREEMPT ────────────▶ handle_job_preempt() ──────▶ SIGSTP victim1
|
|
.victim_jobids = [v1, v2] deliver to each job → JOB_SUSPENDED
|
|
.signal = SIGSTP victim's worker release resources
|
|
.preempting_jobid = J
|
|
|
|
J pended → next round → retry alloc
|
|
```
|
|
|
|
### Integration into Scheduling Loop
|
|
|
|
Preemption fires at Level 3 in the core scheduling loop — after allocation failure, before pending:
|
|
|
|
```c
|
|
while ((job = picker->next_job(picker)) != NULL) {
|
|
if (try_allocate(job) > 0) {
|
|
emit_decision(DISPATCH, job);
|
|
picker->on_dispatch(picker, job->jobid);
|
|
} else if (can_preempt(job)) {
|
|
victims = select_victims(job);
|
|
emit_decision(PREEMPT, job, victims);
|
|
picker->on_pend(picker, job->jobid); // retry next round
|
|
} else {
|
|
emit_decision(PEND, job);
|
|
picker->on_pend(picker, job->jobid);
|
|
}
|
|
}
|
|
```
|
|
|
|
The core loop itself contains no victim selection logic — that lives in the `preempt_policy` plugin. The core only checks `can_preempt(job)` (does the job's queue have a non-NULL `preemptible_queues` list?) and delegates to the policy plugin for victim selection.
|
|
|
|
### What stays unchanged
|
|
- Core scheduling loop — preemption is an additional branch in the alloc-fail path (Level 3), not a restructure
|
|
- Decision callback (`xce_core_decision_cb_t`) — `PREEMPT` flows through the same callback as `DISPATCH` and `PEND`
|
|
- Existing plugin framework — `preempt_policy` is registered, loaded, and invoked through the existing `plugin_register`/`key_find` mechanism
|
|
- `HOOK_DETAIL_JOB_STOP` — existing enum value, reused without change
|
|
|
|
### What's new
|
|
- `XCE_DECISION_PREEMPT` in `xce_decision_op_t` (`decision.h`)
|
|
- `xce_preempt_decision_t` payload carrying `victim_jobids[]`, `n_victims`, `signal`, `preempting_jobid`
|
|
- `preemptible_queues`, `preemptible`, `preempt_policy` fields on `xce_sched_queue_t`
|
|
- Default `"lowest_priority_first"` preemption policy plugin
|
|
- Controller handler for `XCE_DECISION_PREEMPT` → SIGSTP delivery to workers
|
|
- Worker-side SIGSTP handler → process group signal + `JOB_SUSPENDED` state transition
|
|
|
|
## Scope
|
|
|
|
### In
|
|
- `XCE_DECISION_PREEMPT` decision type and `xce_preempt_decision_t` payload
|
|
- `preempt_policy` plugin interface (victim selection)
|
|
- Default `"lowest_priority_first"` preemption policy
|
|
- LSF-style cross-queue victim selection via `preemptible_queues` list
|
|
- SIGSTP pipeline: scheduler → controller → worker → process group
|
|
- Per-queue preemption config: `preemptible_queues`, `preemptible`, `preempt_policy`
|
|
- Victim safety guards: never preempt same-or-higher priority, never preempt non-`preemptible` queues
|
|
- `JOB_SUSPENDED` state transition on victim jobs
|
|
- Reuse of `HOOK_DETAIL_JOB_STOP` for preemption stop notifications
|
|
- Unit tests: victim selection policy, safety guard enforcement, edge cases (empty target queues, no eligible victims, insufficient resources from any victim set)
|
|
- Integration test: end-to-end preemption flow — high-priority job submits, preemption fires, victims get SIGSTP, resources freed, preempting job dispatched in next round
|
|
|
|
### Out
|
|
- Kill-and-requeue escalation (v1 is SIGSTP only)
|
|
- Deadline-based preemption (requires `deadline` field on jobs — deferred)
|
|
- Preempted job resume logic (outside this spec's scope — the `JOB_SUSPENDED` state is defined, but the resume mechanism is a separate feature)
|
|
- Preemption priority inversion guards beyond the priority comparison already specified
|
|
- Fair-share or proportional victim selection beyond the default `"lowest_priority_first"` policy (the plugin interface enables custom policies, but none beyond the default ship here)
|
|
|
|
## Upstream / Downstream
|
|
|
|
### Upstream (dependencies)
|
|
- **scheduler-core** (sibling spec): provides the decision callback (`xce_core_decision_cb_t`) through which `XCE_DECISION_PREEMPT` flows. The core scheduling loop and `xce_sched_core_t` are defined there. Preemption hooks into the core loop at Level 3 (alloc-fail branch).
|
|
- **scheduler-allocation** (sibling spec): provides `try_allocate()` — preemption fires only when allocation fails. The resreq fingerprint cache interacts with preemption (a cached-unsatisfiable resreq skips allocation AND preemption).
|
|
- **Controller message handling**: existing handler dispatch path for decisions — extended to handle `XCE_DECISION_PREEMPT`.
|
|
- **Worker job control**: existing job lifecycle management — extended to handle SIGSTP delivery and `JOB_SUSPENDED`.
|
|
|
|
### Downstream (future consumers)
|
|
- **Resume mechanism** — a future spec for resuming `JOB_SUSPENDED` jobs, potentially with priority-based ordering
|
|
- **Kill-and-requeue escalation** — a future enhancement where repeated preemption failure escalates to SIGKILL
|
|
- **Custom preemption policies** — deadline-aware, fair-share, or resource-specific victim selection implemented as new `preempt_policy` plugins
|
|
|
|
## Constraints
|
|
- **C (gnu17)** — no C++, no external libraries beyond existing (pthreads)
|
|
- **Async preemption** — the preempting job is pended and retried in the next scheduling round; preemption is not synchronous
|
|
- **Priority guard** — victim priority must be strictly less than preempting job priority (never preempt equal or higher)
|
|
- **Preemptible guard** — victim queue must have `preemptible == true` (opt-in — queues are not preemptible by default)
|
|
- **Minimal victim set** — select the minimum subset of victims whose freed resources satisfy the preempting job's resreq (avoid over-preemption)
|
|
- **No self-preemption** — the preempting job's own queue is implicitly excluded from victim selection
|
|
- **Cancel-safe** — `preempt_policy.select_victims()` must have `pthread_cancel` cancellation points
|