KAI: why is my job pending?
A KAI "job" is a Pod (single) or PodGroup (gang). Its verdict is on the PodGroup's
.status.schedulingConditions, one condition per node-pool (nodePool names it); read each
condition's reasons[] - the top-level reason/message are deprecated. Walk the steps in order.
When a branch points to a references/ file, read that file and follow it before running
anything else - the procedure lives there, not here.
1. Rule out non-KAI causes - stop if any holds
Running / ContainerCreating / ImagePullBackOff / CrashLoopBackOff -> not scheduling;
check the image / volume / app.
SchedulingGated, spec.schedulingGates set, or Job.spec.suspend: true -> held by design ->
read scheduling-gates.
spec.schedulerName != kai-scheduler -> KAI never sees the pod (no PodGroup); set it.
- unbound PVC, native
ResourceQuota (not a KAI Queue), or cordoned node -> plain Kubernetes,
not KAI.
2. Fetch the verdict
PG=$(kubectl get pod <pod> -n <ns> -o jsonpath='{.metadata.annotations.pod-group-name}')
kubectl get podgroup "$PG" -n <ns> -o json \
| jq '.status.schedulingConditions[] | {nodePool, reasons: (.reasons | unique_by(.message))}'
- no
pod-group-name annotation -> never grouped -> step 5 (pod-grouper).
schedulingConditions empty -> no verdict yet; re-check. Persists -> scheduler down, or gated (step 1).
- populated ->
reasons[] repeats per cycle; unique_by(.message) dedupes, the richest message
(e.g. MaxNodePoolResources) is the verdict. Take .reason -> step 3.
3. Explain the reason to the user - read its .message, then:
For these reasons, guide the user (do not apply changes yourself):
QueueDoesNotExist -> suggest setting the kai.scheduler/queue label to an existing
queue, or creating it (+ parent). (default-queue named = no label at all.)
OverLimit (allocated + requested > limit) -> suggest waiting, lowering the request,
or raising the limit.
NonPreemptibleOverQuota (allocatedNP + requestedNP > deserved) -> suggest raising
quota, or making the workload preemptible. If preemptibility is not explicitly set,
suggest using a priority class with value < 100.
For the reason PodSchedulingErrors go to step 4.
4. PodSchedulingErrors - read the per-node fit detail
The default message is an aggregated histogram, to get per-node numbers
(requested / used / capacity) try to read this:
- per-node lines are in the condition
message
(<node-a>: Insufficient GPUs, requested: 2, used: 6, capacity: 8).
(available if installed with --detailed-fit-errors=true)
- otherwise the same detail is only in the scheduler log (
Full fit error: ...) -> step 5. (available if installed with high verbosity -v=6)
Match the per-node reason (or, with just the histogram, the short dimension):
- a node's
capacity >= request but used blocks it -> capacity is held by others -> contention /
preemption -> read fair-share.
capacity < request on every node -> too big for any single node -> read node-fit.
- a node-affinity / selector / taint predicate reason (not a resource shortage) -> affinity trap ->
read node-pool-affinity.
Resources were found for N pods while M are required for gang scheduling -> each pod fits but not
minMember at once -> read gang.
gpu-fraction / gpu-memory request -> fractional fit isn't decidable here yet. Treat whole-GPU fit as context only.
5. Object path silent - which component's logs
When the pod / PodGroup don't answer, read the owning component's logs - find its deployment with
kubectl -n <kai scheduler namespace> get deploy (names are install-specific):
- no PodGroup (
pod-group-name missing) -> pod-grouper (unknown owner, webhook reject, panic).
- PodGroup exists,
schedulingConditions stays empty, no event -> scheduler (no verdict produced).
PodSchedulingErrors histogram too vague -> scheduler at -v=6 carries the per-node Full fit error (same data --detailed-fit-errors=true puts in the verdict; step 4).
- scheduled but not Running (
BindRequest .status.phase: Failed) -> binder (reservation
timeout, scale-up, bind error).
kubectl logs keeps only recent lines.
RBAC
The verdict and fit errors are in your own PodGroup. fair-share needs cluster-scoped get queues;
the scheduler logs need read access in the scheduler's namespace. Lack them -> say so, don't guess.
1---2name: kai-pending3description: Use when a KAI-Scheduler pod or PodGroup is stuck Pending and you need to know why - GPU jobs that won't start, queue quota/limit, fair-share, gang scheduling, fractional GPU, node-pool affinity, or scheduling gates. Reads the PodGroup's scheduling verdict and the scheduler's own per-node fit errors.4license: MIT5---67# KAI: why is my job pending?89A KAI "job" is a Pod (single) or PodGroup (gang). Its verdict is on the PodGroup's10`.status.schedulingConditions`, one condition **per node-pool** (`nodePool` names it); read each11condition's `reasons[]` - the top-level `reason`/`message` are deprecated. Walk the steps in order.12When a branch points to a `references/` file, read that file and follow it before running13anything else - the procedure lives there, not here.1415## 1. Rule out non-KAI causes - stop if any holds1617- `Running` / `ContainerCreating` / `ImagePullBackOff` / `CrashLoopBackOff` -> not scheduling;18 check the image / volume / app.19- `SchedulingGated`, `spec.schedulingGates` set, or `Job.spec.suspend: true` -> held by design ->20 read [scheduling-gates](references/scheduling-gates.md).21- `spec.schedulerName != kai-scheduler` -> KAI never sees the pod (no PodGroup); set it.22- unbound PVC, native `ResourceQuota` (not a KAI `Queue`), or cordoned node -> plain Kubernetes,23 not KAI.2425## 2. Fetch the verdict2627```bash28PG=$(kubectl get pod <pod> -n <ns> -o jsonpath='{.metadata.annotations.pod-group-name}')29kubectl get podgroup "$PG" -n <ns> -o json \30 | jq '.status.schedulingConditions[] | {nodePool, reasons: (.reasons | unique_by(.message))}'31```3233- no `pod-group-name` annotation -> never grouped -> step 5 (pod-grouper).34- `schedulingConditions` empty -> no verdict yet; re-check. Persists -> scheduler down, or gated (step 1).35- populated -> `reasons[]` repeats per cycle; `unique_by(.message)` dedupes, the richest message36 (e.g. `MaxNodePoolResources`) is the verdict. Take `.reason` -> step 3.3738## 3. Explain the reason to the user - read its `.message`, then:3940For these reasons, guide the user (do not apply changes yourself):4142- `QueueDoesNotExist` -> suggest setting the `kai.scheduler/queue` label to an existing43 queue, or creating it (+ parent). (`default-queue` named = no label at all.)44- `OverLimit` (`allocated + requested > limit`) -> suggest waiting, lowering the request,45 or raising the limit.46- `NonPreemptibleOverQuota` (`allocatedNP + requestedNP > deserved`) -> suggest raising47 `quota`, or making the workload preemptible. If preemptibility is not explicitly set,48 suggest using a priority class with `value < 100`.4950For the reason `PodSchedulingErrors` go to step 4.5152## 4. PodSchedulingErrors - read the per-node fit detail5354The default message is an aggregated histogram, to get per-node numbers55(requested / used / capacity) try to read this:5657- per-node lines are in the condition `message`58 (`<node-a>: Insufficient GPUs, requested: 2, used: 6, capacity: 8`).59 (available if installed with `--detailed-fit-errors=true`)60- otherwise the same detail is only in the scheduler log (`Full fit error: ...`) -> step 5. (available if installed with high verbosity `-v=6`)6162Match the per-node reason (or, with just the histogram, the short dimension):6364- a node's `capacity` >= request but `used` blocks it -> capacity is held by others -> contention /65 preemption -> read [fair-share](references/fair-share.md).66- `capacity` < request on every node -> too big for any single node -> read [node-fit](references/node-fit.md).67- a node-affinity / selector / taint predicate reason (not a resource shortage) -> affinity trap ->68 read [node-pool-affinity](references/node-pool-affinity.md).69- `Resources were found for N pods while M are required for gang scheduling` -> each pod fits but not70 `minMember` at once -> read [gang](references/gang.md).71- `gpu-fraction` / `gpu-memory` request -> fractional fit isn't decidable here yet. Treat whole-GPU fit as context only.7273## 5. Object path silent - which component's logs7475When the pod / PodGroup don't answer, read the owning component's logs - find its deployment with76`kubectl -n <kai scheduler namespace> get deploy` (names are install-specific):7778- no PodGroup (`pod-group-name` missing) -> **pod-grouper** (unknown owner, webhook reject, panic).79- PodGroup exists, `schedulingConditions` stays empty, no event -> **scheduler** (no verdict produced).80- `PodSchedulingErrors` histogram too vague -> **scheduler** at `-v=6` carries the per-node `Full fit81 error` (same data `--detailed-fit-errors=true` puts in the verdict; step 4).82- scheduled but not Running (`BindRequest` `.status.phase: Failed`) -> **binder** (reservation83 timeout, scale-up, bind error).8485`kubectl logs` keeps only recent lines.8687## RBAC8889The verdict and fit errors are in your own PodGroup. fair-share needs cluster-scoped `get queues`;90the scheduler logs need read access in the scheduler's namespace. Lack them -> say so, don't guess.