|
| 1 | +<!-- |
| 2 | +Copyright 2026 NVIDIA CORPORATION |
| 3 | +SPDX-License-Identifier: Apache-2.0 |
| 4 | +--> |
| 5 | + |
| 6 | +# In-Place Pod Resize Accounting and Queue Admission |
| 7 | + |
| 8 | +*Status: Implemented* |
| 9 | + |
| 10 | +Related issues: [#1906](https://github.com/kai-scheduler/KAI-Scheduler/issues/1906), |
| 11 | +[#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872) (deferred resize eviction — separate track) |
| 12 | + |
| 13 | +## Motivation |
| 14 | + |
| 15 | +In-place resize (`pods/resize`) lets a running Pod change CPU/memory without |
| 16 | +rescheduling. KAI has three gaps: |
| 17 | + |
| 18 | +1. **Accounting.** KAI charges Pod resources from the spec. While a resize is |
| 19 | + pending, infeasible, or in progress, spec ≠ occupancy. That creates phantom |
| 20 | + capacity on downsize, overcharges on infeasible upsize, and corrupts queue |
| 21 | + allocation, fair share, reclaim, and reporting. |
| 22 | +2. **Queue admission.** Resize bypasses scheduling. A Pod can grow past queue |
| 23 | + `limit`, or past deserved `quota` when non-preemptible. The validating |
| 24 | + webhook matches `pods`, not `pods/resize`, so it never sees an old→new |
| 25 | + delta. |
| 26 | +3. **Deferred resize eviction.** When the kubelet marks a resize `Deferred` |
| 27 | + for node capacity, KAI does not preempt or reclaim on that node to make |
| 28 | + room. Out of scope here — tracked in |
| 29 | + [#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872). |
| 30 | + |
| 31 | +This design covers (1) and (2). Upstream ResourceQuota already admits resize |
| 32 | +via status-aware requests and a positive usage delta; KAI needs the same |
| 33 | +effective-request model and a hierarchical check. |
| 34 | + |
| 35 | +## Design |
| 36 | + |
| 37 | +### Effective accounting |
| 38 | + |
| 39 | +Use the upstream effective request as KAI's default Pod resource vector. Per |
| 40 | +container and resource, before Pod-level aggregation: |
| 41 | + |
| 42 | +```text |
| 43 | +normal / Deferred / in progress: |
| 44 | + effective = max(spec request, allocatedResources, status.resources) |
| 45 | +
|
| 46 | +Infeasible: |
| 47 | + effective = max(allocatedResources, status.resources) |
| 48 | +``` |
| 49 | + |
| 50 | +Drive node fit, queue `Allocated` / `AllocatedNotPreemptible`, fair share, |
| 51 | +ordering, reclaim, victim selection, and allocated status/metrics from this |
| 52 | +vector. Do not plumb a separate desired-resource vector through the |
| 53 | +scheduler; read the raw spec only for intent (proposed target, user-facing |
| 54 | +desired fields, infeasible diagnostics). |
| 55 | + |
| 56 | +**`Infeasible` detection** delegates to upstream |
| 57 | +`resource.IsPodResizeInfeasible`, which keys off the condition reason alone. |
| 58 | +The kubelet owns the condition lifecycle — it clears or replaces |
| 59 | +`PodResizePending` when a new resize is submitted — so generation tracking is |
| 60 | +not needed, and `observedGeneration` is only populated behind the non-GA |
| 61 | +`PodObservedGenerationTracking` gate (a guard on it would disable `Infeasible` |
| 62 | +handling on gate-off clusters). Upstream semantics are pinned by a |
| 63 | +characterization test so a future upstream tightening surfaces as a failure. |
| 64 | + |
| 65 | +The `Deferred` charge at `max(spec, actual)` is load-bearing beyond |
| 66 | +accounting: deferred-resize eviction |
| 67 | +([#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872), |
| 68 | +[#2051](https://github.com/kai-scheduler/KAI-Scheduler/pull/2051)) relies on |
| 69 | +the deferred target already being reserved in node and queue accounting, so |
| 70 | +capacity freed by evicting victims is not backfilled before the kubelet |
| 71 | +enacts the resize. Charging `Deferred` at actual only would reintroduce |
| 72 | +eviction thrash. |
| 73 | + |
| 74 | +Implementation: upstream `resource.AggregateContainerRequests` (status |
| 75 | +resources enabled), then KAI custom-resource logic. |
| 76 | + |
| 77 | +### Best-effort resize quota admission |
| 78 | + |
| 79 | +Validating webhook on `UPDATE`/`pods/resize` with old and proposed Pods. For |
| 80 | +KAI-scheduled Pods: |
| 81 | + |
| 82 | +1. Resolve PodGroup, preemptibility, leaf queue, and ancestors. |
| 83 | +2. Compute old and proposed effective requests (inherited `Infeasible` stale). |
| 84 | +3. `delta = max(proposed - old, 0)` per resource. |
| 85 | +4. Reject if any queue on the path would violate: |
| 86 | + |
| 87 | +```text |
| 88 | +all workloads: |
| 89 | + Allocated + delta > limit |
| 90 | +
|
| 91 | +non-preemptible: |
| 92 | + AllocatedNotPreemptible + delta > quota (deserved) |
| 93 | +``` |
| 94 | + |
| 95 | +Usage comes from the best-available snapshot (Queue status / live effective |
| 96 | +requests). Downsize always admissible; capacity frees only when effective |
| 97 | +usage falls. Limit-only changes with no request growth have zero delta. |
| 98 | +Rejections name Pod, PodGroup, limiting queue/ancestor, resource, allocation, |
| 99 | +delta, and boundary. |
| 100 | + |
| 101 | +This is **not** atomic across concurrent resizes or against in-flight |
| 102 | +scheduler allocations. Existing violations (race, bypass, pre-rollout) keep |
| 103 | +full effective accounting, are reported, and block further deepenings via the |
| 104 | +webhook and allocate-time capacity checks. No proactive "drain until under |
| 105 | +limit" action for now. |
| 106 | + |
| 107 | +### Optional strict upsize block |
| 108 | + |
| 109 | +Config knob, **disabled by default**, rejects request upsizes that could race |
| 110 | +past a hard queue bound — even if current usage would still fit: |
| 111 | + |
| 112 | +- **Any** upsize, if the leaf queue or an ancestor has a finite `limit` on that |
| 113 | + resource. |
| 114 | +- **Non-preemptible** upsize, if the leaf queue or an ancestor has a finite |
| 115 | + `quota` (deserved) on that resource. |
| 116 | + |
| 117 | +Escape hatch for operators who cannot tolerate best-effort races without a |
| 118 | +ledger. Preemptible upsizes on queues that are only bounded by deserved (no |
| 119 | +finite limit) remain subject to the best-effort check only — they may go over |
| 120 | +deserved by design. |
| 121 | + |
| 122 | +```yaml |
| 123 | +spec: |
| 124 | + admission: |
| 125 | + inPlacePodResize: |
| 126 | + validateQuota: true # best-effort hierarchical checks |
| 127 | + blockUpsizeOnBoundedQueues: false # conservative; default off |
| 128 | +``` |
| 129 | +
|
| 130 | +Exact CR field names TBD at implementation. |
| 131 | +
|
| 132 | +## Decisions |
| 133 | +
|
| 134 | +| Topic | Decision | |
| 135 | +| --- | --- | |
| 136 | +| Accounting model | Upstream effective request; upstream reason-only `Infeasible` semantics (kubelet owns the condition lifecycle) | |
| 137 | +| Resize admit path | Keep native `pods/resize`; validate in webhook (do **not** convert to a KAI-owned scheduling API — breaks VPA / API contract) | |
| 138 | +| Non-preemptible growth past quota | Reject at webhook; keep reject at allocate | |
| 139 | +| Concurrent-resize / scheduler races | Best effort; optional `blockUpsizeOnBoundedQueues`; reservation ledger only if a concrete issue appears | |
| 140 | +| Drain-until-under-limit action | Rejected — poor UX; reclaim stays demand-driven | |
| 141 | +| Deferred resize preemption | Separate track: [#1872](https://github.com/kai-scheduler/KAI-Scheduler/issues/1872) / [#2051](https://github.com/kai-scheduler/KAI-Scheduler/pull/2051). Depends on `Deferred` charged at max(spec, actual) — the reserved target is its thrash safety | |
| 142 | +| Wait for upstream resize gates / KEP-5836 only | Rejected as sole plan — ship Goals accounting + admit now; upstream remains complementary ([kubernetes#131835](https://github.com/kubernetes/kubernetes/issues/131835)) | |
| 143 | + |
| 144 | +## Known gaps |
| 145 | + |
| 146 | +Best-effort admission can race: concurrent upsizes, or a resize vs scheduler |
| 147 | +allocate, may double-spend the same remaining headroom. |
| 148 | +`blockUpsizeOnBoundedQueues` and allocate/webhook deepenings mitigate today. |
| 149 | +Prefer upstream resize gates |
| 150 | +([kubernetes#131835](https://github.com/kubernetes/kubernetes/issues/131835)) |
| 151 | +as the long-term out-of-tree quota hook; add a reservation ledger only if |
| 152 | +gates do not land and races hurt in production. |
| 153 | + |
| 154 | +## References |
| 155 | + |
| 156 | +- [KEP-1287: In-place Update of Pod Resources](https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/1287-in-place-update-pod-resources/README.md) |
| 157 | +- [Kubernetes 1.35 in-place container resize](https://v1-35.docs.kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) |
| 158 | +- [Upstream effective Pod request helper](https://github.com/kubernetes/kubernetes/blob/v1.35.4/staging/src/k8s.io/component-helpers/resource/helpers.go) |
| 159 | +- [Upstream Pod quota evaluator](https://github.com/kubernetes/kubernetes/blob/v1.35.4/pkg/quota/v1/evaluator/core/pods.go) |
| 160 | +- [KEP-5836: Scheduler Preemption for In-Place Pod Resize](https://www.kubernetes.dev/resources/keps/5836) |
| 161 | +- [kubernetes#131835: out-of-tree quota vs in-place resize](https://github.com/kubernetes/kubernetes/issues/131835) |
0 commit comments